From 7e49d43b27d730a51c13d9226e92967697215fc9 Mon Sep 17 00:00:00 2001 From: angelica-gregorio Date: Sat, 25 Apr 2026 00:41:43 +0800 Subject: [PATCH 1/4] feat: add link shortener functionality with UI components - Implemented LinkShortenerClient for creating and managing shortened links. - Created page for link shortener with authentication checks. - Added various UI components including AuthWarning, Button, EmptyState, FeatureCard, FilterButton, Hero, PageHeader, Placeholder, SearchInput, SectionHeader, StatCard, and Text. - Ensured components are styled consistently and support dark mode. --- src/app/academics/resources/page.tsx | 435 ++++++++++++++++ src/app/globals.css | 485 ++++++++++++++++++ .../link-shortener/LinkShortenerClient.tsx | 169 ++++++ src/app/officers/link-shortener/page.tsx | 45 ++ src/components/ui/auth-warning/index.tsx | 35 ++ src/components/ui/button/index.tsx | 28 + src/components/ui/empty-state/index.tsx | 19 + src/components/ui/feature-card/index.tsx | 41 ++ src/components/ui/filter-button/index.tsx | 25 + src/components/ui/hero/index.tsx | 46 ++ src/components/ui/page-header/index.tsx | 37 ++ src/components/ui/placeholder/index.tsx | 45 ++ src/components/ui/search-input/index.tsx | 31 ++ src/components/ui/section-header/index.tsx | 17 + src/components/ui/stat-card/index.tsx | 39 ++ src/components/ui/text/index.tsx | 62 +++ 16 files changed, 1559 insertions(+) create mode 100644 src/app/officers/link-shortener/LinkShortenerClient.tsx create mode 100644 src/app/officers/link-shortener/page.tsx create mode 100644 src/components/ui/auth-warning/index.tsx create mode 100644 src/components/ui/button/index.tsx create mode 100644 src/components/ui/empty-state/index.tsx create mode 100644 src/components/ui/feature-card/index.tsx create mode 100644 src/components/ui/filter-button/index.tsx create mode 100644 src/components/ui/hero/index.tsx create mode 100644 src/components/ui/page-header/index.tsx create mode 100644 src/components/ui/placeholder/index.tsx create mode 100644 src/components/ui/search-input/index.tsx create mode 100644 src/components/ui/section-header/index.tsx create mode 100644 src/components/ui/stat-card/index.tsx create mode 100644 src/components/ui/text/index.tsx diff --git a/src/app/academics/resources/page.tsx b/src/app/academics/resources/page.tsx index eb68e10..7d6cb68 100644 --- a/src/app/academics/resources/page.tsx +++ b/src/app/academics/resources/page.tsx @@ -1,6 +1,441 @@ +<<<<<<< Updated upstream import ResourcesClient from './client'; export default function ResourcesPage() { +======= +'use client'; + +import { useEffect, useState, useRef } from 'react'; +import { useSession } from 'next-auth/react'; +import { BookOpen, FileText, Video, Code, Lock, Download } from 'lucide-react'; +import { PageHeader } from '@/components/ui/page-header'; +import { SearchInput } from '@/components/ui/search-input'; +import { FilterGroup } from '@/components/ui/filter-group'; +import type { FilterOption } from '@/components/ui/filter-group'; +import { Notification } from '@/components/ui/notification'; +import { useNotification } from '@/components/ui/notification/useNotification'; +import { LoadingProgress } from '@/components/ui/loading-progress'; +import { AuthWarning } from '@/components/ui/auth-warning'; +import { EmptyState } from '@/components/ui/empty-state'; + +// Resource categories +const categories: FilterOption[] = [ + { id: 'all', label: 'All Resources', icon: BookOpen }, + { id: 'notes', label: 'Reviewers', icon: FileText }, + { id: 'textbooks', label: 'Textbooks', icon: BookOpen }, + { id: 'videos', label: 'Video Tutorials', icon: Video }, + { id: 'code', label: 'Code Examples', icon: Code }, +]; + +interface DriveFile { + id: string; + name: string; + mimeType?: string; + size?: number; + modifiedTime?: string; +} + +interface ResourcesApiResponse { + files?: DriveFile[]; + error?: string; +} + +interface GroupedFile extends DriveFile { + category: string; + restricted: boolean; + displayName: string; + courseCode?: string | null; + academicYearTag?: string | null; + fileType?: string; + quizNumber?: number | null; +} + +interface CourseGroup { + academicYearTerm: string | null; + files: GroupedFile[]; +} + +const CURRENT_ACADEMIC_YEAR_TAG = '25-26'; +const PREVIOUS_ACADEMIC_YEAR_TAG = '24-25'; +const FILE_FORMAT_REGEX = /^([^_]+)_(\d{2})-(\d{2})-T(\d)(?:_|$)/; + +export default function ResourcesPage() { + const { data: session } = useSession(); + const [selectedCategory, setSelectedCategory] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + const [files, setFiles] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingProgress, setLoadingProgress] = useState(0); + const [resourcesError, setResourcesError] = useState(null); + const { notification, showNotification, hideNotification } = useNotification(); + const [showAuthWarning, setShowAuthWarning] = useState(true); + const [isAuthWarningFading, setIsAuthWarningFading] = useState(false); + const [hasCheckedAuth, setHasCheckedAuth] = useState(false); + const downloadRef = useRef(null); + + // Set user info cookie when logged in and fade out auth warning + useEffect(() => { + // Mark that we've checked auth + if (!hasCheckedAuth) { + setHasCheckedAuth(true); + } + + if (session?.user && showAuthWarning) { + // Start fade out animation + setIsAuthWarningFading(true); + + // Remove after animation completes + const timeout = setTimeout(() => { + setShowAuthWarning(false); + }, 500); + + fetch('/api/set-user-info').catch(() => { + // Silently fail if cookie setting fails + }); + + return () => clearTimeout(timeout); + } + }, [session, hasCheckedAuth, showAuthWarning]); + + useEffect(() => { + (async () => { + try { + const res = await fetch('/api/resources'); + const data: ResourcesApiResponse = await res.json(); + + if (!res.ok) { + const message = data.error || 'Failed to fetch resources'; + setResourcesError(message); + setFiles(data.files || []); + showNotification(message, 'error'); + return; + } + + setResourcesError(null); + setFiles(data.files || []); + } catch { + setResourcesError('Failed to fetch resources'); + setFiles([]); + showNotification('Failed to fetch resources', 'error'); + } finally { + setLoading(false); + } + })(); + }, [showNotification]); + + // Animate loading progress + useEffect(() => { + if (!loading) { + setLoadingProgress(0); + return; + } + + const interval = setInterval(() => { + setLoadingProgress(prev => { + if (prev >= 100) { + clearInterval(interval); + return 100; + } + return prev + Math.random() * 3; + }); + }, 150); + + return () => clearInterval(interval); + }, [loading]); + + + // Extract course code from filename (e.g., "CSYSARC_24-25-T2_QUIZ-1.pdf" -> "CSYSARC") + function extractCourseCode(filename: string): string | null { + const match = filename.match(FILE_FORMAT_REGEX); + if (match) return match[1]; + + // Fallback for non-standard/legacy names + const legacyMatch = filename.match(/^([^_]+)/); + return legacyMatch ? legacyMatch[1] : null; + } + + // Extract quiz/exam number (e.g., "QUIZ-1" -> 1, "EXAM-2" -> 2) + function extractQuizNumber(filename: string): number | null { + const match = filename.match(/(?:QUIZ|EXAM|TEST|ASSESSMENT)[-_]?(\d+)/i); + return match ? parseInt(match[1], 10) : null; + } + + // Extract academic year and term (e.g., "24-25-T2" -> "A.Y. 2024 - 2025 Term 2") + function extractAcademicYearTerm(filename: string): string | null { + const match = filename.match(FILE_FORMAT_REGEX); + if (match) { + const startYear = `20${match[2]}`; + const endYear = `20${match[3]}`; + const term = match[4]; + return `A.Y. ${startYear} - ${endYear} Term ${term}`; + } + return null; + } + + function extractAcademicYearTag(filename: string): string | null { + const match = filename.match(FILE_FORMAT_REGEX); + if (!match) return null; + return `${match[2]}-${match[3]}`; + } + + function formatAcademicYearTag(tag: string | null): string | null { + if (!tag) return null; + const match = tag.match(/^(\d{2})-(\d{2})$/); + if (!match) return tag; + return `A.Y. 20${match[1]} - 20${match[2]}`; + } + + function isCurrentAcademicYear(filename: string): boolean { + return extractAcademicYearTag(filename) === CURRENT_ACADEMIC_YEAR_TAG; + } + + function isPreviousAcademicYear(filename: string): boolean { + return extractAcademicYearTag(filename) === PREVIOUS_ACADEMIC_YEAR_TAG; + } + + // Determine file type (quiz, exam, notes, etc.) + function getFileTypeCategory(filename: string): string { + const lower = filename.toLowerCase(); + if (lower.includes('quiz')) return 'quiz'; + if (lower.includes('exam') || lower.includes('test')) return 'exam'; + if (lower.includes('notes') || lower.includes('lecture')) return 'notes'; + if (lower.includes('lab') || lower.includes('exercise')) return 'lab'; + if (lower.includes('project')) return 'project'; + return 'other'; + } + + // Categorize files based on naming convention + function categorizeFile(filename: string): string { + const lower = filename.toLowerCase(); + if (lower.includes('textbook') || lower.includes('book')) return 'textbooks'; + if (lower.includes('notes') || lower.includes('lecture')) return 'notes'; + if (lower.includes('video') || lower.includes('tutorial') || lower.includes('.mp4') || lower.includes('.mkv')) return 'videos'; + if (lower.includes('code') || lower.includes('.zip') || lower.includes('example')) return 'code'; + return 'notes'; // Default category + } + + // Check if file is restricted (members only) + function isRestricted(filename: string): boolean { + const lower = filename.toLowerCase(); + return lower.includes('[restricted]') || lower.includes('[members]'); + } + + // Filter and organize files with grouping + function getFilteredFiles() { + if (!files) return []; + + return files + .filter((file: DriveFile) => { + if (file.mimeType === 'application/vnd.google-apps.folder') return false; + + const category = categorizeFile(file.name); + const matchesCategory = selectedCategory === 'all' || category === selectedCategory; + const matchesSearch = file.name.toLowerCase().includes(searchQuery.toLowerCase()); + + return matchesCategory && matchesSearch; + }) + .map((file: DriveFile): GroupedFile => ({ + ...file, + category: categorizeFile(file.name), + restricted: isRestricted(file.name), + displayName: file.name.replace(/\[restricted\]/gi, '').replace(/\[members\]/gi, '').trim(), + courseCode: extractCourseCode(file.name), + academicYearTag: extractAcademicYearTag(file.name), + fileType: getFileTypeCategory(file.name), + quizNumber: extractQuizNumber(file.name), + })); + } + + // Group files by course code + function groupByCourse(files: GroupedFile[]): Map { + const grouped = new Map(); + + files.forEach(file => { + const key = `${file.courseCode || 'Other'}::${file.academicYearTag || 'Unknown'}`; + if (!grouped.has(key)) { + grouped.set(key, { + academicYearTerm: formatAcademicYearTag(file.academicYearTag) || extractAcademicYearTerm(file.name), + files: [] + }); + } + // Update academic year/term if we find one and don't have it yet + if (!grouped.get(key)!.academicYearTerm) { + const ayTerm = formatAcademicYearTag(file.academicYearTag) || extractAcademicYearTerm(file.name); + if (ayTerm) { + grouped.get(key)!.academicYearTerm = ayTerm; + } + } + grouped.get(key)!.files.push(file); + }); + + // Sort files within each group by quiz number or name + grouped.forEach((groupData) => { + groupData.files.sort((a, b) => { + if (a.quizNumber && b.quizNumber) { + return a.quizNumber - b.quizNumber; + } + return a.name.localeCompare(b.name); + }); + }); + + return grouped; + } + + function getFileSize(bytes?: number): string { + if (!bytes) return 'Unknown'; + const mb = bytes / (1024 * 1024); + if (mb < 1) return `${(bytes / 1024).toFixed(1)} KB`; + if (mb < 1024) return `${mb.toFixed(1)} MB`; + return `${(mb / 1024).toFixed(2)} GB`; + } + + function initiateDownload(fileId: string, fileName: string) { + showNotification('Preparing your download', 'info'); + fetch(`/api/resources/download?fileId=${encodeURIComponent(fileId)}`) + .then(async res => { + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'Download failed'); + } + let filename = fileName; + const contentDisposition = res.headers.get('Content-Disposition'); + if (contentDisposition) { + const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (match && match[1]) { + filename = match[1].replace(/['"]/g, '').trim(); + } + } + return { filename, blob: await res.blob() }; + }) + .then(data => { + const blobUrl = URL.createObjectURL(data.blob); + if (downloadRef.current) { + downloadRef.current.href = blobUrl; + downloadRef.current.download = data.filename; + downloadRef.current.click(); + URL.revokeObjectURL(blobUrl); + } + showNotification('Download started', 'success'); + }) + .catch(error => { + showNotification(error.message || 'Download failed', 'error'); + }); + } + + const filteredFiles = getFilteredFiles(); + const groupedByCourse = groupByCourse(filteredFiles); + const nonReviewerFiles = filteredFiles.filter((file) => file.category !== 'notes'); + const groupedNonReviewerFiles = groupByCourse(nonReviewerFiles); + const reviewerFilesThisYear = filteredFiles.filter( + (file) => file.category === 'notes' && isCurrentAcademicYear(file.name), + ); + const reviewerFilesPreviousYears = filteredFiles.filter( + (file) => file.category === 'notes' && isPreviousAcademicYear(file.name), + ); + const groupedReviewersThisYear = groupByCourse(reviewerFilesThisYear); + const groupedReviewersPreviousYears = groupByCourse(reviewerFilesPreviousYears); + + function renderCourseGrid(groupedData: Map) { + return ( +
+ {Array.from(groupedData.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([courseKey, courseData]) => { + const [courseCode] = courseKey.split('::'); + + return ( +
+
+

+ {courseCode} +

+ {courseData.academicYearTerm && ( +

+ {courseData.academicYearTerm} +

+ )} +
+ + {(() => { + const byType = new Map(); + courseData.files.forEach((file) => { + const type = file.fileType || 'other'; + if (!byType.has(type)) byType.set(type, []); + byType.get(type)!.push(file); + }); + + return Array.from(byType.entries()).map(([fileType, typeFiles]) => ( +
+

+ {fileType === 'quiz' + ? 'Quizzes' + : fileType === 'exam' + ? 'Exams' + : fileType === 'notes' + ? 'Notes' + : fileType === 'lab' + ? 'Labs' + : fileType === 'project' + ? 'Projects' + : 'Resources'} +

+ +
+ {typeFiles.map((file) => { + const isLocked = file.restricted && !session; + return ( +
+ + + {file.size && ( +
+
+ {getFileSize(file.size)} +
+
+ )} +
+ ); + })} +
+
+ )); + })()} +
+ ); + })} +
+ ); + } +>>>>>>> Stashed changes return (
diff --git a/src/app/globals.css b/src/app/globals.css index ad75cd8..1742462 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,14 @@ @import "tailwindcss"; :root { +<<<<<<< Updated upstream --background: #ffffff; --foreground: #171717; +======= + --background: #ffffff; + --foreground: #171717; + color-scheme: light; +>>>>>>> Stashed changes } @theme inline { @@ -11,10 +17,18 @@ } @media (prefers-color-scheme: dark) { +<<<<<<< Updated upstream :root { --background: #0a0a0a; --foreground: #ededed; } +======= + :root { + --background: #0a0a0a; + --foreground: #ededed; + color-scheme: dark; + } +>>>>>>> Stashed changes } /* Hero Button Styles */ @@ -41,6 +55,12 @@ body { box-shadow: inset 0 0 200px rgba(0, 0, 0, 0.6); } +@media (prefers-color-scheme: light) { + body { + box-shadow: inset 0 0 140px rgba(148, 163, 184, 0.18); + } +} + /* Header visibility transitions */ .header-element { transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; @@ -784,6 +804,7 @@ body { /* File size tooltip - 3D glassmorphism */ .file-size-tooltip { +<<<<<<< Updated upstream background: rgba(28, 28, 28, 0.95); border-radius: 0.5rem; -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(1.2); @@ -800,6 +821,363 @@ body { 0 2px 8px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 -1px 0 rgba(0, 0, 0, 0.3); +======= + background: rgba(28, 28, 28, 0.95); + border-radius: 0.5rem; + -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(1.2); + backdrop-filter: blur(40px) saturate(180%) brightness(1.2); + border: 1.5px solid rgba(255, 255, 255, 0.3); + padding: 0.375rem 0.75rem; + color: #ffffff; + font-size: 0.75rem; + font-weight: 500; + white-space: nowrap; + + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.6), + 0 2px 8px rgba(0, 0, 0, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.2), + inset 0 -1px 0 rgba(0, 0, 0, 0.3); +} + +/* Light mode overrides for resources glass UI */ +@media (prefers-color-scheme: light) { + body { + background: + radial-gradient(circle at top, rgba(94, 148, 50, 0.08), transparent 32%), + linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); + color: #0f172a; + box-shadow: inset 0 0 140px rgba(148, 163, 184, 0.18); + } + + .navbar, + .logo-pill, + .login-pill, + .footer, + .login-expanded-content, + .modal-panel, + .auth-warning-container, + .input-3d, + .navbar-dropdown-menu { + background: rgba(255, 255, 255, 0.72); + border-color: rgba(203, 213, 225, 0.85); + box-shadow: + 0 12px 32px rgba(15, 23, 42, 0.14), + 0 2px 8px rgba(15, 23, 42, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.82), + inset 0 -1px 0 rgba(148, 163, 184, 0.1); + } + + .navbar, + .logo-pill, + .login-pill, + .footer, + .login-expanded-content, + .modal-panel, + .auth-warning-container, + .navbar-dropdown-menu, + .glass-card, + .glass-card-3d { + backdrop-filter: blur(28px) saturate(170%) brightness(1.04); + -webkit-backdrop-filter: blur(28px) saturate(170%) brightness(1.04); + } + + .navbar, + .logo-pill, + .login-pill, + .footer { + color: #0f172a; + } + + .navbar::before, + .logo-pill::before, + .login-pill::before, + .footer::before, + .auth-warning-container::before, + .glass-card::before, + .glass-card-3d::before { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.55) 0%, + rgba(241, 245, 249, 0.24) 50%, + rgba(148, 163, 184, 0.12) 100% + ); + } + + .navbar, + .logo-pill, + .login-pill, + .search-bar, + .category-filter, + .resource-button, + .glass-card, + .glass-card, + .glass-card-3d, + .course-card, + .auth-warning-container, + .navbar-dropdown-menu, + .glass-card, + .glass-card-3d { + } + + .navbar { + color: #1e293b; + text-shadow: none; + } + + .navbar-links a, + .navbar-item, + .navbar-dropdown-trigger, + .navbar-label, + .login-description, + .footer-text, + .footer-links, + .footer-links a, + .navbar-dropdown-item, + .input-3d, + .auth-warning-container, + .modal-panel, + .login-header, + .login-expanded-content { + color: #0f172a; + text-shadow: none; + } + + .navbar-links a:hover, + .navbar-item:hover, + .navbar-dropdown-trigger:hover { + color: #4e8d1f; + } + + .navbar-dropdown-item:hover, + .navbar-dropdown-trigger:hover, + .footer-links a:hover { + background: rgba(94, 148, 50, 0.08); + } + + .resource-button { + color: #ffffff; + } + + .resource-button-locked { + background: rgba(148, 163, 184, 0.16); + border-color: rgba(148, 163, 184, 0.28); + color: #475569; + } + + .search-bar, + .category-filter, + .input-3d, + .navbar-dropdown-menu, + .navbar-dropdown-item, + .modal-panel, + .login-expanded-content { + color: #0f172a; + } + + .input-3d::placeholder, + .search-bar input::placeholder { + color: #64748b; + } + + .modal-backdrop { + background: rgba(226, 232, 240, 0.55); + backdrop-filter: blur(18px) saturate(120%); + } + + .glass-card, + .glass-card-3d, + .course-card, + .auth-warning-container { + background: linear-gradient( + 135deg, + rgba(78, 141, 31, 0.96) 0%, + rgba(94, 148, 50, 0.92) 100% + ); + border: 1.5px solid rgba(255, 255, 255, 0.18); + box-shadow: + 0 12px 34px rgba(78, 141, 31, 0.22), + 0 3px 10px rgba(15, 23, 42, 0.08), + inset 0 1px rgba(255, 255, 255, 0.18), + inset 0 -1px rgba(15, 23, 42, 0.12); + } + + .glass-card:hover, + .glass-card-3d:hover, + .course-card:hover, + .auth-warning-container:hover { + background: linear-gradient( + 135deg, + rgba(68, 127, 27, 0.98) 0%, + rgba(78, 141, 31, 0.96) 100% + ); + border-color: rgba(255, 255, 255, 0.22); + box-shadow: + 0 16px 42px rgba(78, 141, 31, 0.28), + 0 6px 16px rgba(15, 23, 42, 0.1), + inset 0 1px rgba(255, 255, 255, 0.2), + inset 0 -1px rgba(15, 23, 42, 0.12); + } + + .glass-card h1, + .glass-card h2, + .glass-card h3, + .glass-card h4, + .glass-card h5, + .glass-card h6, + .glass-card p, + .glass-card span, + .glass-card label, + .glass-card small, + .glass-card-3d h1, + .glass-card-3d h2, + .glass-card-3d h3, + .glass-card-3d h4, + .glass-card-3d h5, + .glass-card-3d h6, + .glass-card-3d p, + .glass-card-3d span, + .glass-card-3d label, + .glass-card-3d small, + .course-card h1, + .course-card h2, + .course-card h3, + .course-card h4, + .course-card h5, + .course-card h6, + .course-card p, + .course-card span, + .course-card label, + .course-card small, + .auth-warning-container h1, + .auth-warning-container h2, + .auth-warning-container h3, + .auth-warning-container h4, + .auth-warning-container h5, + .auth-warning-container h6, + .auth-warning-container p, + .auth-warning-container span, + .auth-warning-container label, + .auth-warning-container small { + color: #ffffff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); + } + + .glass-card svg, + .glass-card-3d svg, + .course-card svg, + .auth-warning-container svg { + color: #ffffff; + } + + .hero-button-secondary { + background: rgba(255, 255, 255, 0.72); + border-color: rgba(203, 213, 225, 0.9); + color: #0f172a; + box-shadow: + 0 4px 16px rgba(15, 23, 42, 0.12), + 0 1px 4px rgba(15, 23, 42, 0.08), + inset 0 1px rgba(255, 255, 255, 0.8); + } + + .hero-button-secondary:hover { + background: rgba(255, 255, 255, 0.88); + } + + .course-card::before { + background: linear-gradient( + 140deg, + rgba(255, 255, 255, 0.12) 0%, + rgba(255, 255, 255, 0.05) 50%, + rgba(15, 23, 42, 0.08) 100% + ); + } + + .glass-card::before, + .glass-card-3d::before, + .auth-warning-container::before { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.12) 0%, + rgba(255, 255, 255, 0.04) 50%, + rgba(15, 23, 42, 0.08) 100% + ); + } + + .file-size-tooltip { + background: rgba(255, 255, 255, 0.94); + border: 1px solid rgba(148, 163, 184, 0.55); + color: #0f172a; + box-shadow: + 0 8px 24px rgba(15, 23, 42, 0.2), + 0 2px 8px rgba(15, 23, 42, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.85), + inset 0 -1px 0 rgba(148, 163, 184, 0.18); + } + + .glass-card-3d { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.78) 0%, + rgba(248, 250, 252, 0.7) 100% + ); + border: 1.5px solid rgba(255, 255, 255, 0.82); + box-shadow: + 0 8px 32px rgba(15, 23, 42, 0.14), + inset 0 1px rgba(255, 255, 255, 0.85), + inset 0 -1px rgba(148, 163, 184, 0.1); + } + + .glass-card-3d:hover { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.88) 0%, + rgba(241, 245, 249, 0.8) 100% + ); + border-color: rgba(203, 213, 225, 0.9); + box-shadow: + 0 12px 48px rgba(15, 23, 42, 0.18), + 0 4px 16px rgba(15, 23, 42, 0.12), + inset 0 1px rgba(255, 255, 255, 0.95), + inset 0 -1px rgba(148, 163, 184, 0.12); + } + + .search-bar, + .category-filter { + background: rgba(255, 255, 255, 0.72); + border-color: rgba(203, 213, 225, 0.8); + box-shadow: + 0 8px 24px rgba(15, 23, 42, 0.12), + 0 2px 8px rgba(15, 23, 42, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.8); + } + + .search-bar:focus-within, + .category-filter:hover { + border-color: rgba(94, 148, 50, 0.45); + box-shadow: + 0 10px 28px rgba(15, 23, 42, 0.14), + 0 0 0 3px rgba(94, 148, 50, 0.12), + inset 0 1px 0 rgba(255, 255, 255, 0.85); + } + + .resource-button { + box-shadow: + 0 4px 16px rgba(94, 148, 50, 0.18), + 0 1px 4px rgba(15, 23, 42, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.22), + inset 0 -1px 0 rgba(15, 23, 42, 0.08); + } + + .resource-button:hover:not(:disabled) { + box-shadow: + 0 8px 24px rgba(94, 148, 50, 0.22), + 0 2px 8px rgba(15, 23, 42, 0.12), + 0 0 0 2px rgba(94, 148, 50, 0.12), + inset 0 1px 0 rgba(255, 255, 255, 0.28), + inset 0 -1px 0 rgba(15, 23, 42, 0.08); + } +>>>>>>> Stashed changes } /* Login container and expandable box */ @@ -1706,6 +2084,113 @@ body { transform: translateY(-4px) scale(1.02); } +@media (prefers-color-scheme: light) { + .glass-card, + .glass-card-3d, + .course-card, + .auth-warning-container, + .modal-panel, + .login-expanded-content { + background: linear-gradient( + 135deg, + rgba(78, 141, 31, 0.98) 0%, + rgba(94, 148, 50, 0.94) 100% + ) !important; + border-color: rgba(255, 255, 255, 0.2) !important; + box-shadow: + 0 14px 36px rgba(78, 141, 31, 0.24), + 0 4px 14px rgba(15, 23, 42, 0.1), + inset 0 1px rgba(255, 255, 255, 0.2), + inset 0 -1px rgba(15, 23, 42, 0.12) !important; + } + + .glass-card:hover, + .glass-card-3d:hover, + .course-card:hover, + .auth-warning-container:hover, + .modal-panel:hover { + background: linear-gradient( + 135deg, + rgba(68, 127, 27, 0.98) 0%, + rgba(78, 141, 31, 0.98) 100% + ) !important; + border-color: rgba(255, 255, 255, 0.24) !important; + } + + .glass-card h1, + .glass-card h2, + .glass-card h3, + .glass-card h4, + .glass-card h5, + .glass-card h6, + .glass-card p, + .glass-card span, + .glass-card label, + .glass-card small, + .glass-card-3d h1, + .glass-card-3d h2, + .glass-card-3d h3, + .glass-card-3d h4, + .glass-card-3d h5, + .glass-card-3d h6, + .glass-card-3d p, + .glass-card-3d span, + .glass-card-3d label, + .glass-card-3d small, + .course-card h1, + .course-card h2, + .course-card h3, + .course-card h4, + .course-card h5, + .course-card h6, + .course-card p, + .course-card span, + .course-card label, + .course-card small, + .auth-warning-container h1, + .auth-warning-container h2, + .auth-warning-container h3, + .auth-warning-container h4, + .auth-warning-container h5, + .auth-warning-container h6, + .auth-warning-container p, + .auth-warning-container span, + .auth-warning-container label, + .auth-warning-container small, + .modal-panel h1, + .modal-panel h2, + .modal-panel h3, + .modal-panel h4, + .modal-panel h5, + .modal-panel h6, + .modal-panel p, + .modal-panel span, + .modal-panel label, + .modal-panel small, + .login-expanded-content h1, + .login-expanded-content h2, + .login-expanded-content h3, + .login-expanded-content h4, + .login-expanded-content h5, + .login-expanded-content h6, + .login-expanded-content p, + .login-expanded-content span, + .login-expanded-content label, + .login-expanded-content small { + color: #ffffff !important; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35) !important; + } + + .glass-card svg, + .glass-card-3d svg, + .course-card svg, + .auth-warning-container svg, + .modal-panel svg, + .login-expanded-content svg { + color: #ffffff !important; + } +} + /* Custom green color overrides for brand color #4e8d1f */ .text-green-400 { color: #4e8d1f !important; diff --git a/src/app/officers/link-shortener/LinkShortenerClient.tsx b/src/app/officers/link-shortener/LinkShortenerClient.tsx new file mode 100644 index 0000000..26e8cfe --- /dev/null +++ b/src/app/officers/link-shortener/LinkShortenerClient.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import { Plus, Rocket } from 'lucide-react'; + +type LinkItem = { + id: number; + slug: string; + target_url: string; + clicks: number; + created_at: string; + created_by?: string | null; + expires_at?: string | null; + archived?: number | null; +}; + +export default function LinkShortenerClient() { + const [links, setLinks] = useState([]); + const [url, setUrl] = useState(''); + const [alias, setAlias] = useState(''); + const [expiresAt, setExpiresAt] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + async function fetchLinks() { + try { + const res = await fetch('/api/link-shortener'); + if (!res.ok) throw new Error('Failed to fetch links'); + const data = await res.json(); + setLinks(data.links || []); + } catch (err) { + const e = err as Error | { message?: string } | undefined; + setError(e?.message || 'Unknown error'); + } + } + + useEffect(() => { + fetchLinks(); + }, []); + + async function handleCreate(e: React.FormEvent) { + e.preventDefault(); + setError(null); + if (!url) return setError('Please enter a URL'); + setLoading(true); + try { + const res = await fetch('/api/link-shortener', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ target_url: url, slug: alias || undefined, expires_at: expiresAt || null }) + }); + + const body = await res.json(); + if (!res.ok) throw new Error(body?.error || 'Failed to create link'); + + setUrl(''); + setAlias(''); + setExpiresAt(null); + fetchLinks(); + } catch (err) { + const e = err as Error | { message?: string } | undefined; + setError(e?.message || 'Unknown error'); + } finally { + setLoading(false); + } + } + + return ( +
+
+ +
+ + {/* Modal */} + {isModalOpen && ( +
+
setIsModalOpen(false)} aria-hidden /> +
+
+

New Short Link

+ +
+
{ await handleCreate(e); setIsModalOpen(false); }} className="space-y-4"> +
+ + setUrl(e.target.value)} + type="url" + placeholder="https://example.com/very-long-url" + className="input-3d w-full placeholder:text-zinc-400 dark:placeholder:text-gray-400" + /> +
+
+ + setAlias(e.target.value)} + type="text" + placeholder="my-link" + className="input-3d w-full placeholder:text-zinc-400 dark:placeholder:text-gray-400" + /> +
+
+ + setExpiresAt(e.target.value || null)} + type="datetime-local" + className="input-3d w-full placeholder:text-zinc-400 dark:placeholder:text-gray-400" + /> +
+
+ {error &&

{error}

} + + +
+
+
+
+ )} + +
+
+
+

Create Short Link

+
+ +
+ +
+

Recent Links

+
+ {links.length === 0 &&

No links yet.

} + {links.map((l) => ( +
+
+
+

{typeof window !== 'undefined' ? `${window.location.host}/${l.slug}` : l.slug}

+

→ {l.target_url}

+ {l.expires_at &&

Expires: {new Date(l.expires_at).toLocaleString()}

} +
+
+

Clicks: {l.clicks}

+ +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/app/officers/link-shortener/page.tsx b/src/app/officers/link-shortener/page.tsx new file mode 100644 index 0000000..98ad6a1 --- /dev/null +++ b/src/app/officers/link-shortener/page.tsx @@ -0,0 +1,45 @@ +import { redirect } from 'next/navigation'; +import { auth } from '@/lib/auth'; +import { getCloudflareContext } from '@opennextjs/cloudflare'; +import LinkShortenerClient from './LinkShortenerClient'; + +export default async function LinkShortenerPage() { + const session = await auth(); + + if (!session?.user?.email) { + redirect('/'); + } + + const { env } = await getCloudflareContext(); + const { DB } = env; + + // Check if the user is an officer + const result = await DB.prepare( + 'SELECT id, name, position FROM officers WHERE email = ?' + ).bind(session.user.email).first(); + + if (!result) { + redirect('/'); + } + + return ( +
+
+
+
+
+ Officer Tools +
+

+ Link Shortener +

+

+ Create and manage shortened links for ACCESS resources and events. +

+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/ui/auth-warning/index.tsx b/src/components/ui/auth-warning/index.tsx new file mode 100644 index 0000000..f07ea30 --- /dev/null +++ b/src/components/ui/auth-warning/index.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { Lock } from "lucide-react"; +import { AuthWarningProps } from "./types"; + +export function AuthWarning({ + title, + message, + icon: Icon = Lock, + isFading = false, + className = "", +}: AuthWarningProps) { + return ( +
+ +

+ {title} +

+

+ {message} +

+
+ ); +} + +export default AuthWarning; + diff --git a/src/components/ui/button/index.tsx b/src/components/ui/button/index.tsx new file mode 100644 index 0000000..51ecc1b --- /dev/null +++ b/src/components/ui/button/index.tsx @@ -0,0 +1,28 @@ +"use client"; + +import Link from "next/link"; +import { ButtonProps } from "./types"; + +export function Button({ + children, + href, + variant = "primary", + className = "", +}: ButtonProps) { + const variantClasses = { + primary: "hero-button-primary text-white", + secondary: "hero-button-secondary text-zinc-200 dark:text-gray-300", + }; + + return ( + + {children} + + ); +} + +export default Button; + diff --git a/src/components/ui/empty-state/index.tsx b/src/components/ui/empty-state/index.tsx new file mode 100644 index 0000000..f42a2d3 --- /dev/null +++ b/src/components/ui/empty-state/index.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { EmptyStateProps } from "./types"; + +export function EmptyState({ message, className = "" }: EmptyStateProps) { + return ( +
+

+ {message} +

+
+ ); +} + +export default EmptyState; + diff --git a/src/components/ui/feature-card/index.tsx b/src/components/ui/feature-card/index.tsx new file mode 100644 index 0000000..52872c7 --- /dev/null +++ b/src/components/ui/feature-card/index.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { ArrowRight } from "lucide-react"; +import Card from "../card"; +import { FeatureCardProps } from "./types"; + +export function FeatureCard({ + icon: Icon, + title, + description, + linkText, + href, + className = "", +}: FeatureCardProps) { + return ( + + +

+ {title} +

+

+ {description} +

+ + {linkText} + +
+ ); +} + +export default FeatureCard; + diff --git a/src/components/ui/filter-button/index.tsx b/src/components/ui/filter-button/index.tsx new file mode 100644 index 0000000..9f96879 --- /dev/null +++ b/src/components/ui/filter-button/index.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { FilterButtonProps } from "./types"; + +export function FilterButton({ + label, + icon: Icon, + isActive, + onClick, + className = "", +}: FilterButtonProps) { + return ( + + ); +} + +export default FilterButton; + diff --git a/src/components/ui/hero/index.tsx b/src/components/ui/hero/index.tsx new file mode 100644 index 0000000..88071cd --- /dev/null +++ b/src/components/ui/hero/index.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { HeroProps } from "./types"; + +export function Hero({ + title, + subtitle, + description, + actions, + className = "", +}: HeroProps) { + return ( +
+
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} + {description && ( +

+ {description} +

+ )} + {actions && ( +
{actions}
+ )} +
+
+ ); +} + +export default Hero; + diff --git a/src/components/ui/page-header/index.tsx b/src/components/ui/page-header/index.tsx new file mode 100644 index 0000000..a949f98 --- /dev/null +++ b/src/components/ui/page-header/index.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { PageHeaderProps } from "./types"; + +export function PageHeader({ + title, + description, + children, + className = "", + centered = true, +}: PageHeaderProps) { + return ( +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} + {children} +
+ ); +} + +export default PageHeader; diff --git a/src/components/ui/placeholder/index.tsx b/src/components/ui/placeholder/index.tsx new file mode 100644 index 0000000..b460175 --- /dev/null +++ b/src/components/ui/placeholder/index.tsx @@ -0,0 +1,45 @@ +"use client"; + +import * as LucideIcons from "lucide-react"; +import Card from "../card"; +import { PlaceholderProps } from "./types"; + +export function Placeholder({ + title = "Coming Soon", + message = "This content is currently being developed. Please check back later.", + icon = "History", + className = "", + cardSize = "full", + children, +}: PlaceholderProps) { + // Dynamically get the icon component from Lucide icons with proper typing + const IconComponent = LucideIcons[icon] as React.ComponentType< + React.SVGProps + >; + const Icon = IconComponent || LucideIcons.History; + + return ( + +
+ +
+

+ {title} +

+

+ {message} +

+ {children} +
+
+
+ ); +} + +export default Placeholder; diff --git a/src/components/ui/search-input/index.tsx b/src/components/ui/search-input/index.tsx new file mode 100644 index 0000000..ab9fbb3 --- /dev/null +++ b/src/components/ui/search-input/index.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Search } from "lucide-react"; +import { SearchInputProps } from "./types"; + +export function SearchInput({ + value, + onChange, + placeholder = "Search...", + icon: Icon = Search, + className = "", +}: SearchInputProps) { + return ( +
+
+ + onChange(e.target.value)} + className="w-full pl-12 pr-4 py-3 bg-transparent text-zinc-950 dark:text-white placeholder:text-zinc-500 dark:placeholder:text-gray-400 focus:outline-none relative" + style={{ fontFamily: "var(--font-manrope)" }} + /> +
+
+ ); +} + +export default SearchInput; + diff --git a/src/components/ui/section-header/index.tsx b/src/components/ui/section-header/index.tsx new file mode 100644 index 0000000..3088a2c --- /dev/null +++ b/src/components/ui/section-header/index.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { SectionHeaderProps } from "./types"; + +export function SectionHeader({ title, className = "" }: SectionHeaderProps) { + return ( +

+ {title} +

+ ); +} + +export default SectionHeader; + diff --git a/src/components/ui/stat-card/index.tsx b/src/components/ui/stat-card/index.tsx new file mode 100644 index 0000000..406cc95 --- /dev/null +++ b/src/components/ui/stat-card/index.tsx @@ -0,0 +1,39 @@ +"use client"; + +import Card from "../card"; +import { StatCardProps } from "./types"; + +export function StatCard({ + icon: Icon, + value, + title, + description, + className = "", +}: StatCardProps) { + return ( + + +
+ {value} +
+

+ {title} +

+

+ {description} +

+
+ ); +} + +export default StatCard; + diff --git a/src/components/ui/text/index.tsx b/src/components/ui/text/index.tsx new file mode 100644 index 0000000..a3bc9a2 --- /dev/null +++ b/src/components/ui/text/index.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { ElementType } from "react"; +import { TextProps, TextVariant } from "./types"; + +const variantStyles: Record = { + h1: "text-5xl font-bold text-zinc-950 dark:text-white", + h2: "text-3xl font-bold text-zinc-950 dark:text-white", + h3: "text-xl font-semibold text-zinc-950 dark:text-white", + h4: "text-lg font-semibold text-zinc-950 dark:text-white", + body: "text-zinc-600 dark:text-gray-300", + "body-relaxed": "text-zinc-600 dark:text-gray-300 leading-relaxed", + description: "text-zinc-600 dark:text-gray-300 text-xl", + caption: "text-sm font-semibold text-zinc-500 dark:text-gray-400 uppercase tracking-wide", + label: "text-sm text-zinc-500 dark:text-gray-400", +}; + +const variantFonts: Record = { + h1: "var(--font-poppins)", + h2: "var(--font-poppins)", + h3: "var(--font-poppins)", + h4: "var(--font-poppins)", + body: "var(--font-manrope)", + "body-relaxed": "var(--font-manrope)", + description: "var(--font-manrope)", + caption: "var(--font-manrope)", + label: "var(--font-manrope)", +}; + +const defaultAs: Record = { + h1: "h1", + h2: "h2", + h3: "h3", + h4: "h4", + body: "p", + "body-relaxed": "p", + description: "p", + caption: "span", + label: "span", +}; + +export function Text({ + children, + variant = "body", + as, + className = "", +}: TextProps) { + const Component = (as || defaultAs[variant]) as ElementType; + const style = { fontFamily: variantFonts[variant] }; + + return ( + + {children} + + ); +} + +export default Text; + From a178bd8bcfa577218601480c14ba09460ca1397c Mon Sep 17 00:00:00 2001 From: angelica-gregorio Date: Thu, 30 Apr 2026 23:30:47 +0800 Subject: [PATCH 2/4] feat: implement core UI component library and add authenticated link shortener page --- src/app/academics/resources/page.tsx | 167 ------------------ src/app/globals.css | 75 +------- .../link-shortener/LinkShortenerClient.tsx | 57 ------ src/app/officers/link-shortener/page.tsx | 13 -- src/components/ui/auth-warning/index.tsx | 10 -- src/components/ui/button/index.tsx | 4 - src/components/ui/empty-state/index.tsx | 4 - src/components/ui/feature-card/index.tsx | 8 - src/components/ui/filter-button/index.tsx | 4 - src/components/ui/hero/index.tsx | 12 -- src/components/ui/page-header/index.tsx | 8 - src/components/ui/placeholder/index.tsx | 8 - src/components/ui/search-input/index.tsx | 8 - src/components/ui/section-header/index.tsx | 4 - src/components/ui/stat-card/index.tsx | 8 - src/components/ui/text/index.tsx | 12 -- 16 files changed, 1 insertion(+), 401 deletions(-) diff --git a/src/app/academics/resources/page.tsx b/src/app/academics/resources/page.tsx index 85e8e1d..bdf0d1d 100644 --- a/src/app/academics/resources/page.tsx +++ b/src/app/academics/resources/page.tsx @@ -1,9 +1,3 @@ -<<<<<<< HEAD -<<<<<<< Updated upstream -import ResourcesClient from './client'; - -export default function ResourcesPage() { -======= 'use client'; import { useEffect, useState, useRef } from 'react'; @@ -61,64 +55,6 @@ const PREVIOUS_ACADEMIC_YEAR_TAG = '24-25'; const FILE_FORMAT_REGEX = /^([^_]+)_(\d{2})-(\d{2})-T(\d)(?:_|$)/; export default function ResourcesPage() { -======= -'use client'; - -import { useEffect, useState, useRef } from 'react'; -import { useSession } from 'next-auth/react'; -import { BookOpen, FileText, Video, Code, Lock, Download } from 'lucide-react'; -import { PageHeader } from '@/components/ui/page-header'; -import { SearchInput } from '@/components/ui/search-input'; -import { FilterGroup } from '@/components/ui/filter-group'; -import type { FilterOption } from '@/components/ui/filter-group'; -import { Notification } from '@/components/ui/notification'; -import { useNotification } from '@/components/ui/notification/useNotification'; -import { LoadingProgress } from '@/components/ui/loading-progress'; -import { AuthWarning } from '@/components/ui/auth-warning'; -import { EmptyState } from '@/components/ui/empty-state'; - -// Resource categories -const categories: FilterOption[] = [ - { id: 'all', label: 'All Resources', icon: BookOpen }, - { id: 'notes', label: 'Reviewers', icon: FileText }, - { id: 'textbooks', label: 'Textbooks', icon: BookOpen }, - { id: 'videos', label: 'Video Tutorials', icon: Video }, - { id: 'code', label: 'Code Examples', icon: Code }, -]; - -interface DriveFile { - id: string; - name: string; - mimeType?: string; - size?: number; - modifiedTime?: string; -} - -interface ResourcesApiResponse { - files?: DriveFile[]; - error?: string; -} - -interface GroupedFile extends DriveFile { - category: string; - restricted: boolean; - displayName: string; - courseCode?: string | null; - fileType?: string; - quizNumber?: number | null; -} - -interface CourseGroup { - academicYearTerm: string | null; - files: GroupedFile[]; -} - -const CURRENT_ACADEMIC_YEAR_TAG = '25-26'; -const PREVIOUS_ACADEMIC_YEAR_TAG = '24-25'; -const FILE_FORMAT_REGEX = /^([^_]+)_(\d{2})-(\d{2})-T(\d)(?:_|$)/; - -export default function ResourcesPage() { ->>>>>>> origin/test const { data: session } = useSession(); const [selectedCategory, setSelectedCategory] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); @@ -237,7 +173,6 @@ export default function ResourcesPage() { return `${match[2]}-${match[3]}`; } -<<<<<<< HEAD function formatAcademicYearTag(tag: string | null): string | null { if (!tag) return null; const match = tag.match(/^(\d{2})-(\d{2})$/); @@ -245,8 +180,6 @@ export default function ResourcesPage() { return `A.Y. 20${match[1]} - 20${match[2]}`; } -======= ->>>>>>> origin/test function isCurrentAcademicYear(filename: string): boolean { return extractAcademicYearTag(filename) === CURRENT_ACADEMIC_YEAR_TAG; } @@ -302,10 +235,7 @@ export default function ResourcesPage() { restricted: isRestricted(file.name), displayName: file.name.replace(/\[restricted\]/gi, '').replace(/\[members\]/gi, '').trim(), courseCode: extractCourseCode(file.name), -<<<<<<< HEAD academicYearTag: extractAcademicYearTag(file.name), -======= ->>>>>>> origin/test fileType: getFileTypeCategory(file.name), quizNumber: extractQuizNumber(file.name), })); @@ -316,27 +246,16 @@ export default function ResourcesPage() { const grouped = new Map(); files.forEach(file => { -<<<<<<< HEAD const key = `${file.courseCode || 'Other'}::${file.academicYearTag || 'Unknown'}`; if (!grouped.has(key)) { grouped.set(key, { academicYearTerm: formatAcademicYearTag(file.academicYearTag) || extractAcademicYearTerm(file.name), -======= - const key = file.courseCode || 'Other'; - if (!grouped.has(key)) { - grouped.set(key, { - academicYearTerm: extractAcademicYearTerm(file.name), ->>>>>>> origin/test files: [] }); } // Update academic year/term if we find one and don't have it yet if (!grouped.get(key)!.academicYearTerm) { -<<<<<<< HEAD const ayTerm = formatAcademicYearTag(file.academicYearTag) || extractAcademicYearTerm(file.name); -======= - const ayTerm = extractAcademicYearTerm(file.name); ->>>>>>> origin/test if (ayTerm) { grouped.get(key)!.academicYearTerm = ayTerm; } @@ -416,7 +335,6 @@ export default function ResourcesPage() {
{Array.from(groupedData.entries()) .sort(([a], [b]) => a.localeCompare(b)) -<<<<<<< HEAD .map(([courseKey, courseData]) => { const [courseCode] = courseKey.split('::'); @@ -512,91 +430,6 @@ export default function ResourcesPage() {
); } ->>>>>>> Stashed changes -======= - .map(([courseCode, courseData]) => ( -
- {/* Course Header */} -
-

- {courseCode} -

- {courseData.academicYearTerm && ( -

- {courseData.academicYearTerm} -

- )} -
- - {/* Group by file type within course */} - {(() => { - const byType = new Map(); - courseData.files.forEach(file => { - const type = file.fileType || 'other'; - if (!byType.has(type)) byType.set(type, []); - byType.get(type)!.push(file); - }); - - return Array.from(byType.entries()).map(([fileType, typeFiles]) => ( -
- {/* File Type Label */} -

- {fileType === 'quiz' ? 'Quizzes' : - fileType === 'exam' ? 'Exams' : - fileType === 'notes' ? 'Notes' : - fileType === 'lab' ? 'Labs' : - fileType === 'project' ? 'Projects' : 'Resources'} -

- - {/* Buttons for each file */} -
- {typeFiles.map((file) => { - const isLocked = file.restricted && !session; - return ( -
- - - {/* File size tooltip - shows on hover */} - {file.size && ( -
-
- {getFileSize(file.size)} -
-
- )} -
- ); - })} -
-
- )); - })()} -
- ))} -
- ); - } - ->>>>>>> origin/test return (
diff --git a/src/app/globals.css b/src/app/globals.css index 55d1279..fe975ee 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,19 +1,9 @@ @import "tailwindcss"; :root { -<<<<<<< HEAD -<<<<<<< Updated upstream - --background: #ffffff; - --foreground: #171717; -======= --background: #ffffff; --foreground: #171717; color-scheme: light; ->>>>>>> Stashed changes -======= - --background: #ffffff; - --foreground: #171717; ->>>>>>> origin/test } @theme inline { @@ -24,25 +14,11 @@ } @media (prefers-color-scheme: dark) { -<<<<<<< HEAD -<<<<<<< Updated upstream - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -======= :root { --background: #0a0a0a; --foreground: #ededed; color-scheme: dark; } ->>>>>>> Stashed changes -======= - :root { - --background: #0a0a0a; - --foreground: #ededed; - } ->>>>>>> origin/test } /* Hero Button Styles */ @@ -966,27 +942,7 @@ body { /* File size tooltip - 3D glassmorphism */ .file-size-tooltip { -<<<<<<< HEAD -<<<<<<< Updated upstream - background: rgba(28, 28, 28, 0.95); - border-radius: 0.5rem; - -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(1.2); - backdrop-filter: blur(40px) saturate(180%) brightness(1.2); - border: 1.5px solid rgba(255, 255, 255, 0.3); - padding: 0.375rem 0.75rem; - color: #ffffff; - font-size: 0.75rem; - font-weight: 500; - white-space: nowrap; - - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.6), - 0 2px 8px rgba(0, 0, 0, 0.4), - inset 0 1px 0 rgba(255, 255, 255, 0.2), - inset 0 -1px 0 rgba(0, 0, 0, 0.3); -======= -======= ->>>>>>> origin/test + background: rgba(28, 28, 28, 0.95); border-radius: 0.5rem; -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(1.2); @@ -1007,7 +963,6 @@ body { /* Light mode overrides for resources glass UI */ @media (prefers-color-scheme: light) { -<<<<<<< HEAD body { background: radial-gradient(circle at top, rgba(94, 148, 50, 0.08), transparent 32%), @@ -1248,30 +1203,11 @@ body { .hero-button-secondary:hover { background: rgba(255, 255, 255, 0.88); -======= - .course-card { - background: rgba(255, 255, 255, 0.72); - border: 1.5px solid rgba(255, 255, 255, 0.85); - box-shadow: - 0 10px 30px rgba(15, 23, 42, 0.18), - 0 2px 6px rgba(15, 23, 42, 0.12), - inset 0 1px 0 rgba(255, 255, 255, 0.85), - inset 0 -1px 0 rgba(15, 23, 42, 0.08); - } - - .course-card:hover { - box-shadow: - 0 14px 34px rgba(15, 23, 42, 0.22), - 0 4px 12px rgba(15, 23, 42, 0.16), - inset 0 1px 0 rgba(255, 255, 255, 0.9), - inset 0 -1px 0 rgba(15, 23, 42, 0.1); ->>>>>>> origin/test } .course-card::before { background: linear-gradient( 140deg, -<<<<<<< HEAD rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.05) 50%, rgba(15, 23, 42, 0.08) 100% @@ -1286,11 +1222,6 @@ body { rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.04) 50%, rgba(15, 23, 42, 0.08) 100% -======= - rgba(255, 255, 255, 0.5) 0%, - rgba(241, 245, 249, 0.25) 50%, - rgba(148, 163, 184, 0.14) 100% ->>>>>>> origin/test ); } @@ -1304,7 +1235,6 @@ body { inset 0 1px 0 rgba(255, 255, 255, 0.85), inset 0 -1px 0 rgba(148, 163, 184, 0.18); } -<<<<<<< HEAD .glass-card-3d { background: linear-gradient( @@ -1368,9 +1298,6 @@ body { inset 0 1px 0 rgba(255, 255, 255, 0.28), inset 0 -1px 0 rgba(15, 23, 42, 0.08); } ->>>>>>> Stashed changes -======= ->>>>>>> origin/test } /* Login container and expandable box */ diff --git a/src/app/officers/link-shortener/LinkShortenerClient.tsx b/src/app/officers/link-shortener/LinkShortenerClient.tsx index d118b0f..26e8cfe 100644 --- a/src/app/officers/link-shortener/LinkShortenerClient.tsx +++ b/src/app/officers/link-shortener/LinkShortenerClient.tsx @@ -84,75 +84,42 @@ export default function LinkShortenerClient() {
setIsModalOpen(false)} aria-hidden />
-<<<<<<< HEAD

New Short Link

{ await handleCreate(e); setIsModalOpen(false); }} className="space-y-4">
-======= -

New Short Link

- -
- { await handleCreate(e); setIsModalOpen(false); }} className="space-y-4"> -
- ->>>>>>> origin/test setUrl(e.target.value)} type="url" placeholder="https://example.com/very-long-url" -<<<<<<< HEAD className="input-3d w-full placeholder:text-zinc-400 dark:placeholder:text-gray-400" />
-======= - className="input-3d w-full placeholder-gray-400" - /> -
-
- ->>>>>>> origin/test setAlias(e.target.value)} type="text" placeholder="my-link" -<<<<<<< HEAD className="input-3d w-full placeholder:text-zinc-400 dark:placeholder:text-gray-400" />
-======= - className="input-3d w-full placeholder-gray-400" - /> -
-
- ->>>>>>> origin/test setExpiresAt(e.target.value || null)} type="datetime-local" -<<<<<<< HEAD className="input-3d w-full placeholder:text-zinc-400 dark:placeholder:text-gray-400" -======= - className="input-3d w-full placeholder-gray-400" ->>>>>>> origin/test />
{error &&

{error}

} -<<<<<<< HEAD -======= - ->>>>>>> origin/test -======= - }} className="text-red-400 hover:text-red-300 text-sm">Delete ->>>>>>> origin/test
diff --git a/src/app/officers/link-shortener/page.tsx b/src/app/officers/link-shortener/page.tsx index 0f75988..98ad6a1 100644 --- a/src/app/officers/link-shortener/page.tsx +++ b/src/app/officers/link-shortener/page.tsx @@ -24,7 +24,6 @@ export default async function LinkShortenerPage() { return (
-<<<<<<< HEAD
@@ -41,18 +40,6 @@ export default async function LinkShortenerPage() {
-======= -
-

- Link Shortener -

-

- Create and manage shortened links for ACCESS resources and events. -

- - -
->>>>>>> origin/test
); } \ No newline at end of file diff --git a/src/components/ui/auth-warning/index.tsx b/src/components/ui/auth-warning/index.tsx index 08dee76..f07ea30 100644 --- a/src/components/ui/auth-warning/index.tsx +++ b/src/components/ui/auth-warning/index.tsx @@ -14,25 +14,15 @@ export function AuthWarning({
-<<<<<<< HEAD

-

>>>>>> origin/test style={{ fontFamily: "var(--font-manrope)" }} > {title}

>>>>>> origin/test style={{ fontFamily: "var(--font-manrope)" }} > {message} diff --git a/src/components/ui/button/index.tsx b/src/components/ui/button/index.tsx index 69b9558..51ecc1b 100644 --- a/src/components/ui/button/index.tsx +++ b/src/components/ui/button/index.tsx @@ -11,11 +11,7 @@ export function Button({ }: ButtonProps) { const variantClasses = { primary: "hero-button-primary text-white", -<<<<<<< HEAD secondary: "hero-button-secondary text-zinc-200 dark:text-gray-300", -======= - secondary: "hero-button-secondary text-gray-300", ->>>>>>> origin/test }; return ( diff --git a/src/components/ui/empty-state/index.tsx b/src/components/ui/empty-state/index.tsx index 2c20f54..f42a2d3 100644 --- a/src/components/ui/empty-state/index.tsx +++ b/src/components/ui/empty-state/index.tsx @@ -6,11 +6,7 @@ export function EmptyState({ message, className = "" }: EmptyStateProps) { return (

>>>>>> origin/test style={{ fontFamily: "var(--font-manrope)" }} > {message} diff --git a/src/components/ui/feature-card/index.tsx b/src/components/ui/feature-card/index.tsx index 254dbf2..52872c7 100644 --- a/src/components/ui/feature-card/index.tsx +++ b/src/components/ui/feature-card/index.tsx @@ -22,21 +22,13 @@ export function FeatureCard({ {title}

>>>>>> origin/test style={{ fontFamily: "var(--font-manrope)" }} > {description}

>>>>>> origin/test style={{ fontFamily: "var(--font-manrope)" }} > {linkText} diff --git a/src/components/ui/filter-button/index.tsx b/src/components/ui/filter-button/index.tsx index f44bf59..9f96879 100644 --- a/src/components/ui/filter-button/index.tsx +++ b/src/components/ui/filter-button/index.tsx @@ -12,11 +12,7 @@ export function FilterButton({ return (