Compare commits

...

4 commits
main ... test

Author SHA1 Message Date
chark1es
a86140a6b5 change theme 2025-03-28 23:15:02 -07:00
chark1es
e27a78a15b Create initTheme.ts 2025-03-28 23:14:48 -07:00
chark1es
c4278b7773 Create ThemeToggle.tsx 2025-03-28 23:14:48 -07:00
chark1es
d055c5ee69 fix colors 2025-03-28 23:14:48 -07:00
10 changed files with 619 additions and 571 deletions

View file

@ -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>

View file

@ -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();

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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");