Fix toast hydration
This commit is contained in:
parent
eb77c00540
commit
ef3e8f38d6
9 changed files with 855 additions and 931 deletions
|
@ -2,6 +2,9 @@
|
|||
import FilePreview from "./universal/FilePreview";
|
||||
import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||
import EventLoad from "./EventsSection/EventLoad";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { Get } from "../../scripts/pocketbase/Get";
|
||||
import { toast } from "react-hot-toast";
|
||||
---
|
||||
|
||||
<div id="" class="">
|
||||
|
@ -28,7 +31,8 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
<div
|
||||
class="absolute inset-0 bg-base-100 opacity-0 group-hover:opacity-90 transition-opacity duration-300 flex items-center justify-center z-10"
|
||||
>
|
||||
<span class="text-base-content font-medium text-sm sm:text-base"
|
||||
<span
|
||||
class="text-base-content font-medium text-sm sm:text-base"
|
||||
>Coming Soon</span
|
||||
>
|
||||
</div>
|
||||
|
@ -48,8 +52,12 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
disabled
|
||||
>
|
||||
<option disabled selected>Pick an event</option>
|
||||
<option>Technical Workshop - Web Development</option>
|
||||
<option>Professional Development Workshop</option>
|
||||
<option
|
||||
>Technical Workshop - Web Development</option
|
||||
>
|
||||
<option
|
||||
>Professional Development Workshop</option
|
||||
>
|
||||
<option>Social Event - Game Night</option>
|
||||
</select>
|
||||
<button
|
||||
|
@ -89,8 +97,9 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
class="btn btn-circle btn-ghost btn-sm sm:btn-md"
|
||||
onclick="window.closeEventDetailsModal()"
|
||||
>
|
||||
<iconify-icon icon="heroicons:x-mark" className="h-4 w-4 sm:h-6 sm:w-6"
|
||||
></iconify-icon>
|
||||
<iconify-icon
|
||||
icon="heroicons:x-mark"
|
||||
className="h-4 w-4 sm:h-6 sm:w-6"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -124,7 +133,8 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
id="previewLoadingSpinner"
|
||||
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
|
||||
>
|
||||
<span class="loading loading-spinner loading-md sm:loading-lg"></span>
|
||||
<span class="loading loading-spinner loading-md sm:loading-lg"
|
||||
></span>
|
||||
</div>
|
||||
<div id="previewContent" class="w-full">
|
||||
<FilePreview client:load isModal={true} />
|
||||
|
@ -137,109 +147,26 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
</dialog>
|
||||
|
||||
<script>
|
||||
import { toast } from "react-hot-toast";
|
||||
import JSZip from "jszip";
|
||||
|
||||
// Toast management system
|
||||
const createToast = (
|
||||
message: string,
|
||||
type: "success" | "error" | "warning" = "success",
|
||||
) => {
|
||||
let toastContainer = document.querySelector(".toast-container");
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement("div");
|
||||
toastContainer.className = "toast-container fixed bottom-4 right-4 z-50";
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
const existingToasts = document.querySelectorAll(".toast-container .toast");
|
||||
if (existingToasts.length >= 2) {
|
||||
const oldestToast = existingToasts[0];
|
||||
oldestToast.classList.add("toast-exit");
|
||||
setTimeout(() => oldestToast.remove(), 150);
|
||||
}
|
||||
|
||||
// Update positions of existing toasts
|
||||
existingToasts.forEach((t) => {
|
||||
const toast = t as HTMLElement;
|
||||
const currentIndex = parseInt(toast.getAttribute("data-index") || "0");
|
||||
toast.setAttribute("data-index", (currentIndex + 1).toString());
|
||||
});
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "toast translate-x-full";
|
||||
toast.setAttribute("data-index", "0");
|
||||
|
||||
// Update alert styling based on type
|
||||
const alertClass =
|
||||
type === "success"
|
||||
? "alert-success bg-success text-success-content"
|
||||
: type === "error"
|
||||
? "alert-error bg-error text-error-content"
|
||||
: "alert-warning bg-warning text-warning-content";
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="alert ${alertClass} shadow-lg min-w-[300px]">
|
||||
<div class="flex items-center gap-2">
|
||||
${
|
||||
type === "success"
|
||||
? '<iconify-icon icon="heroicons:check-circle" class="stroke-current shrink-0 h-6 w-6"></iconify-icon>'
|
||||
: type === "error"
|
||||
? '<iconify-icon icon="heroicons:x-circle" class="stroke-current shrink-0 h-6 w-6"></iconify-icon>'
|
||||
: '<iconify-icon icon="heroicons:exclamation-triangle" class="stroke-current shrink-0 w-6"></iconify-icon>'
|
||||
}
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Force a reflow to ensure the animation triggers
|
||||
toast.offsetHeight;
|
||||
|
||||
// Add the transition class and remove transform
|
||||
toast.classList.add("transition-all", "duration-300", "ease-out");
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.remove("translate-x-full");
|
||||
});
|
||||
|
||||
// Setup exit animation
|
||||
setTimeout(() => {
|
||||
toast.classList.add("toast-exit");
|
||||
setTimeout(() => toast.remove(), 150);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// Add styles to the document
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.toast-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: none;
|
||||
/* Custom styles for the event details modal */
|
||||
.event-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.toast {
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.event-details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.toast-exit {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
.toast.translate-x-full {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.toast-container .toast {
|
||||
transform: translateY(calc((1 - attr(data-index number)) * -0.25rem));
|
||||
}
|
||||
.toast-container .toast[data-index="0"] {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.toast-container .toast[data-index="1"] {
|
||||
transform: translateY(-0.025rem);
|
||||
}
|
||||
|
||||
/* Remove custom toast styles since we're using react-hot-toast */
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
|
@ -269,7 +196,7 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
window.previewFileEvents = function (url: string, filename: string) {
|
||||
console.log("previewFileEvents called with:", { url, filename });
|
||||
const modal = document.getElementById(
|
||||
"filePreviewModal",
|
||||
"filePreviewModal"
|
||||
) as HTMLDialogElement;
|
||||
const previewFileName = document.getElementById("previewFileName");
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
|
@ -286,7 +213,7 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
window.dispatchEvent(
|
||||
new CustomEvent("filePreviewStateChange", {
|
||||
detail: { url, filename },
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -295,7 +222,7 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
window.closeFilePreviewEvents = function () {
|
||||
console.log("closeFilePreviewEvents called");
|
||||
const modal = document.getElementById(
|
||||
"filePreviewModal",
|
||||
"filePreviewModal"
|
||||
) as HTMLDialogElement;
|
||||
const previewFileName = document.getElementById("previewFileName");
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
|
@ -306,7 +233,7 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
window.dispatchEvent(
|
||||
new CustomEvent("filePreviewStateChange", {
|
||||
detail: { url: "", filename: "" },
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// Reset the UI
|
||||
|
@ -329,10 +256,10 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
// Update the openDetailsModal function to use the events-specific preview
|
||||
window.openDetailsModal = function (event: any) {
|
||||
const modal = document.getElementById(
|
||||
"eventDetailsModal",
|
||||
"eventDetailsModal"
|
||||
) as HTMLDialogElement;
|
||||
const filesContent = document.getElementById(
|
||||
"filesContent",
|
||||
"filesContent"
|
||||
) as HTMLDivElement;
|
||||
|
||||
// Check if event has ended
|
||||
|
@ -340,10 +267,14 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
const now = new Date();
|
||||
|
||||
if (eventEndDate > now) {
|
||||
createToast(
|
||||
"Files are only available after the event has ended.",
|
||||
"warning",
|
||||
);
|
||||
toast("Files are only available after the event has ended.", {
|
||||
icon: "⚠️",
|
||||
style: {
|
||||
borderRadius: "10px",
|
||||
background: "#FFC107",
|
||||
color: "#000",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -352,7 +283,11 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
if (filesContent) filesContent.classList.remove("hidden");
|
||||
|
||||
// Populate files content
|
||||
if (event.files && Array.isArray(event.files) && event.files.length > 0) {
|
||||
if (
|
||||
event.files &&
|
||||
Array.isArray(event.files) &&
|
||||
event.files.length > 0
|
||||
) {
|
||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||
const collectionId = "events";
|
||||
const recordId = event.id;
|
||||
|
@ -409,7 +344,7 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
// Add downloadAllFiles function
|
||||
window.downloadAllFiles = async function () {
|
||||
const downloadBtn = document.getElementById(
|
||||
"downloadAllBtn",
|
||||
"downloadAllBtn"
|
||||
) as HTMLButtonElement;
|
||||
if (!downloadBtn) return;
|
||||
const originalBtnContent = downloadBtn.innerHTML;
|
||||
|
@ -460,12 +395,11 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
// Show success message
|
||||
createToast("Files downloaded successfully!", "success");
|
||||
toast.success("Files downloaded successfully!");
|
||||
} catch (error: any) {
|
||||
console.error("Failed to download files:", error);
|
||||
createToast(
|
||||
error?.message || "Failed to download files. Please try again.",
|
||||
"error",
|
||||
toast.error(
|
||||
error?.message || "Failed to download files. Please try again."
|
||||
);
|
||||
} finally {
|
||||
// Reset button state
|
||||
|
@ -477,7 +411,7 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
// Close event details modal
|
||||
window.closeEventDetailsModal = function () {
|
||||
const modal = document.getElementById(
|
||||
"eventDetailsModal",
|
||||
"eventDetailsModal"
|
||||
) as HTMLDialogElement;
|
||||
const filesContent = document.getElementById("filesContent");
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
|||
import { DataSyncService } from "../../../scripts/database/DataSyncService";
|
||||
import { Collections } from "../../../schemas/pocketbase/schema";
|
||||
import { Icon } from "@iconify/react";
|
||||
import toast from "react-hot-toast";
|
||||
import type { Event, AttendeeEntry } from "../../../schemas/pocketbase";
|
||||
|
||||
// Extended Event interface with additional properties needed for this component
|
||||
|
@ -17,77 +18,6 @@ interface ExtendedEvent extends Event {
|
|||
// When fetching events, UTC dates are converted to local time.
|
||||
// When saving events, local dates are converted back to UTC.
|
||||
|
||||
// Toast management system
|
||||
const createToast = (
|
||||
message: string,
|
||||
type: "success" | "error" | "warning" = "success"
|
||||
) => {
|
||||
let toastContainer = document.querySelector(".toast-container");
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement("div");
|
||||
toastContainer.className = "toast-container fixed bottom-4 right-4 z-50";
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
const existingToasts = document.querySelectorAll(".toast-container .toast");
|
||||
if (existingToasts.length >= 2) {
|
||||
const oldestToast = existingToasts[0];
|
||||
oldestToast.classList.add("toast-exit");
|
||||
setTimeout(() => oldestToast.remove(), 150);
|
||||
}
|
||||
|
||||
// Update positions of existing toasts
|
||||
existingToasts.forEach((t) => {
|
||||
const toast = t as HTMLElement;
|
||||
const currentIndex = parseInt(toast.getAttribute("data-index") || "0");
|
||||
toast.setAttribute("data-index", (currentIndex + 1).toString());
|
||||
});
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "toast translate-x-full";
|
||||
toast.setAttribute("data-index", "0");
|
||||
|
||||
// Update alert styling based on type
|
||||
const alertClass =
|
||||
type === "success"
|
||||
? "alert-success bg-success text-success-content"
|
||||
: type === "error"
|
||||
? "alert-error bg-error text-error-content"
|
||||
: "alert-warning bg-warning text-warning-content";
|
||||
|
||||
const iconName = type === "success"
|
||||
? "heroicons:check-circle"
|
||||
: type === "error"
|
||||
? "heroicons:x-circle"
|
||||
: "heroicons:exclamation-triangle";
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="alert ${alertClass} shadow-lg min-w-[300px]">
|
||||
<div class="flex items-center gap-2">
|
||||
<iconify-icon icon="${iconName}" width="20" height="20"></iconify-icon>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Force a reflow to ensure the animation triggers
|
||||
toast.offsetHeight;
|
||||
|
||||
// Add the transition class and remove transform
|
||||
toast.classList.add("transition-all", "duration-300", "ease-out");
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.remove("translate-x-full");
|
||||
});
|
||||
|
||||
// Setup exit animation
|
||||
setTimeout(() => {
|
||||
toast.classList.add("toast-exit");
|
||||
setTimeout(() => toast.remove(), 150);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const EventCheckIn = () => {
|
||||
const [currentCheckInEvent, setCurrentCheckInEvent] = useState<Event | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -101,7 +31,7 @@ const EventCheckIn = () => {
|
|||
|
||||
const currentUser = auth.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
createToast("You must be logged in to check in to events", "error");
|
||||
toast.error("You must be logged in to check in to events");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -151,7 +81,7 @@ const EventCheckIn = () => {
|
|||
await completeCheckIn(event, null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
createToast(error?.message || "Failed to check in to event", "error");
|
||||
toast.error(error?.message || "Failed to check in to event");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,7 +101,7 @@ const EventCheckIn = () => {
|
|||
const userId = auth.getUserId();
|
||||
|
||||
if (!userId) {
|
||||
createToast("You must be logged in to check in to an event", "error");
|
||||
toast.error("You must be logged in to check in to an event");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -184,7 +114,14 @@ const EventCheckIn = () => {
|
|||
);
|
||||
|
||||
if (isAlreadyCheckedIn) {
|
||||
createToast("You are already checked in to this event", "warning");
|
||||
toast("You are already checked in to this event", {
|
||||
icon: '⚠️',
|
||||
style: {
|
||||
borderRadius: '10px',
|
||||
background: '#FFC107',
|
||||
color: '#000',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -243,12 +180,11 @@ const EventCheckIn = () => {
|
|||
}
|
||||
|
||||
// Show success message with points if awarded
|
||||
createToast(
|
||||
toast.success(
|
||||
`Successfully checked in to ${event.event_name}${event.points_to_reward > 0
|
||||
? ` (+${event.points_to_reward} points!)`
|
||||
: ""
|
||||
}`,
|
||||
"success"
|
||||
}`
|
||||
);
|
||||
|
||||
// Log the check-in
|
||||
|
@ -265,7 +201,7 @@ const EventCheckIn = () => {
|
|||
setFoodInput("");
|
||||
}
|
||||
} catch (error: any) {
|
||||
createToast(error?.message || "Failed to check in to event", "error");
|
||||
toast.error(error?.message || "Failed to check in to event");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -296,7 +232,14 @@ const EventCheckIn = () => {
|
|||
input.value = "";
|
||||
});
|
||||
} else {
|
||||
createToast("Please enter an event code", "warning");
|
||||
toast("Please enter an event code", {
|
||||
icon: '⚠️',
|
||||
style: {
|
||||
borderRadius: '10px',
|
||||
background: '#FFC107',
|
||||
color: '#000',
|
||||
},
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
|
|
|
@ -4,7 +4,10 @@ import { Get } from "../../scripts/pocketbase/Get";
|
|||
import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
|
||||
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
|
||||
import { Collections } from "../../schemas/pocketbase/schema";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { Create } from "../../scripts/pocketbase/Create";
|
||||
import { Update } from "../../scripts/pocketbase/Update";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
// Import the EventRequest type from UserEventRequests to ensure consistency
|
||||
import type { EventRequest as UserEventRequest } from "./Officer_EventRequestForm/UserEventRequests";
|
||||
|
@ -118,8 +121,9 @@ if (auth.isAuthenticated()) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for notifications -->
|
||||
<Toaster client:load position="bottom-right" />
|
||||
<div class="dashboard-section hidden" id="eventRequestFormSection">
|
||||
<!-- ... existing code ... -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Import the DataSyncService for client-side use
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
---
|
||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||
import { Get } from "../../scripts/pocketbase/Get";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { toast } from "react-hot-toast";
|
||||
import EventRequestManagementTable from "./Officer_EventRequestManagement/EventRequestManagementTable";
|
||||
import type { EventRequest } from "../../schemas/pocketbase";
|
||||
import { Collections } from "../../schemas/pocketbase/schema";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
// Get instances
|
||||
const get = Get.getInstance();
|
||||
|
@ -152,9 +153,6 @@ try {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Toast container for notifications -->
|
||||
<Toaster client:load position="bottom-right" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
||||
|
@ -7,7 +7,6 @@ import ReceiptForm from './ReceiptForm';
|
|||
import { toast } from 'react-hot-toast';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import FilePreview from '../universal/FilePreview';
|
||||
import ToastProvider from './ToastProvider';
|
||||
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
||||
|
||||
interface ReceiptFormData {
|
||||
|
@ -310,7 +309,6 @@ export default function ReimbursementForm() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ToastProvider />
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
|
|
|
@ -6,7 +6,6 @@ import { FileManager } from '../../../scripts/pocketbase/FileManager';
|
|||
import FilePreview from '../universal/FilePreview';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import ToastProvider from './ToastProvider';
|
||||
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
||||
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
||||
import { Collections } from '../../../schemas/pocketbase/schema';
|
||||
|
@ -355,7 +354,6 @@ export default function ReimbursementList() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ToastProvider />
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
export default function ToastProvider() {
|
||||
return (
|
||||
<Toaster
|
||||
position="top-center"
|
||||
reverseOrder={false}
|
||||
toastOptions={{
|
||||
duration: 5000,
|
||||
style: {
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
31
src/components/dashboard/universal/ToastProvider.tsx
Normal file
31
src/components/dashboard/universal/ToastProvider.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
// Centralized toast provider to ensure consistent rendering
|
||||
export default function ToastProvider() {
|
||||
return (
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
},
|
||||
success: {
|
||||
style: {
|
||||
background: 'green',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
style: {
|
||||
background: 'red',
|
||||
},
|
||||
duration: 5000,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,7 @@ import { SendLog } from "../scripts/pocketbase/SendLog";
|
|||
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
||||
import { OfficerTypes } from "../schemas/pocketbase/schema";
|
||||
import { initAuthSync } from "../scripts/database/initAuthSync";
|
||||
import ToastProvider from "../components/dashboard/universal/ToastProvider";
|
||||
|
||||
const title = "Dashboard";
|
||||
|
||||
|
@ -365,10 +366,11 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
<!-- Centralized Toast Provider -->
|
||||
<ToastProvider client:load />
|
||||
|
||||
<script>
|
||||
import { Authentication } from "../scripts/pocketbase/Authentication";
|
||||
import { Get } from "../scripts/pocketbase/Get";
|
||||
import { SendLog } from "../scripts/pocketbase/SendLog";
|
||||
|
@ -381,7 +383,8 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
const logger = SendLog.getInstance();
|
||||
|
||||
// Initialize page state
|
||||
const pageLoadingState = document.getElementById("pageLoadingState");
|
||||
const pageLoadingState =
|
||||
document.getElementById("pageLoadingState");
|
||||
const pageErrorState = document.getElementById("pageErrorState");
|
||||
const notAuthenticatedState = document.getElementById(
|
||||
"notAuthenticatedState"
|
||||
|
@ -415,7 +418,9 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
}
|
||||
|
||||
// For non-sponsor roles, handle normally
|
||||
document.querySelectorAll("[data-role-required]").forEach((element) => {
|
||||
document
|
||||
.querySelectorAll("[data-role-required]")
|
||||
.forEach((element) => {
|
||||
const requiredRole = element.getAttribute(
|
||||
"data-role-required"
|
||||
) as OfficerStatus;
|
||||
|
@ -427,7 +432,10 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
}
|
||||
|
||||
// Check if user has permission for this role
|
||||
const hasPermission = hasAccess(officerStatus, requiredRole);
|
||||
const hasPermission = hasAccess(
|
||||
officerStatus,
|
||||
requiredRole
|
||||
);
|
||||
|
||||
// Only show elements if user has permission
|
||||
element.classList.toggle("hidden", !hasPermission);
|
||||
|
@ -436,8 +444,10 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
|
||||
// Handle navigation
|
||||
const handleNavigation = () => {
|
||||
const navButtons = document.querySelectorAll(".dashboard-nav-btn");
|
||||
const sections = document.querySelectorAll(".dashboard-section");
|
||||
const navButtons =
|
||||
document.querySelectorAll(".dashboard-nav-btn");
|
||||
const sections =
|
||||
document.querySelectorAll(".dashboard-section");
|
||||
const mainContentDiv = document.getElementById("mainContent");
|
||||
|
||||
// Ensure mainContent is visible
|
||||
|
@ -471,7 +481,8 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
|
||||
// Show selected section
|
||||
const sectionId = `${sectionKey}Section`;
|
||||
const targetSection = document.getElementById(sectionId);
|
||||
const targetSection =
|
||||
document.getElementById(sectionId);
|
||||
if (targetSection) {
|
||||
targetSection.classList.remove("hidden");
|
||||
console.log(`Showing section: ${sectionId}`); // Debug log
|
||||
|
@ -481,7 +492,8 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
if (window.innerWidth < 1024 && sidebar) {
|
||||
sidebar.classList.add("-translate-x-full");
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
const overlay = document.getElementById("sidebarOverlay");
|
||||
const overlay =
|
||||
document.getElementById("sidebarOverlay");
|
||||
overlay?.remove();
|
||||
}
|
||||
});
|
||||
|
@ -581,15 +593,19 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
};
|
||||
|
||||
// Mobile sidebar toggle
|
||||
const mobileSidebarToggle = document.getElementById("mobileSidebarToggle");
|
||||
const mobileSidebarToggle = document.getElementById(
|
||||
"mobileSidebarToggle"
|
||||
);
|
||||
if (mobileSidebarToggle && sidebar) {
|
||||
const toggleSidebar = () => {
|
||||
const isOpen = !sidebar.classList.contains("-translate-x-full");
|
||||
const isOpen =
|
||||
!sidebar.classList.contains("-translate-x-full");
|
||||
|
||||
if (isOpen) {
|
||||
sidebar.classList.add("-translate-x-full");
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
const overlay = document.getElementById("sidebarOverlay");
|
||||
const overlay =
|
||||
document.getElementById("sidebarOverlay");
|
||||
overlay?.remove();
|
||||
} else {
|
||||
sidebar.classList.remove("-translate-x-full");
|
||||
|
@ -615,13 +631,15 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
// Check if user is authenticated
|
||||
if (!auth.isAuthenticated()) {
|
||||
console.log("User not authenticated");
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.remove("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.add("hidden");
|
||||
if (notAuthenticatedState)
|
||||
notAuthenticatedState.classList.add("hidden");
|
||||
|
@ -642,7 +660,8 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
|
||||
if (userProfileSkeleton)
|
||||
userProfileSkeleton.classList.remove("hidden");
|
||||
if (userProfileSummary) userProfileSummary.classList.add("hidden");
|
||||
if (userProfileSummary)
|
||||
userProfileSummary.classList.add("hidden");
|
||||
if (userProfileSignedOut)
|
||||
userProfileSignedOut.classList.add("hidden");
|
||||
if (menuLoadingSkeleton)
|
||||
|
@ -705,16 +724,23 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
officerStatus = "none";
|
||||
}
|
||||
} else {
|
||||
const extendedUser = await get.getOne("users", user.id, {
|
||||
const extendedUser = await get.getOne(
|
||||
"users",
|
||||
user.id,
|
||||
{
|
||||
fields: ["member_type"],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (extendedUser.member_type === "Sponsor") {
|
||||
officerStatus = "sponsor";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error determining officer status:", error);
|
||||
console.error(
|
||||
"Error determining officer status:",
|
||||
error
|
||||
);
|
||||
officerStatus = "none";
|
||||
}
|
||||
|
||||
|
@ -736,7 +762,8 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
'[data-section="adminDashboard"]'
|
||||
);
|
||||
} else {
|
||||
defaultSection = document.getElementById("profileSection");
|
||||
defaultSection =
|
||||
document.getElementById("profileSection");
|
||||
defaultButton = document.querySelector(
|
||||
'[data-section="profile"]'
|
||||
);
|
||||
|
@ -759,11 +786,14 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
|
||||
// Show main content and hide loading
|
||||
if (mainContent) mainContent.classList.remove("hidden");
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
} catch (error) {
|
||||
console.error("Error initializing dashboard:", error);
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState)
|
||||
pageErrorState.classList.remove("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -782,13 +812,17 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
await auth.login();
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
||||
if (pageLoadingState)
|
||||
pageLoadingState.classList.add("hidden");
|
||||
if (pageErrorState)
|
||||
pageErrorState.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle logout button click
|
||||
document.getElementById("logoutButton")?.addEventListener("click", () => {
|
||||
document
|
||||
.getElementById("logoutButton")
|
||||
?.addEventListener("click", () => {
|
||||
auth.logout();
|
||||
window.location.reload();
|
||||
});
|
||||
|
@ -801,7 +835,8 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
|
||||
window.addEventListener("resize", () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
const overlay = document.getElementById("sidebarOverlay");
|
||||
const overlay =
|
||||
document.getElementById("sidebarOverlay");
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
|
@ -810,4 +845,6 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
|||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue