Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a86140a6b5 | ||
![]() |
e27a78a15b | ||
![]() |
c4278b7773 | ||
![]() |
d055c5ee69 |
10 changed files with 619 additions and 571 deletions
|
@ -5,144 +5,151 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
---
|
||||
|
||||
<div id="" class="">
|
||||
<div class="mb-4 sm:mb-6 px-4 sm:px-6">
|
||||
<h2 class="text-xl sm:text-2xl font-bold">Events</h2>
|
||||
<p class="opacity-70 text-sm sm:text-base">
|
||||
View and manage your IEEE UCSD events
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-4 sm:mb-6 px-4 sm:px-6"
|
||||
>
|
||||
<!-- Event Check-in Card -->
|
||||
<div class="w-full">
|
||||
<EventCheckIn client:load />
|
||||
<div class="mb-4 sm:mb-6 px-4 sm:px-6">
|
||||
<h2 class="text-xl sm:text-2xl font-bold">Events</h2>
|
||||
<p class="opacity-70 text-sm sm:text-base">
|
||||
View and manage your IEEE UCSD events
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Event Registration Card -->
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 opacity-50 cursor-not-allowed relative group h-full"
|
||||
>
|
||||
<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"
|
||||
>Coming Soon</span
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-4 sm:mb-6 px-4 sm:px-6"
|
||||
>
|
||||
<!-- Event Check-in Card -->
|
||||
<div class="w-full">
|
||||
<EventCheckIn client:load />
|
||||
</div>
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<h3 class="card-title text-base sm:text-lg mb-3 sm:mb-4">
|
||||
Event Registration
|
||||
</h3>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text text-sm sm:text-base"
|
||||
>Select an event to register</span
|
||||
>
|
||||
</label>
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<select
|
||||
class="select select-bordered flex-1 text-sm sm:text-base h-10 min-h-[2.5rem] w-full"
|
||||
disabled
|
||||
>
|
||||
<option disabled selected>Pick an event</option>
|
||||
<option>Technical Workshop - Web Development</option>
|
||||
<option>Professional Development Workshop</option>
|
||||
<option>Social Event - Game Night</option>
|
||||
</select>
|
||||
<button
|
||||
class="btn btn-primary text-sm sm:text-base h-10 min-h-[2.5rem] w-full sm:w-[100px]"
|
||||
disabled>Register</button
|
||||
>
|
||||
|
||||
<!-- Event Registration Card -->
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="card bg-card shadow-xl border border-border opacity-50 cursor-not-allowed relative group h-full"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-card opacity-0 group-hover:opacity-90 transition-opacity duration-300 flex items-center justify-center z-10"
|
||||
>
|
||||
<span
|
||||
class="text-card-foreground font-medium text-sm sm:text-base"
|
||||
>Coming Soon</span
|
||||
>
|
||||
</div>
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<h3 class="card-title text-base sm:text-lg mb-3 sm:mb-4">
|
||||
Event Registration
|
||||
</h3>
|
||||
<div class="w-full">
|
||||
<label class="block text-sm sm:text-base mb-2">
|
||||
<span class="text-sm sm:text-base"
|
||||
>Select an event to register</span
|
||||
>
|
||||
</label>
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<select
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 flex-1 text-sm sm:text-base min-h-[2.5rem]"
|
||||
disabled
|
||||
>
|
||||
<option disabled selected>Pick an event</option>
|
||||
<option
|
||||
>Technical Workshop - Web Development</option
|
||||
>
|
||||
<option
|
||||
>Professional Development Workshop</option
|
||||
>
|
||||
<option>Social Event - Game Night</option>
|
||||
</select>
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 min-h-[2.5rem] w-full sm:w-[100px] px-4 py-2"
|
||||
disabled>Register</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EventLoad client:load />
|
||||
<EventLoad client:load />
|
||||
</div>
|
||||
|
||||
<!-- Event Details Modal -->
|
||||
<dialog id="eventDetailsModal" class="modal">
|
||||
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<h3 class="font-bold text-base sm:text-lg" id="modalTitle">
|
||||
Event Files
|
||||
</h3>
|
||||
<button
|
||||
id="downloadAllBtn"
|
||||
class="btn btn-primary btn-sm gap-1 text-xs sm:text-sm"
|
||||
onclick="window.downloadAllFiles()"
|
||||
>
|
||||
<iconify-icon
|
||||
icon="heroicons:arrow-down-tray-20-solid"
|
||||
className="h-3 w-3 sm:h-4 sm:w-4"></iconify-icon>
|
||||
Download All
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<h3 class="font-bold text-base sm:text-lg" id="modalTitle">
|
||||
Event Files
|
||||
</h3>
|
||||
<button
|
||||
id="downloadAllBtn"
|
||||
class="btn btn-primary btn-sm gap-1 text-xs sm:text-sm"
|
||||
onclick="window.downloadAllFiles()"
|
||||
>
|
||||
<iconify-icon
|
||||
icon="heroicons:arrow-down-tray-20-solid"
|
||||
className="h-3 w-3 sm:h-4 sm:w-4"></iconify-icon>
|
||||
Download All
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="filesContent" class="space-y-3 sm:space-y-4">
|
||||
<!-- Files list will be populated here -->
|
||||
<div id="filesContent" class="space-y-3 sm:space-y-4">
|
||||
<!-- Files list will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick="window.closeEventDetailsModal()">close</button>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick="window.closeEventDetailsModal()">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Universal File Preview Modal -->
|
||||
<dialog id="filePreviewModal" class="modal">
|
||||
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm text-xs sm:text-sm"
|
||||
onclick="window.closeFilePreviewEvents()">Close</button
|
||||
>
|
||||
<h3
|
||||
class="font-bold text-base sm:text-lg truncate"
|
||||
id="previewFileName"
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm text-xs sm:text-sm"
|
||||
onclick="window.closeFilePreviewEvents()">Close</button
|
||||
>
|
||||
<h3
|
||||
class="font-bold text-base sm:text-lg truncate"
|
||||
id="previewFileName"
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" id="previewContainer">
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
<div id="previewContent" class="w-full">
|
||||
<FilePreview client:load isModal={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" id="previewContainer">
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
<div id="previewContent" class="w-full">
|
||||
<FilePreview client:load isModal={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick="window.closeFilePreviewEvents()">close</button>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick="window.closeFilePreviewEvents()">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
import { toast } from "react-hot-toast";
|
||||
import JSZip from "jszip";
|
||||
import { toast } from "react-hot-toast";
|
||||
import JSZip from "jszip";
|
||||
|
||||
// Add styles to the document
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
// Add styles to the document
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
/* Custom styles for the event details modal */
|
||||
.event-details-grid {
|
||||
display: grid;
|
||||
|
@ -158,227 +165,234 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
|
||||
/* Remove custom toast styles since we're using react-hot-toast */
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Add helper functions for file preview
|
||||
function getFileType(filename: string): string {
|
||||
const extension = filename.split(".").pop()?.toLowerCase();
|
||||
const mimeTypes: { [key: string]: string } = {
|
||||
pdf: "application/pdf",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
png: "image/png",
|
||||
gif: "image/gif",
|
||||
mp4: "video/mp4",
|
||||
mp3: "audio/mpeg",
|
||||
txt: "text/plain",
|
||||
doc: "application/msword",
|
||||
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
xls: "application/vnd.ms-excel",
|
||||
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
json: "application/json",
|
||||
// Add helper functions for file preview
|
||||
function getFileType(filename: string): string {
|
||||
const extension = filename.split(".").pop()?.toLowerCase();
|
||||
const mimeTypes: { [key: string]: string } = {
|
||||
pdf: "application/pdf",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
png: "image/png",
|
||||
gif: "image/gif",
|
||||
mp4: "video/mp4",
|
||||
mp3: "audio/mpeg",
|
||||
txt: "text/plain",
|
||||
doc: "application/msword",
|
||||
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
xls: "application/vnd.ms-excel",
|
||||
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
json: "application/json",
|
||||
};
|
||||
|
||||
return mimeTypes[extension || ""] || "application/octet-stream";
|
||||
}
|
||||
|
||||
// Universal file preview function for events section
|
||||
window.previewFileEvents = function (url: string, filename: string) {
|
||||
// console.log("previewFileEvents called with:", { url, filename });
|
||||
// console.log("URL type:", typeof url, "URL length:", url?.length || 0);
|
||||
// console.log(
|
||||
// "Filename type:",
|
||||
// typeof filename,
|
||||
// "Filename length:",
|
||||
// filename?.length || 0
|
||||
// );
|
||||
|
||||
// Validate inputs
|
||||
if (!url || typeof url !== "string") {
|
||||
console.error("Invalid URL provided to previewFileEvents:", url);
|
||||
toast.error("Cannot preview file: Invalid URL");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filename || typeof filename !== "string") {
|
||||
console.error(
|
||||
"Invalid filename provided to previewFileEvents:",
|
||||
filename
|
||||
);
|
||||
toast.error("Cannot preview file: Invalid filename");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure URL is properly formatted
|
||||
if (!url.startsWith("http")) {
|
||||
console.warn(
|
||||
"URL doesn't start with http, attempting to fix:",
|
||||
url
|
||||
);
|
||||
if (url.startsWith("/")) {
|
||||
url = `https://pocketbase.ieeeucsd.org${url}`;
|
||||
} else {
|
||||
url = `https://pocketbase.ieeeucsd.org/${url}`;
|
||||
}
|
||||
// console.log("Fixed URL:", url);
|
||||
}
|
||||
|
||||
const modal = document.getElementById(
|
||||
"filePreviewModal"
|
||||
) as HTMLDialogElement;
|
||||
const previewFileName = document.getElementById("previewFileName");
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
const loadingSpinner = document.getElementById("previewLoadingSpinner");
|
||||
|
||||
if (modal && previewFileName && previewContent) {
|
||||
// console.log("Found all required elements");
|
||||
|
||||
// Show loading spinner
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Update the filename display
|
||||
previewFileName.textContent = filename;
|
||||
|
||||
// Show the modal
|
||||
modal.showModal();
|
||||
|
||||
// Test the URL with a fetch before dispatching the event
|
||||
fetch(url, { method: "HEAD" })
|
||||
.then((response) => {
|
||||
// console.log(
|
||||
// "URL test response:",
|
||||
// response.status,
|
||||
// response.ok
|
||||
// );
|
||||
if (!response.ok) {
|
||||
console.warn("URL might not be accessible:", url);
|
||||
toast(
|
||||
"File might not be accessible. Attempting to preview anyway.",
|
||||
{
|
||||
icon: "⚠️",
|
||||
style: {
|
||||
borderRadius: "10px",
|
||||
background: "#FFC107",
|
||||
color: "#000",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error testing URL:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
// Dispatch state change event to update the FilePreview component
|
||||
// console.log(
|
||||
// "Dispatching filePreviewStateChange event with:",
|
||||
// { url, filename }
|
||||
// );
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("filePreviewStateChange", {
|
||||
detail: { url, filename },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Hide loading spinner after a short delay
|
||||
setTimeout(() => {
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.classList.add("hidden");
|
||||
}
|
||||
}, 1000); // Increased delay to allow for URL testing
|
||||
} else {
|
||||
console.error("Missing required elements for file preview");
|
||||
toast.error("Could not initialize file preview");
|
||||
}
|
||||
};
|
||||
|
||||
return mimeTypes[extension || ""] || "application/octet-stream";
|
||||
}
|
||||
// Close file preview for events section
|
||||
window.closeFilePreviewEvents = function () {
|
||||
// console.log("closeFilePreviewEvents called");
|
||||
const modal = document.getElementById(
|
||||
"filePreviewModal"
|
||||
) as HTMLDialogElement;
|
||||
const previewFileName = document.getElementById("previewFileName");
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
const loadingSpinner = document.getElementById("previewLoadingSpinner");
|
||||
|
||||
// Universal file preview function for events section
|
||||
window.previewFileEvents = function (url: string, filename: string) {
|
||||
// console.log("previewFileEvents called with:", { url, filename });
|
||||
// console.log("URL type:", typeof url, "URL length:", url?.length || 0);
|
||||
// console.log(
|
||||
// "Filename type:",
|
||||
// typeof filename,
|
||||
// "Filename length:",
|
||||
// filename?.length || 0
|
||||
// );
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if (!url || typeof url !== "string") {
|
||||
console.error("Invalid URL provided to previewFileEvents:", url);
|
||||
toast.error("Cannot preview file: Invalid URL");
|
||||
return;
|
||||
}
|
||||
if (modal && previewFileName && previewContent) {
|
||||
// console.log("Resetting preview and closing modal");
|
||||
|
||||
if (!filename || typeof filename !== "string") {
|
||||
console.error(
|
||||
"Invalid filename provided to previewFileEvents:",
|
||||
filename,
|
||||
);
|
||||
toast.error("Cannot preview file: Invalid filename");
|
||||
return;
|
||||
}
|
||||
// First reset the preview state by dispatching an event with empty values
|
||||
// This ensures the FilePreview component clears its internal state
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("filePreviewStateChange", {
|
||||
detail: { url: "", filename: "" },
|
||||
})
|
||||
);
|
||||
|
||||
// Ensure URL is properly formatted
|
||||
if (!url.startsWith("http")) {
|
||||
console.warn("URL doesn't start with http, attempting to fix:", url);
|
||||
if (url.startsWith("/")) {
|
||||
url = `https://pocketbase.ieeeucsd.org${url}`;
|
||||
} else {
|
||||
url = `https://pocketbase.ieeeucsd.org/${url}`;
|
||||
}
|
||||
// console.log("Fixed URL:", url);
|
||||
}
|
||||
// Reset the UI
|
||||
previewFileName.textContent = "";
|
||||
|
||||
const modal = document.getElementById(
|
||||
"filePreviewModal",
|
||||
) as HTMLDialogElement;
|
||||
const previewFileName = document.getElementById("previewFileName");
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
const loadingSpinner = document.getElementById("previewLoadingSpinner");
|
||||
// Close the modal
|
||||
modal.close();
|
||||
|
||||
if (modal && previewFileName && previewContent) {
|
||||
// console.log("Found all required elements");
|
||||
// console.log("File preview modal closed and state reset");
|
||||
} else {
|
||||
console.error("Could not find elements to close file preview");
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading spinner
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.classList.remove("hidden");
|
||||
}
|
||||
// Update the showFilePreview function for events section
|
||||
window.showFilePreviewEvents = function (file: {
|
||||
url: string;
|
||||
name: string;
|
||||
}) {
|
||||
// console.log("showFilePreviewEvents called with:", file);
|
||||
if (!file || !file.url || !file.name) {
|
||||
console.error("Invalid file data:", file);
|
||||
toast.error("Could not preview file: missing file information");
|
||||
return;
|
||||
}
|
||||
window.previewFileEvents(file.url, file.name);
|
||||
};
|
||||
|
||||
// Update the filename display
|
||||
previewFileName.textContent = filename;
|
||||
// Update the openDetailsModal function to use the events-specific preview
|
||||
window.openDetailsModal = function (event: any) {
|
||||
const modal = document.getElementById(
|
||||
"eventDetailsModal"
|
||||
) as HTMLDialogElement;
|
||||
const filesContent = document.getElementById(
|
||||
"filesContent"
|
||||
) as HTMLDivElement;
|
||||
|
||||
// Show the modal
|
||||
modal.showModal();
|
||||
// Check if event has ended
|
||||
const eventEndDate = new Date(event.end_date);
|
||||
const now = new Date();
|
||||
|
||||
// Test the URL with a fetch before dispatching the event
|
||||
fetch(url, { method: "HEAD" })
|
||||
.then((response) => {
|
||||
// console.log(
|
||||
// "URL test response:",
|
||||
// response.status,
|
||||
// response.ok
|
||||
// );
|
||||
if (!response.ok) {
|
||||
console.warn("URL might not be accessible:", url);
|
||||
toast(
|
||||
"File might not be accessible. Attempting to preview anyway.",
|
||||
{
|
||||
if (eventEndDate > now) {
|
||||
toast("Files are only available after the event has ended.", {
|
||||
icon: "⚠️",
|
||||
style: {
|
||||
borderRadius: "10px",
|
||||
background: "#FFC107",
|
||||
color: "#000",
|
||||
borderRadius: "10px",
|
||||
background: "#FFC107",
|
||||
color: "#000",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error testing URL:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
// Dispatch state change event to update the FilePreview component
|
||||
// console.log(
|
||||
// "Dispatching filePreviewStateChange event with:",
|
||||
// { url, filename }
|
||||
// );
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("filePreviewStateChange", {
|
||||
detail: { url, filename },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Hide loading spinner after a short delay
|
||||
setTimeout(() => {
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.classList.add("hidden");
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, 1000); // Increased delay to allow for URL testing
|
||||
} else {
|
||||
console.error("Missing required elements for file preview");
|
||||
toast.error("Could not initialize file preview");
|
||||
}
|
||||
};
|
||||
|
||||
// Close file preview for events section
|
||||
window.closeFilePreviewEvents = function () {
|
||||
// console.log("closeFilePreviewEvents called");
|
||||
const modal = document.getElementById(
|
||||
"filePreviewModal",
|
||||
) as HTMLDialogElement;
|
||||
const previewFileName = document.getElementById("previewFileName");
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
const loadingSpinner = document.getElementById("previewLoadingSpinner");
|
||||
// Reset state
|
||||
window.currentEventId = event.id;
|
||||
if (filesContent) filesContent.classList.remove("hidden");
|
||||
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.classList.add("hidden");
|
||||
}
|
||||
// Populate files content
|
||||
if (
|
||||
event.files &&
|
||||
Array.isArray(event.files) &&
|
||||
event.files.length > 0
|
||||
) {
|
||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||
const collectionId = "events";
|
||||
const recordId = event.id;
|
||||
|
||||
if (modal && previewFileName && previewContent) {
|
||||
// console.log("Resetting preview and closing modal");
|
||||
|
||||
// First reset the preview state by dispatching an event with empty values
|
||||
// This ensures the FilePreview component clears its internal state
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("filePreviewStateChange", {
|
||||
detail: { url: "", filename: "" },
|
||||
}),
|
||||
);
|
||||
|
||||
// Reset the UI
|
||||
previewFileName.textContent = "";
|
||||
|
||||
// Close the modal
|
||||
modal.close();
|
||||
|
||||
// console.log("File preview modal closed and state reset");
|
||||
} else {
|
||||
console.error("Could not find elements to close file preview");
|
||||
}
|
||||
};
|
||||
|
||||
// Update the showFilePreview function for events section
|
||||
window.showFilePreviewEvents = function (file: {
|
||||
url: string;
|
||||
name: string;
|
||||
}) {
|
||||
// console.log("showFilePreviewEvents called with:", file);
|
||||
if (!file || !file.url || !file.name) {
|
||||
console.error("Invalid file data:", file);
|
||||
toast.error("Could not preview file: missing file information");
|
||||
return;
|
||||
}
|
||||
window.previewFileEvents(file.url, file.name);
|
||||
};
|
||||
|
||||
// Update the openDetailsModal function to use the events-specific preview
|
||||
window.openDetailsModal = function (event: any) {
|
||||
const modal = document.getElementById(
|
||||
"eventDetailsModal",
|
||||
) as HTMLDialogElement;
|
||||
const filesContent = document.getElementById(
|
||||
"filesContent",
|
||||
) as HTMLDivElement;
|
||||
|
||||
// Check if event has ended
|
||||
const eventEndDate = new Date(event.end_date);
|
||||
const now = new Date();
|
||||
|
||||
if (eventEndDate > now) {
|
||||
toast("Files are only available after the event has ended.", {
|
||||
icon: "⚠️",
|
||||
style: {
|
||||
borderRadius: "10px",
|
||||
background: "#FFC107",
|
||||
color: "#000",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
window.currentEventId = event.id;
|
||||
if (filesContent) filesContent.classList.remove("hidden");
|
||||
|
||||
// Populate files content
|
||||
if (event.files && Array.isArray(event.files) && event.files.length > 0) {
|
||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||
const collectionId = "events";
|
||||
const recordId = event.id;
|
||||
|
||||
filesContent.innerHTML = `
|
||||
filesContent.innerHTML = `
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
|
@ -389,16 +403,16 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
</thead>
|
||||
<tbody>
|
||||
${event.files
|
||||
.map((file: string) => {
|
||||
// Ensure the file URL is properly formatted
|
||||
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
|
||||
const fileType = getFileType(file);
|
||||
// Properly escape the data for the onclick handler
|
||||
const fileData = {
|
||||
url: fileUrl,
|
||||
name: file,
|
||||
};
|
||||
return `
|
||||
.map((file: string) => {
|
||||
// Ensure the file URL is properly formatted
|
||||
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
|
||||
const fileType = getFileType(file);
|
||||
// Properly escape the data for the onclick handler
|
||||
const fileData = {
|
||||
url: fileUrl,
|
||||
name: file,
|
||||
};
|
||||
return `
|
||||
<tr>
|
||||
<td>${file}</td>
|
||||
<td class="text-right">
|
||||
|
@ -411,123 +425,123 @@ import EventLoad from "./EventsSection/EventLoad";
|
|||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
filesContent.innerHTML = `
|
||||
} else {
|
||||
filesContent.innerHTML = `
|
||||
<div class="text-center py-8 text-base-content/70">
|
||||
<iconify-icon icon="heroicons:document-duplicate" className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No files attached to this event</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
modal.showModal();
|
||||
};
|
||||
|
||||
// Add downloadAllFiles function
|
||||
window.downloadAllFiles = async function () {
|
||||
const downloadBtn = document.getElementById(
|
||||
"downloadAllBtn",
|
||||
) as HTMLButtonElement;
|
||||
if (!downloadBtn) return;
|
||||
const originalBtnContent = downloadBtn.innerHTML;
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
downloadBtn.innerHTML =
|
||||
'<span class="loading loading-spinner loading-xs"></span> Preparing...';
|
||||
downloadBtn.disabled = true;
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
// Get current event files
|
||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||
const collectionId = "events";
|
||||
const recordId = window.currentEventId;
|
||||
|
||||
// Get the current event from the window object
|
||||
const eventDataId = `event_${window.currentEventId}`;
|
||||
const event = window[eventDataId];
|
||||
|
||||
if (!event || !event.files || event.files.length === 0) {
|
||||
throw new Error("No files available to download");
|
||||
}
|
||||
|
||||
// Download each file and add to zip
|
||||
const filePromises = event.files.map(async (filename: string) => {
|
||||
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${filename}`;
|
||||
const response = await fetch(fileUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${filename}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
zip.file(filename, blob);
|
||||
});
|
||||
|
||||
await Promise.all(filePromises);
|
||||
modal.showModal();
|
||||
};
|
||||
|
||||
// Generate and download zip
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
const downloadUrl = URL.createObjectURL(zipBlob);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.download = `${event.event_name}_files.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
// Add downloadAllFiles function
|
||||
window.downloadAllFiles = async function () {
|
||||
const downloadBtn = document.getElementById(
|
||||
"downloadAllBtn"
|
||||
) as HTMLButtonElement;
|
||||
if (!downloadBtn) return;
|
||||
const originalBtnContent = downloadBtn.innerHTML;
|
||||
|
||||
// Show success message
|
||||
toast.success("Files downloaded successfully!");
|
||||
} catch (error: any) {
|
||||
console.error("Failed to download files:", error);
|
||||
toast.error(
|
||||
error?.message || "Failed to download files. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
// Reset button state
|
||||
downloadBtn.innerHTML = originalBtnContent;
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
try {
|
||||
// Show loading state
|
||||
downloadBtn.innerHTML =
|
||||
'<span class="loading loading-spinner loading-xs"></span> Preparing...';
|
||||
downloadBtn.disabled = true;
|
||||
|
||||
// Close event details modal
|
||||
window.closeEventDetailsModal = function () {
|
||||
const modal = document.getElementById(
|
||||
"eventDetailsModal",
|
||||
) as HTMLDialogElement;
|
||||
const filesContent = document.getElementById("filesContent");
|
||||
const zip = new JSZip();
|
||||
|
||||
if (modal) {
|
||||
// Reset the files content
|
||||
if (filesContent) {
|
||||
filesContent.innerHTML = "";
|
||||
}
|
||||
// Get current event files
|
||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||
const collectionId = "events";
|
||||
const recordId = window.currentEventId;
|
||||
|
||||
// Reset any other state if needed
|
||||
window.currentEventId = "";
|
||||
// Get the current event from the window object
|
||||
const eventDataId = `event_${window.currentEventId}`;
|
||||
const event = window[eventDataId];
|
||||
|
||||
// Close the modal
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
if (!event || !event.files || event.files.length === 0) {
|
||||
throw new Error("No files available to download");
|
||||
}
|
||||
|
||||
// Make helper functions available globally
|
||||
window.showFilePreview = window.showFilePreviewEvents;
|
||||
window.handlePreviewError = function () {
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
if (previewContent) {
|
||||
previewContent.innerHTML = `
|
||||
// Download each file and add to zip
|
||||
const filePromises = event.files.map(async (filename: string) => {
|
||||
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${filename}`;
|
||||
const response = await fetch(fileUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${filename}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
zip.file(filename, blob);
|
||||
});
|
||||
|
||||
await Promise.all(filePromises);
|
||||
|
||||
// Generate and download zip
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
const downloadUrl = URL.createObjectURL(zipBlob);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.download = `${event.event_name}_files.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
// Show success message
|
||||
toast.success("Files downloaded successfully!");
|
||||
} catch (error: any) {
|
||||
console.error("Failed to download files:", error);
|
||||
toast.error(
|
||||
error?.message || "Failed to download files. Please try again."
|
||||
);
|
||||
} finally {
|
||||
// Reset button state
|
||||
downloadBtn.innerHTML = originalBtnContent;
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Close event details modal
|
||||
window.closeEventDetailsModal = function () {
|
||||
const modal = document.getElementById(
|
||||
"eventDetailsModal"
|
||||
) as HTMLDialogElement;
|
||||
const filesContent = document.getElementById("filesContent");
|
||||
|
||||
if (modal) {
|
||||
// Reset the files content
|
||||
if (filesContent) {
|
||||
filesContent.innerHTML = "";
|
||||
}
|
||||
|
||||
// Reset any other state if needed
|
||||
window.currentEventId = "";
|
||||
|
||||
// Close the modal
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Make helper functions available globally
|
||||
window.showFilePreview = window.showFilePreviewEvents;
|
||||
window.handlePreviewError = function () {
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
if (previewContent) {
|
||||
previewContent.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
<Icon icon="heroicons:x-circle" className="h-6 w-6" />
|
||||
<span>Failed to load file preview</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -359,12 +359,12 @@ const EventCheckIn = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 h-full">
|
||||
<div className="card bg-card shadow-xl border border-border h-full">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Event Check-in</h3>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text text-sm sm:text-base">Enter event code to check in</span>
|
||||
<div className="w-full">
|
||||
<label className="block text-sm sm:text-base mb-2">
|
||||
<span className="text-sm sm:text-base">Enter event code to check in</span>
|
||||
</label>
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -103,28 +103,28 @@ const EventLoad = () => {
|
|||
}, []);
|
||||
|
||||
const createSkeletonCard = () => (
|
||||
<div className="card bg-base-200 shadow-lg animate-pulse">
|
||||
<div className="card bg-base-200 dark:bg-gray-800/90 shadow-lg animate-pulse border border-base-300 dark:border-gray-700">
|
||||
<div className="card-body p-5">
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div className="flex-1">
|
||||
<div className="skeleton h-6 w-3/4 mb-2"></div>
|
||||
<div className="skeleton h-6 w-3/4 mb-2 bg-base-300 dark:bg-gray-700"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="skeleton h-5 w-16"></div>
|
||||
<div className="skeleton h-5 w-20"></div>
|
||||
<div className="skeleton h-5 w-16 bg-base-300 dark:bg-gray-700"></div>
|
||||
<div className="skeleton h-5 w-20 bg-base-300 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="skeleton h-5 w-24 mb-1"></div>
|
||||
<div className="skeleton h-4 w-16"></div>
|
||||
<div className="skeleton h-5 w-24 mb-1 bg-base-300 dark:bg-gray-700"></div>
|
||||
<div className="skeleton h-4 w-16 bg-base-300 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="skeleton h-4 w-full mb-3"></div>
|
||||
<div className="skeleton h-4 w-full mb-3 bg-base-300 dark:bg-gray-700"></div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="skeleton h-4 w-4"></div>
|
||||
<div className="skeleton h-4 w-1/2"></div>
|
||||
<div className="skeleton h-4 w-4 bg-base-300 dark:bg-gray-700"></div>
|
||||
<div className="skeleton h-4 w-1/2 bg-base-300 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -178,15 +178,15 @@ const EventLoad = () => {
|
|||
const isPastEvent = endDate < now;
|
||||
|
||||
return (
|
||||
<div key={event.id} className="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-300 relative overflow-hidden">
|
||||
<div id={`event-card-${event.id}`} key={event.id} className="card bg-base-200 dark:bg-gray-800/90 shadow-lg hover:shadow-xl transition-all duration-300 relative overflow-hidden border border-base-300 dark:border-gray-700">
|
||||
<div className="card-body p-3 sm:p-4">
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex-1">
|
||||
<h3 className="card-title text-base sm:text-lg font-semibold mb-1 line-clamp-2">{event.event_name}</h3>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm text-base-content/70">
|
||||
<h3 className="card-title text-base sm:text-lg font-semibold mb-1 line-clamp-2 text-gray-800 dark:text-gray-100">{event.event_name}</h3>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm text-gray-600 dark:text-gray-300">
|
||||
<div className="badge badge-primary badge-sm">{event.points_to_reward} pts</div>
|
||||
<div className="text-xs sm:text-sm opacity-75">
|
||||
<div className="text-xs sm:text-sm text-gray-600 dark:text-gray-300">
|
||||
{startDate.toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
|
@ -202,7 +202,7 @@ const EventLoad = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs sm:text-sm text-base-content/70 my-2 line-clamp-2">
|
||||
<div className="text-xs sm:text-sm text-gray-600 dark:text-gray-300 my-2 line-clamp-2">
|
||||
{event.event_description || "No description available"}
|
||||
</div>
|
||||
|
||||
|
@ -210,14 +210,14 @@ const EventLoad = () => {
|
|||
{event.files && event.files.length > 0 && (
|
||||
<button
|
||||
onClick={() => window.openDetailsModal(event as ExtendedEvent)}
|
||||
className="btn btn-ghost btn-sm text-xs sm:text-sm gap-1 h-8 min-h-0 px-2"
|
||||
className="btn btn-ghost btn-sm text-xs sm:text-sm gap-1 h-8 min-h-0 px-2 text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<Icon icon="heroicons:document-duplicate" className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
Files ({event.files.length})
|
||||
</button>
|
||||
)}
|
||||
{isPastEvent && (
|
||||
<div className={`badge ${hasAttended ? 'badge-success' : 'badge-ghost'} text-xs gap-1`}>
|
||||
<div className={`badge ${hasAttended ? 'badge-success' : 'badge-ghost'} text-xs gap-1 attended-badge ${!hasAttended ? 'hidden' : ''}`}>
|
||||
<Icon
|
||||
icon={hasAttended ? "heroicons:check-circle" : "heroicons:x-circle"}
|
||||
className="h-3 w-3"
|
||||
|
@ -225,7 +225,7 @@ const EventLoad = () => {
|
|||
{hasAttended ? 'Attended' : 'Not Attended'}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs sm:text-sm opacity-75 ml-auto">
|
||||
<div className="text-xs sm:text-sm text-gray-600 dark:text-gray-400 ml-auto">
|
||||
{event.location}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -480,9 +480,9 @@ const EventLoad = () => {
|
|||
return (
|
||||
<>
|
||||
{/* Ongoing Events */}
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card bg-base-100 dark:bg-gray-900/90 shadow-xl border border-base-200 dark:border-gray-700 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Ongoing Events</h3>
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4 text-gray-800 dark:text-gray-100">Ongoing Events</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={`ongoing-skeleton-${i}`}>{createSkeletonCard()}</div>
|
||||
|
@ -492,9 +492,9 @@ const EventLoad = () => {
|
|||
</div>
|
||||
|
||||
{/* Upcoming Events */}
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card bg-base-100 dark:bg-gray-900/90 shadow-xl border border-base-200 dark:border-gray-700 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Upcoming Events</h3>
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4 text-gray-800 dark:text-gray-100">Upcoming Events</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={`upcoming-skeleton-${i}`}>{createSkeletonCard()}</div>
|
||||
|
@ -504,9 +504,9 @@ const EventLoad = () => {
|
|||
</div>
|
||||
|
||||
{/* Past Events */}
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mx-4 sm:mx-6">
|
||||
<div className="card bg-base-100 dark:bg-gray-900/90 shadow-xl border border-base-200 dark:border-gray-700 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mx-4 sm:mx-6">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Past Events</h3>
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4 text-gray-800 dark:text-gray-100">Past Events</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={`past-skeleton-${i}`}>{createSkeletonCard()}</div>
|
||||
|
@ -525,14 +525,14 @@ const EventLoad = () => {
|
|||
<>
|
||||
{/* No Events Message */}
|
||||
{noEvents && (
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 mx-4 sm:mx-6 p-8">
|
||||
<div className="card bg-base-100 dark:bg-gray-900/90 shadow-xl border border-base-200 dark:border-gray-700 mx-4 sm:mx-6 p-8">
|
||||
<div className="text-center">
|
||||
<Icon icon="heroicons:calendar" className="w-16 h-16 mx-auto text-base-content/30 mb-4" />
|
||||
<h3 className="text-xl font-bold mb-2">No Events Found</h3>
|
||||
<p className="text-base-content/70 mb-4">
|
||||
<Icon icon="heroicons:calendar" className="w-16 h-16 mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
||||
<h3 className="text-xl font-bold mb-2 text-gray-800 dark:text-gray-100">No Events Found</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
There are currently no events to display. This could be due to:
|
||||
</p>
|
||||
<ul className="list-disc text-left max-w-md mx-auto text-base-content/70 mb-6">
|
||||
<ul className="list-disc text-left max-w-md mx-auto text-gray-600 dark:text-gray-300 mb-6">
|
||||
<li className="mb-1">No events have been published yet</li>
|
||||
<li className="mb-1">There might be a connection issue with the event database</li>
|
||||
<li className="mb-1">The events data might be temporarily unavailable</li>
|
||||
|
@ -560,9 +560,9 @@ const EventLoad = () => {
|
|||
|
||||
{/* Ongoing Events */}
|
||||
{events.ongoing.length > 0 && (
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card bg-base-100 dark:bg-gray-900/90 shadow-xl border border-base-200 dark:border-gray-700 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Ongoing Events</h3>
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4 text-gray-800 dark:text-gray-100">Ongoing Events</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{events.ongoing.map(renderEventCard)}
|
||||
</div>
|
||||
|
@ -572,9 +572,9 @@ const EventLoad = () => {
|
|||
|
||||
{/* Upcoming Events */}
|
||||
{events.upcoming.length > 0 && (
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card bg-base-100 dark:bg-gray-900/90 shadow-xl border border-base-200 dark:border-gray-700 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Upcoming Events</h3>
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4 text-gray-800 dark:text-gray-100">Upcoming Events</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{events.upcoming.map(renderEventCard)}
|
||||
</div>
|
||||
|
@ -584,9 +584,9 @@ const EventLoad = () => {
|
|||
|
||||
{/* Past Events */}
|
||||
{events.past.length > 0 && (
|
||||
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mx-4 sm:mx-6">
|
||||
<div className="card bg-base-100 dark:bg-gray-900/90 shadow-xl border border-base-200 dark:border-gray-700 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mx-4 sm:mx-6">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Past Events</h3>
|
||||
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4 text-gray-800 dark:text-gray-100">Past Events</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{events.past.map(renderEventCard)}
|
||||
</div>
|
||||
|
|
|
@ -125,9 +125,9 @@ export default function LeaderboardStats() {
|
|||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="h-24 bg-gray-100/50 dark:bg-gray-800/50 animate-pulse rounded-xl">
|
||||
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-2 mt-4 mx-4"></div>
|
||||
<div className="h-8 w-16 bg-gray-200 dark:bg-gray-700 rounded mx-4"></div>
|
||||
<div key={i} className="h-24 bg-base-200 dark:bg-gray-800/50 animate-pulse rounded-xl">
|
||||
<div className="h-4 w-24 bg-base-300 dark:bg-gray-700 rounded mb-2 mt-4 mx-4"></div>
|
||||
<div className="h-8 w-16 bg-base-300 dark:bg-gray-700 rounded mx-4"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -136,27 +136,27 @@ export default function LeaderboardStats() {
|
|||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="p-6 bg-white/90 dark:bg-gray-800/90 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-gray-300">Total Members</div>
|
||||
<div className="p-6 bg-base-100 dark:bg-gray-800/90 rounded-xl shadow-sm border border-base-200 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">Total Members</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-800 dark:text-white">{stats.totalUsers}</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">In the leaderboard</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white/90 dark:bg-gray-800/90 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-gray-300">Total Points</div>
|
||||
<div className="p-6 bg-base-100 dark:bg-gray-800/90 rounded-xl shadow-sm border border-base-200 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">Total Points</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-800 dark:text-white">{stats.totalPoints}</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">Earned by all members</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white/90 dark:bg-gray-800/90 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-gray-300">Top Score</div>
|
||||
<div className="mt-2 text-3xl font-bold text-indigo-600 dark:text-indigo-400">{stats.topScore}</div>
|
||||
<div className="p-6 bg-base-100 dark:bg-gray-800/90 rounded-xl shadow-sm border border-base-200 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">Top Score</div>
|
||||
<div className="mt-2 text-3xl font-bold text-primary dark:text-primary">{stats.topScore}</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">Highest individual points</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white/90 dark:bg-gray-800/90 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-gray-300">Your Score</div>
|
||||
<div className="mt-2 text-3xl font-bold text-indigo-600 dark:text-indigo-400">
|
||||
<div className="p-6 bg-base-100 dark:bg-gray-800/90 rounded-xl shadow-sm border border-base-200 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">Your Score</div>
|
||||
<div className="mt-2 text-3xl font-bold text-primary dark:text-primary">
|
||||
{isAuthenticated ? stats.yourPoints : '-'}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
|
|
|
@ -184,37 +184,37 @@ export default function LeaderboardTable() {
|
|||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or major..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg
|
||||
bg-white/90 dark:bg-gray-800/90 text-gray-700 dark:text-gray-200 focus:outline-none
|
||||
focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full pl-10 pr-4 py-2 border border-base-300 dark:border-gray-700 rounded-lg
|
||||
bg-base-100 dark:bg-gray-800/90 text-gray-700 dark:text-gray-200 focus:outline-none
|
||||
focus:ring-2 focus:ring-primary focus:border-transparent shadow-sm"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Leaderboard table */}
|
||||
<div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50/80 dark:bg-gray-800/80">
|
||||
<thead className="bg-base-200 dark:bg-gray-800/80">
|
||||
<tr>
|
||||
<th scope="col" className="w-16 px-6 py-3 text-center text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th scope="col" className="w-16 px-6 py-3 text-center text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
Rank
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
User
|
||||
</th>
|
||||
<th scope="col" className="w-24 px-6 py-3 text-center text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider">
|
||||
<th scope="col" className="w-24 px-6 py-3 text-center text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
Points
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white/90 dark:bg-gray-900/90 divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<tbody className="bg-base-100 dark:bg-gray-900/90 divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{currentUsers.map((user, index) => {
|
||||
const actualRank = user.points > 0 ? indexOfFirstUser + index + 1 : null;
|
||||
const isCurrentUser = user.id === currentUserId;
|
||||
|
||||
return (
|
||||
<tr key={user.id} className={isCurrentUser ? 'bg-indigo-50 dark:bg-indigo-900/20' : ''}>
|
||||
<tr key={user.id} className={isCurrentUser ? 'bg-primary/10 dark:bg-primary/20' : ''}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
{actualRank ? (
|
||||
actualRank <= 3 ? (
|
||||
|
@ -233,7 +233,7 @@ export default function LeaderboardTable() {
|
|||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-10 w-10">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center overflow-hidden relative">
|
||||
<div className="w-10 h-10 rounded-full bg-base-300 dark:bg-gray-700 flex items-center justify-center overflow-hidden relative">
|
||||
{user.avatar ? (
|
||||
<img className="h-10 w-10 rounded-full" src={user.avatar} alt={user.name} />
|
||||
) : (
|
||||
|
@ -255,7 +255,7 @@ export default function LeaderboardTable() {
|
|||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center font-bold text-indigo-600 dark:text-indigo-400">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center font-bold text-primary dark:text-primary">
|
||||
{user.points}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -270,8 +270,8 @@ export default function LeaderboardTable() {
|
|||
<div className="flex justify-center mt-6">
|
||||
<nav className="flex items-center">
|
||||
<button
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 dark:border-gray-700
|
||||
bg-white/90 dark:bg-gray-800/90 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-base-300 dark:border-gray-700
|
||||
bg-base-100 dark:bg-gray-800/90 text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-base-200 dark:hover:bg-gray-700 shadow-sm"
|
||||
onClick={() => paginate(Math.max(1, currentPage - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
|
@ -284,11 +284,11 @@ export default function LeaderboardTable() {
|
|||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<button
|
||||
key={i + 1}
|
||||
className={`relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-700
|
||||
bg-white/90 dark:bg-gray-800/90 text-sm font-medium ${currentPage === i + 1
|
||||
? 'text-indigo-600 dark:text-indigo-400 border-indigo-500 dark:border-indigo-500 z-10'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
className={`relative inline-flex items-center px-4 py-2 border border-base-300 dark:border-gray-700
|
||||
bg-base-100 dark:bg-gray-800/90 text-sm font-medium ${currentPage === i + 1
|
||||
? 'text-primary dark:text-primary border-primary dark:border-primary z-10 font-bold'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-base-200 dark:hover:bg-gray-700'
|
||||
} shadow-sm`}
|
||||
onClick={() => paginate(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
|
@ -296,8 +296,8 @@ export default function LeaderboardTable() {
|
|||
))}
|
||||
|
||||
<button
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 dark:border-gray-700
|
||||
bg-white/90 dark:bg-gray-800/90 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-base-300 dark:border-gray-700
|
||||
bg-base-100 dark:bg-gray-800/90 text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-base-200 dark:hover:bg-gray-700 shadow-sm"
|
||||
onClick={() => paginate(Math.min(totalPages, currentPage + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
|
@ -312,9 +312,9 @@ export default function LeaderboardTable() {
|
|||
|
||||
{/* Show current user rank if not in current page */}
|
||||
{isAuthenticated && currentUserRank && !currentUsers.some(user => user.id === currentUserId) && (
|
||||
<div className="mt-4 p-3 bg-gray-50/80 dark:bg-gray-800/80 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="mt-4 p-3 bg-base-200 dark:bg-gray-800/80 border border-base-300 dark:border-gray-700 rounded-lg shadow-sm">
|
||||
<p className="text-center text-sm text-gray-700 dark:text-gray-300">
|
||||
Your rank: <span className="font-bold text-indigo-600 dark:text-indigo-400">#{currentUserRank}</span>
|
||||
Your rank: <span className="font-bold text-primary dark:text-primary">#{currentUserRank}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
@ -323,7 +323,7 @@ export default function LeaderboardTable() {
|
|||
{isAuthenticated && currentUserId &&
|
||||
!currentUserRank &&
|
||||
currentUsers.some(user => user.id === currentUserId) && (
|
||||
<div className="mt-4 p-3 bg-gray-50/80 dark:bg-gray-800/80 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="mt-4 p-3 bg-base-200 dark:bg-gray-800/80 border border-base-300 dark:border-gray-700 rounded-lg shadow-sm">
|
||||
<p className="text-center text-sm text-gray-700 dark:text-gray-300">
|
||||
Participate in events to earn points and get ranked!
|
||||
</p>
|
||||
|
|
|
@ -215,11 +215,13 @@ const safeLogtoApiEndpoint = logtoApiEndpoint || "";
|
|||
|
||||
<!-- Display Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
|
||||
class="card bg-card shadow-xl border border-border hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center p-3 rounded-full bg-primary text-primary-foreground"
|
||||
>
|
||||
<Icon name="heroicons:computer-desktop" class="h-5 w-5" />
|
||||
</div>
|
||||
Display Settings
|
||||
|
@ -227,14 +229,18 @@ const safeLogtoApiEndpoint = logtoApiEndpoint || "";
|
|||
<p class="text-sm opacity-70 mb-4">
|
||||
Customize your dashboard appearance and display preferences
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<div class="h-px w-full bg-border my-4"></div>
|
||||
|
||||
<div class="alert alert-warning mb-4">
|
||||
<div
|
||||
class="flex p-4 mb-4 text-sm rounded-lg bg-warning/20 text-warning-foreground"
|
||||
role="alert"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
class="flex-shrink-0 w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
|
@ -258,17 +258,17 @@ export default function DisplaySettings() {
|
|||
{/* Theme Settings */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg mb-2">Theme</h4>
|
||||
<div className="form-control w-full max-w-xs">
|
||||
<div className="w-full max-w-xs">
|
||||
<select
|
||||
value={theme}
|
||||
onChange={handleThemeChange}
|
||||
className="select select-bordered"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Select your preferred theme</span>
|
||||
<label className="mt-1 block">
|
||||
<span className="text-xs text-muted-foreground">Select your preferred theme</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -276,19 +276,19 @@ export default function DisplaySettings() {
|
|||
{/* Font Size Settings */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg mb-2">Font Size</h4>
|
||||
<div className="form-control w-full max-w-xs">
|
||||
<div className="w-full max-w-xs">
|
||||
<select
|
||||
value={fontSize}
|
||||
onChange={handleFontSizeChange}
|
||||
className="select select-bordered"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
<option value="small">Small</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="large">Large</option>
|
||||
<option value="extra-large">Extra Large</option>
|
||||
</select>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">Select your preferred font size</span>
|
||||
<label className="mt-1 block">
|
||||
<span className="text-xs text-muted-foreground">Select your preferred font size</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -297,54 +297,64 @@ export default function DisplaySettings() {
|
|||
<div>
|
||||
<h4 className="font-semibold text-lg mb-2">Accessibility</h4>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={colorBlindMode}
|
||||
onChange={handleColorBlindModeChange}
|
||||
className="toggle toggle-primary"
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Color Blind Mode</span>
|
||||
<p className="text-xs opacity-70">Enhances color contrast and uses color-blind friendly palettes</p>
|
||||
</div>
|
||||
<div className="w-11 h-6 bg-muted rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
<div>
|
||||
<span className="font-medium">Color Blind Mode</span>
|
||||
<p className="text-xs text-muted-foreground">Enhances color contrast and uses color-blind friendly palettes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control mt-2">
|
||||
<label className="cursor-pointer label justify-start gap-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={reducedMotion}
|
||||
onChange={handleReducedMotionChange}
|
||||
className="toggle toggle-primary"
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div>
|
||||
<span className="label-text font-medium">Reduced Motion</span>
|
||||
<p className="text-xs opacity-70">Minimizes animations and transitions</p>
|
||||
</div>
|
||||
<div className="w-11 h-6 bg-muted rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
<div>
|
||||
<span className="font-medium">Reduced Motion</span>
|
||||
<p className="text-xs text-muted-foreground">Minimizes animations and transitions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-info">
|
||||
<p className="text-sm text-blue-500 dark:text-blue-400 mt-4">
|
||||
These settings are saved to your browser using IndexedDB and your IEEE UCSD account. They will be applied whenever you log in.
|
||||
</p>
|
||||
|
||||
<div className="form-control">
|
||||
<div className="mt-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{hasChanges && (
|
||||
<p className="text-sm text-warning">
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400">
|
||||
You have unsaved changes. Click "Save Settings" to apply them.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn btn-primary ${saving ? 'loading' : ''}`}
|
||||
className={`inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 ${saving ? 'opacity-70' : ''}`}
|
||||
disabled={saving || !hasChanges}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
{saving ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Saving...
|
||||
</>
|
||||
) : 'Save Settings'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -84,15 +84,20 @@ export default function ThemeToggle() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="dropdown dropdown-end">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={`btn btn-circle btn-sm ${isLoading ? 'loading' : ''}`}
|
||||
className={`inline-flex items-center justify-center rounded-full w-8 h-8 text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground ${isLoading ? 'opacity-70' : ''}`}
|
||||
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
|
||||
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme (Light mode is experimental)`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{!isLoading && (
|
||||
{isLoading ? (
|
||||
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
) : (
|
||||
theme === 'light' ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
|
@ -104,9 +109,9 @@ export default function ThemeToggle() {
|
|||
)
|
||||
)}
|
||||
</button>
|
||||
<div className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 mt-2 text-xs">
|
||||
<div className="p-2">
|
||||
<p className="font-bold text-warning mb-1">Warning:</p>
|
||||
<div className="absolute right-0 z-10 mt-2 w-52 origin-top-right rounded-md bg-card shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none hidden group-hover:block">
|
||||
<div className="p-3 text-xs">
|
||||
<p className="font-bold text-amber-600 dark:text-amber-400 mb-1">Warning:</p>
|
||||
<p>Light mode is experimental and not fully supported yet. Some UI elements may not display correctly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,41 +2,42 @@
|
|||
import Navbar from "../components/core/Navbar.astro";
|
||||
import Footer from "../components/core/Footer.astro";
|
||||
import InView from "../components/core/InView.astro";
|
||||
import { initTheme } from "../scripts/database/initTheme";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark" class="w-full h-full m-0 bg-ieee-black">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>IEEEUCSD</title>
|
||||
<script
|
||||
src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"
|
||||
></script>
|
||||
<script is:inline>
|
||||
// Set default theme to dark if not already set
|
||||
if (!localStorage.getItem("theme")) {
|
||||
localStorage.setItem("theme", "dark");
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
// Apply saved theme
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
document.documentElement.setAttribute("data-theme", savedTheme);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<InView />
|
||||
<body class="w-full h-full m-0 bg-ieee-black">
|
||||
<div class="text-white min-h-screen">
|
||||
<header class="sticky top-0 w-full z-[999]">
|
||||
<Navbar />
|
||||
</header>
|
||||
<main class="w-[95%] mx-auto">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
<html lang="en" class="w-full h-full m-0 bg-ieee-black">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>IEEEUCSD</title>
|
||||
<script
|
||||
src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"
|
||||
></script>
|
||||
<script is:inline>
|
||||
// Set a default theme until IndexedDB loads
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
</script>
|
||||
</head>
|
||||
<InView />
|
||||
<body class="w-full h-full m-0 bg-ieee-black">
|
||||
<script>
|
||||
// Initialize theme from IndexedDB
|
||||
import { initTheme } from "../scripts/database/initTheme";
|
||||
initTheme().catch((err) =>
|
||||
console.error("Error initializing theme:", err)
|
||||
);
|
||||
</script>
|
||||
<div class="text-white min-h-screen">
|
||||
<header class="sticky top-0 w-full z-[999]">
|
||||
<Navbar />
|
||||
</header>
|
||||
<main class="w-[95%] mx-auto">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -9,8 +9,10 @@ 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 { initTheme } from "../scripts/database/initTheme";
|
||||
import ToastProvider from "../components/dashboard/universal/ToastProvider";
|
||||
import FirstTimeLoginManager from "../components/dashboard/universal/FirstTimeLoginManager";
|
||||
import ThemeToggle from "../components/dashboard/universal/ThemeToggle";
|
||||
|
||||
const title = "Dashboard";
|
||||
|
||||
|
@ -40,7 +42,7 @@ const components = Object.fromEntries(
|
|||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
@ -49,6 +51,10 @@ const components = Object.fromEntries(
|
|||
<script
|
||||
src="https://code.iconify.design/iconify-icon/2.3.0/iconify-icon.min.js"
|
||||
></script>
|
||||
<script is:inline>
|
||||
// Set a default theme until IndexedDB loads
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-base-200">
|
||||
<!-- First Time Login Manager - This handles the onboarding popup for new users -->
|
||||
|
@ -387,6 +393,7 @@ const components = Object.fromEntries(
|
|||
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
||||
import { OfficerTypes } from "../schemas/pocketbase/schema";
|
||||
import { initAuthSync } from "../scripts/database/initAuthSync";
|
||||
import { initTheme } from "../scripts/database/initTheme";
|
||||
|
||||
const auth = Authentication.getInstance();
|
||||
const get = Get.getInstance();
|
||||
|
@ -397,6 +404,11 @@ const components = Object.fromEntries(
|
|||
window.toast = () => {};
|
||||
}
|
||||
|
||||
// Initialize theme from IndexedDB
|
||||
initTheme().catch((err) =>
|
||||
console.error("Error initializing theme:", err)
|
||||
);
|
||||
|
||||
// Initialize page state
|
||||
const pageLoadingState =
|
||||
document.getElementById("pageLoadingState");
|
||||
|
|
Loading…
Reference in a new issue