diff --git a/.gitignore b/.gitignore index fd59b86..baa95b0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ logs .DS_Store .fleet .idea +prisma/*.json +prisma/defaultCredentials.js +prisma/generated # Local env files .env diff --git a/app/app.vue b/app/app.vue index db11dda..a6e0b07 100644 --- a/app/app.vue +++ b/app/app.vue @@ -38,7 +38,9 @@
- + + +
diff --git a/app/components/AppHeader.vue b/app/components/AppHeader.vue new file mode 100644 index 0000000..e69de29 diff --git a/app/components/AppNav.vue b/app/components/AppNav.vue new file mode 100644 index 0000000..e69de29 diff --git a/app/components/StatsBar.vue b/app/components/StatsBar.vue new file mode 100644 index 0000000..e69de29 diff --git a/app/composables/useAdmin.ts b/app/composables/useAdmin.ts new file mode 100644 index 0000000..88ed1e6 --- /dev/null +++ b/app/composables/useAdmin.ts @@ -0,0 +1,260 @@ +// composables/useAdmin.ts +// Place this at: app/composables/useAdmin.ts + +export const useAdmin = () => { + // ── Builder state ── + const builderSubTab = useState<'history' | 'creation'>('builderSubTab', () => 'history') + const formTitle = useState('formTitle', () => '') + const editingFormId = useState('editingFormId', () => null) + const questions = useState('questions', () => []) + + // Week/day pickers — default to current Monday + const todayDate = new Date() + const dayOff = (todayDate.getDay() + 6) % 7 + const mon = new Date(todayDate) + mon.setDate(todayDate.getDate() - dayOff) + const monStr = mon.toISOString().split('T')[0] + + const formWeekStart = useState('formWeekStart', () => monStr) + const formDays = useState('formDays', () => ['Monday']) + const historyWeekStart = useState('historyWeekStart', () => '') + const selectedFormDetails = useState('selectedFormDetails', () => null) + + // ── Helpers ── + const getCalculatedDate = (weekStartStr: string, dayName: string): string => { + if (!weekStartStr) return '' + const days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] + const idx = days.indexOf(dayName) + if (idx === -1) return '' + const d = new Date(`${weekStartStr}T00:00:00Z`) + d.setDate(d.getDate() + idx) + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + + } + const getLastMonday = (dateStr: string) => { + const d = new Date(`${dateStr}T00:00:00Z`) + const daysSinceMonday = (d.getDay() + 6) % 7 + d.setDate(d.getDate() - daysSinceMonday) + return d.toISOString().slice(0, 10) + } + const formatDate = (dateStr: string): string => { + if (!dateStr) return '' + return new Date(`${dateStr}T00:00:00Z`).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) + } + + const defaultQuestions = (): any[] => [ + { id: Date.now(), type: 'video', text: '', textEs: '', reference: '', referenceEs: '', url: '' }, + { id: Date.now() + 1, type: 'text', text: '', textEs: '', reference: '', referenceEs: '', url: '' }, + { id: Date.now() + 2, type: 'mcq', text: '', textEs: '', reference: '', referenceEs: '', url: '', + choices: [ + { text: '', correct: true }, + { text: '', correct: false }, + { text: '', correct: false }, + { text: '', correct: false }, + ], + }, + ] + + // ── Published forms ── + const publishedForms = useState('publishedForms', () => [ + { + id: 1, weekStart: '2026-02-09', day: 'Monday', title: 'Kindness & Compassion', + date: 'Feb 9, 2026', status: 'Active', + questions: [ + { id: 101, type: 'context', text: 'Read the story of the Bell of Atri.', textEs: 'Lee la historia de la Campana de Atri.', reference: 'A story about justice and kindness to animals.', referenceEs: 'Una historia sobre la justicia y la bondad.' }, + { id: 102, type: 'video', text: 'Watch this video on empathy', textEs: 'Mira este video sobre la empatía', url: 'https://www.youtube.com/embed/1Evwgu369Jw', reference: '', referenceEs: '' }, + { id: 103, type: 'text', text: 'What did the horse do in the story?', textEs: '¿Qué hizo el caballo en la historia?', reference: 'He rang the bell to ask for justice.', referenceEs: 'Tocó la campana para pedir justicia.' }, + { id: 104, type: 'mcq', text: 'If there are 5 bells and 3 horses, how many?', textEs: 'Si hay 5 campanas y 3 caballos, ¿cuántos en total?', reference: '8', referenceEs: '8', + choices: [{ text:'6',correct:false },{ text:'8',correct:true },{ text:'10',correct:false },{ text:'15',correct:false }] }, + ], + }, + { id: 2, weekStart: '2026-02-09', day: 'Tuesday', title: 'Honesty & Truth', date: 'Feb 10, 2026', status: 'Active', questions: [{ id: 201, type: 'text', text: 'Why tell the truth?', textEs: '¿Por qué decir la verdad?', reference: 'Truth builds trust.', referenceEs: 'La verdad genera confianza.', url: '' }] }, + { id: 3, weekStart: '2026-02-09', day: 'Wednesday', title: 'Courage & Bravery', date: 'Feb 11, 2026', status: 'Active', questions: [{ id: 301, type: 'text', text: 'Who is a brave person?', textEs: '¿Quién es valiente?', reference: 'Someone who faces fear.', referenceEs: 'Alguien que enfrenta el miedo.', url: '' }] }, + { id: 4, weekStart: '2026-02-16', day: 'Monday', title: 'Patience & Persistence', date: 'Feb 16, 2026', status: 'Active', questions: [{ id: 401, type: 'text', text: 'What is patience?', textEs: '¿Qué es la paciencia?', reference: 'Waiting without getting upset.', referenceEs: 'Esperar sin molestarse.', url: '' }] }, + { id: 5, weekStart: '2026-02-16', day: 'Friday', title: 'Weekly Review', date: 'Feb 20, 2026', status: 'Unpublished', questions: [{ id: 501, type: 'text', text: 'What did you learn?', textEs: '¿Qué aprendiste?', reference: 'Review of the week.', referenceEs: 'Resumen de la semana.', url: '' }] }, + { id: 6, weekStart: '2026-03-02', day: 'Monday', title: 'Empathy in Action', date: 'Mar 2, 2026', status: 'Active', questions: [{ id: 601, type: 'text', text: 'Help a friend.', textEs: 'Ayuda a un amigo.', reference: 'Show kindness.', referenceEs: 'Muestra bondad.', url: '' }] }, + ]) + + const filteredPublishedForms = computed(() => + publishedForms.value.filter(f => !historyWeekStart.value || f.weekStart === historyWeekStart.value) + ) + + // ── Drag-and-drop for question reorder ── + const draggedIdx = useState('draggedIdx', () => null) + + const dragStart = (e: DragEvent, index: number) => { + draggedIdx.value = index + if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move' + } + + const onDrop = (_e: DragEvent, index: number) => { + if (draggedIdx.value === null) return + const dragged = questions.value[draggedIdx.value] + questions.value.splice(draggedIdx.value, 1) + questions.value.splice(index, 0, dragged) + draggedIdx.value = null + } + + // ── CRUD ── + const addQuestion = (type: string) => { + const q: any = { id: Date.now(), type, text: '', textEs: '', reference: '', referenceEs: '', url: '' } + if (type === 'mcq') { + q.choices = [ + { text: '', correct: true }, + { text: '', correct: false }, + { text: '', correct: false }, + { text: '', correct: false }, + ] + } + questions.value.push(q) + } + + const publishForm = () => { + if (!formTitle.value) { alert('Please enter a title!'); return } + if (!formDays.value.length) { alert('Please select at least one day!'); return } + + formDays.value.forEach(day => { + const calcDate = getCalculatedDate(getLastMonday(formWeekStart.value || ''), day) + const existingIdx = publishedForms.value.findIndex( + f => f.weekStart === formWeekStart.value && f.day === day + ) + const newForm = { + id: existingIdx !== -1 ? publishedForms.value[existingIdx].id : Date.now() + Math.random(), + weekStart: formWeekStart.value, + day, + title: formTitle.value, + date: calcDate, + status: 'Active', + questions: JSON.parse(JSON.stringify(questions.value)), + } + if (existingIdx !== -1) publishedForms.value[existingIdx] = newForm + else publishedForms.value.unshift(newForm) + }) + + alert(`Published for: ${formDays.value.join(', ')}`) + } + + const editPublishedForm = (form: any) => { + formTitle.value = form.title + formWeekStart.value = form.weekStart || formWeekStart.value + formDays.value = [form.day || 'Monday'] + questions.value = JSON.parse(JSON.stringify(form.questions)) + editingFormId.value = form.id + builderSubTab.value = 'creation' + navigateTo('/admin/builder') + } + + const toggleFormPublish = (form: any) => { + form.status = form.status === 'Active' ? 'Unpublished' : 'Active' + } + + const viewFormDetails = (form: any) => { + selectedFormDetails.value = form + } + + // ── Students / Progress ── + const students = useState('adminStudents', () => [ + { id: 1, name: 'Aiden Smith', initials: 'AS', email: 'aiden@school.edu', tickets: 12, streak: 4, lastActive: '2 hours ago' }, + { id: 2, name: 'Nevin Kumar', initials: 'NK', email: 'nevin@school.edu', tickets: 14, streak: 5, lastActive: 'Just now' }, + { id: 3, name: 'Swarna Jay', initials: 'SJ', email: 'swarna@school.edu',tickets: 8, streak: 2, lastActive: 'Yesterday' }, + ]) + + const searchStudent = useState('searchStudent', () => '') + const sortStudent = useState('sortStudent', () => 'tickets') + + const filteredAndSortedStudents = computed(() => { + let res = students.value + if (searchStudent.value) { + const q = searchStudent.value.toLowerCase() + res = res.filter(s => s.name.toLowerCase().includes(q)) + } + return [...res].sort((a, b) => { + if (sortStudent.value === 'tickets') return b.tickets - a.tickets + if (sortStudent.value === 'streak') return b.streak - a.streak + if (sortStudent.value === 'name') return a.name.localeCompare(b.name) + return 0 + }) + }) + + // ── Raffle ── + const raffleWinner = useState('raffleWinner', () => null) + const isSpinning = useState('isSpinning', () => false) + const raffleWeek = useState('raffleWeek', () => 'Feb 9 – Feb 15, 2026') + const raffleSubmissions = useState('raffleSubmissions', () => 42) + + const spinRaffle = () => { + isSpinning.value = true + raffleWinner.value = null + setTimeout(() => { + isSpinning.value = false + raffleWinner.value = students.value[Math.floor(Math.random() * students.value.length)] + }, 2000) + } + + // ── Announcements ── + const announcementSubTab = useState<'creation' | 'history'>('announcementSubTab', () => 'creation') + + const announcements = useState('announcements', () => [ + { id: 1, title: 'Summer Reading Challenge!', content: 'Log 20 books this month to win a Super Sage badge!', icon: '🌟', startDate: '2026-03-01', endDate: '2026-03-31', weekStart: '2026-03-02', day: 'Monday' }, + { id: 2, title: 'New Badges Available', content: 'Check the shop for new limited edition themes.', icon: '🎉', startDate: '2026-03-05', endDate: '', weekStart: '2026-03-02', day: 'Thursday' }, + { id: 3, title: 'Friday Game Night', content: 'Join us in the library for board games and snacks!', icon: '🎲', startDate: '2026-03-06', endDate: '', weekStart: '2026-03-02', day: 'Friday' }, + { id: 4, title: 'Week 10 Progress', content: 'You are doing amazing! Keep up the streak.', icon: '📈', startDate: '2026-03-09', endDate: '', weekStart: '2026-03-09', day: 'Monday' }, + { id: 5, title: 'Author Visit', content: 'Virtual session this Wednesday at 10 AM.', icon: '✍️', startDate: '2026-03-11', endDate: '', weekStart: '2026-03-09', day: 'Wednesday' }, + ]) + + const newAnnouncement = useState('newAnnouncement', () => ({ + title: '', content: '', icon: '🌟', + startDate: new Date().toISOString().split('T')[0], + endDate: '', weekStart: monStr, day: 'Monday', + })) + + const announcementFilterWeek = useState('announcementFilterWeek', () => monStr) + + const filteredAnnouncements = computed(() => + announcements.value.filter(a => + !announcementFilterWeek.value || a.weekStart === announcementFilterWeek.value + ) + ) + + const isAnnouncementActive = (ann: any): boolean => { + const now = new Date().toISOString().split('T')[0] + if(!now) return false // in case of invalid date + if (ann.startDate > now) return false + if (ann.endDate && ann.endDate < now) return false + return true + } + + const addAnnouncement = () => { + const na = newAnnouncement.value + if (!na.title || !na.content) { alert('Please fill in title and message!'); return } + announcements.value.push({ id: Date.now(), ...JSON.parse(JSON.stringify(na)) }) + newAnnouncement.value = { + title: '', content: '', icon: '🌟', + startDate: new Date().toISOString().split('T')[0], + endDate: '', weekStart: monStr, day: 'Monday', + } + } + + const deleteAnnouncement = (id: number) => { + announcements.value = announcements.value.filter((a: any) => a.id !== id) + } + + return { + // builder + builderSubTab, formTitle, editingFormId, questions, + formWeekStart, formDays, historyWeekStart, getLastMonday, + getCalculatedDate, formatDate, defaultQuestions, + publishedForms, filteredPublishedForms, + selectedFormDetails, viewFormDetails, + draggedIdx, dragStart, onDrop, + addQuestion, publishForm, editPublishedForm, toggleFormPublish, + // progress + students, searchStudent, sortStudent, filteredAndSortedStudents, + // raffle + raffleWinner, isSpinning, raffleWeek, raffleSubmissions, spinRaffle, + // announcements + announcementSubTab, announcements, newAnnouncement, + announcementFilterWeek, filteredAnnouncements, + isAnnouncementActive, addAnnouncement, deleteAnnouncement, + } +} diff --git a/app/layouts/admin.css b/app/layouts/admin.css new file mode 100644 index 0000000..33ec929 --- /dev/null +++ b/app/layouts/admin.css @@ -0,0 +1,235 @@ +html, body { margin: 0; padding: 0; height: 100%; } +#__nuxt { height: 100%; } + +/* ── Shell ── */ +.rh-admin-wrap { + display: flex; + height: 100vh; + overflow: hidden; + background: #f8fafc; + font-family: 'Quicksand', system-ui, sans-serif; + color: #1e293b; +} + +/* ── Sidebar ── */ +.rh-sidebar { + width: 18rem; + flex-shrink: 0; + background: #ffffff; + border-right: 1px solid #e2e8f0; + display: flex; + flex-direction: column; + overflow-y: auto; +} +.rh-sidebar-inner { padding: 24px 24px 0; flex: 1; } + +.rh-logo { display:flex; align-items:center; gap:12px; margin-bottom:32px; } +.rh-logo-icon { width:40px; height:40px; background:#4f46e5; border-radius:8px; display:flex; align-items:center; justify-content:center; color:white; font-weight:700; font-size:1.25rem; } +.rh-logo-name { font-weight:700; font-size:1.1rem; color:#0f172a; line-height:1; } +.rh-logo-accent { color:#4f46e5; } +.rh-logo-sub { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:#2dd4bf; margin-top:2px; } + +.rh-nav-label { font-size:12px; font-weight:500; color:#94a3b8; text-transform:uppercase; letter-spacing:.1em; margin-bottom:16px; } +.rh-nav { display:flex; flex-direction:column; gap:4px; } + +.rh-nav-btn { + display:flex; align-items:center; width:100%; + padding:12px 16px; font-weight:500; font-size:15px; + border-radius:0 12px 12px 0; border-left:4px solid transparent; + color:#64748b; cursor:pointer; transition:all .15s; + background:transparent; text-align:left; + border-top:none; border-right:none; border-bottom:none; +} +.rh-nav-btn:hover { background:#f8fafc; } +.rh-nav-active { background:#eef2ff !important; color:#4338ca !important; border-left-color:#4f46e5 !important; font-weight:600 !important; } + +.rh-sidebar-footer { padding:24px; } +.rh-back-link { font-size:14px; color:#94a3b8; font-weight:500; text-decoration:none; } +.rh-back-link:hover { color:#64748b; } + +/* ── Main ── */ +.rh-main { flex:1; overflow-y:auto; padding:24px 32px; } +.rh-tab-content { max-width:56rem; margin:0 auto; } + +/* ── Spacing helpers ── */ +.rh-space-y-3 > * + * { margin-top:12px; } +.rh-space-y-4 > * + * { margin-top:16px; } +.rh-space-y-5 > * + * { margin-top:20px; } +.rh-space-y-6 > * + * { margin-top:24px; } + +/* ── Cards ── */ +.rh-card { + background:white; padding:20px; border-radius:8px; + border:1px solid #e2e8f0; box-shadow:0 1px 3px rgba(0,0,0,.06); + display:flex; flex-direction:column; gap:16px; +} + +/* ── Buttons ── */ +.rh-btn-ghost { padding:12px 24px; border-radius:8px; font-weight:500; background:white; border:2px solid #e5e7eb; color:#6b7280; cursor:pointer; transition:background .15s; } +.rh-btn-ghost:hover { background:#f9fafb; } +.rh-btn-indigo { padding:12px 24px; border-radius:8px; font-weight:500; background:#4f46e5; color:white; border:none; cursor:pointer; transition:background .15s; box-shadow:0 4px 14px rgba(99,102,241,.3); } +.rh-btn-indigo:hover { background:#4338ca; } +.rh-btn-indigo:active { transform:scale(.97); } +.rh-btn-dark { padding:16px 32px; border-radius:8px; font-weight:700; font-size:1.1rem; background:#111827; color:white; border:none; cursor:pointer; } +.rh-btn-dark:hover { background:black; } + +.rh-btn-sm { padding:8px 16px; border-radius:8px; font-size:14px; font-weight:500; border:none; cursor:pointer; } +.rh-btn-sm-indigo { padding:8px 16px; border-radius:8px; font-size:14px; font-weight:500; border:none; cursor:pointer; background:#eef2ff; color:#4f46e5; } +.rh-btn-sm-indigo:hover { background:#e0e7ff; } +.rh-btn-sm-danger { background:white; color:#6b7280; } +.rh-btn-sm-danger:hover { background:#fef2f2; color:#ef4444; } +.rh-btn-sm-success { background:#f0fdf4; color:#16a34a; } +.rh-btn-sm-success:hover { background:#dcfce7; } + +.rh-flex-gap { display:flex; gap:8px; } + +/* ── Creation bar ── */ +.rh-creation-bar { display:flex; justify-content:space-between; align-items:center; background:#eef2ff; padding:24px; border-radius:8px; border:1px solid #e0e7ff; font-style:italic; } +.rh-creation-title { font-size:1.25rem; font-weight:500; color:#3730a3; } + +/* ── Week row ── */ +.rh-week-row { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:16px; background:rgba(238,242,255,.5); padding:16px; border-radius:8px; border:1px solid rgba(224,231,255,.5); } +.rh-week-title { font-size:1.1rem; font-weight:500; color:#1e1b4b; } +.rh-tiny-label { font-size:10px; font-weight:500; color:#94a3b8; text-transform:uppercase; letter-spacing:.05em; } + +/* ── Inputs ── */ +.rh-input { font-size:16px; font-weight:500; color:#1f2937; border:2px solid #f1f5f9; border-radius:8px; padding:8px 16px; outline:none; transition:border-color .15s; background:white; width:100%; box-sizing:border-box; } +.rh-input:focus { border-color:#6366f1; } +.rh-title-input { font-size:1.5rem; font-weight:500; color:#1f2937; border:none; border-bottom:2px solid #f1f5f9; outline:none; transition:border-color .15s; padding:8px 0; background:transparent; width:100%; } +.rh-title-input:focus { border-bottom-color:#6366f1; } +.rh-field-input { font-weight:500; color:#1f2937; border:2px solid #f8fafc; background:#f8fafc; border-radius:8px; padding:12px 16px; outline:none; transition:all .15s; font-size:16px; width:100%; box-sizing:border-box; } +.rh-field-input:focus { background:white; border-color:#c7d2fe; } + +/* ── Day pills ── */ +.rh-day-pills { display:flex; flex-wrap:wrap; gap:8px; } +.rh-day-pill { padding:8px 16px; border-radius:8px; font-weight:500; font-size:14px; border:2px solid transparent; background:#f8fafc; color:#94a3b8; cursor:pointer; transition:all .15s; } +.rh-day-pill:hover:not(.rh-day-active) { background:#f1f5f9; } +.rh-day-active { background:#e0e7ff !important; color:#4338ca !important; border-color:#a5b4fc !important; box-shadow:inset 0 1px 3px rgba(0,0,0,.1); } + +/* ── Title row ── */ +.rh-title-row { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:24px; padding-top:16px; border-top:1px solid #f8fafc; } +.rh-preview-date { background:#eef2ff; padding:16px 24px; border-radius:8px; text-align:center; min-width:200px; } + +/* ── Q type buttons ── */ +.rh-q-btns { display:flex; gap:16px; flex-wrap:wrap; } +.rh-q-btn { flex:1; font-weight:500; padding:16px; border-radius:8px; cursor:pointer; min-width:140px; transition:background .15s; } +.rh-q-btn-blue { background:#eff6ff; color:#4f46e5; border:1px dashed #bfdbfe; } +.rh-q-btn-blue:hover { background:#dbeafe; } +.rh-q-btn-gray { background:white; color:#6b7280; border:1px dashed #e5e7eb; } +.rh-q-btn-gray:hover { background:#f9fafb; } +.rh-q-btn-gray2 { background:#f9fafb; color:#4b5563; border:1px dashed #e5e7eb; } +.rh-q-btn-gray2:hover { background:#f3f4f6; } + +/* ── Question card ── */ +.rh-empty { text-align:center; padding:48px 0; color:#9ca3af; border:4px dashed #f3f4f6; border-radius:8px; background:white; } +.rh-q-card { background:white; border:2px solid #f3f4f6; border-radius:8px; padding:24px; position:relative; cursor:move; box-shadow:0 1px 3px rgba(0,0,0,.05); transition:border-color .15s; } +.rh-q-card:hover { border-color:#e0e7ff; } +.rh-q-drag-handle { position:absolute; top:16px; left:16px; color:#d1d5db; } +.rh-q-card:hover .rh-q-drag-handle { color:#818cf8; } +.rh-q-remove { position:absolute; top:16px; right:16px; background:none; border:none; color:#d1d5db; cursor:pointer; font-size:1.1rem; } +.rh-q-remove:hover { color:#ef4444; } + +.rh-q-badge { font-size:12px; font-weight:500; text-transform:uppercase; letter-spacing:.05em; padding:4px 12px; border-radius:9999px; } +.rh-badge-blue { background:#dbeafe; color:#4338ca; } +.rh-badge-green { background:#d1fae5; color:#065f46; } +.rh-badge-red { background:#fee2e2; color:#991b1b; } +.rh-badge-gray { background:#f3f4f6; color:#374151; } + +.rh-grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:16px; } +@media (max-width:768px) { .rh-grid-2 { grid-template-columns:1fr; } } + +/* ── MCQ ── */ +.rh-choice-row { display:flex; align-items:center; gap:12px; } +.rh-choice-letter { width:32px; height:32px; border-radius:9999px; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:14px; flex-shrink:0; background:#f3f4f6; color:#6b7280; } +.rh-choice-correct { background:#10b981; color:white; } +.rh-set-correct { padding:8px 12px; border-radius:8px; font-weight:500; font-size:12px; border:none; cursor:pointer; background:#f3f4f6; color:#9ca3af; } +.rh-set-correct:hover { background:#d1fae5; color:#059669; } +.rh-set-correct-active { background:#10b981; color:white; } +.rh-add-choice { width:100%; padding:12px; border:2px dashed #a7f3d0; color:#10b981; font-weight:500; border-radius:8px; background:transparent; cursor:pointer; } +.rh-add-choice:hover { background:#ecfdf5; } + +/* ── History form rows ── */ +.rh-form-row { background:white; padding:20px 24px; border-radius:24px; border:1px solid #e2e8f0; display:flex; align-items:center; gap:20px; transition:all .2s; } +.rh-form-row:hover { border-color:#a5b4fc; box-shadow:0 2px 8px rgba(99,102,241,.08); } +.rh-unpublished { opacity:.6; } +.rh-week-badge { background:#eef2ff; color:#4338ca; padding:12px; border-radius:8px; display:flex; flex-direction:column; align-items:center; min-width:90px; border:1px solid #e0e7ff; flex-shrink:0; font-size:11px; font-weight:500; } +.rh-day-badge { font-size:11px; font-weight:500; text-transform:uppercase; background:#4f46e5; color:white; padding:2px 8px; border-radius:6px; margin-top:4px; } +.rh-form-actions { display:flex; gap:8px; align-items:center; flex-shrink:0; } +.rh-status-pill { font-size:10px; font-weight:500; text-transform:uppercase; letter-spacing:.1em; padding:6px 12px; border-radius:9999px; } +.rh-active { background:#dcfce7; color:#15803d; } +.rh-inactive { background:#f3f4f6; color:#6b7280; } + +/* ── Progress table ── */ +.rh-th { padding:16px; font-size:12px; font-weight:500; color:#9ca3af; text-transform:uppercase; letter-spacing:.05em; } +.rh-tr { border-bottom:1px solid #f3f4f6; transition:background .15s; } +.rh-tr:hover { background:rgba(238,242,255,.5); } +.rh-tr:last-child { border-bottom:none; } +.rh-td { padding:16px; } +.rh-avatar { width:32px; height:32px; border-radius:9999px; background:#e0e7ff; color:#4338ca; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; box-shadow:0 1px 3px rgba(0,0,0,.1); flex-shrink:0; } +.rh-badge-pill { display:inline-flex; align-items:center; gap:8px; padding:4px 12px; border-radius:8px; font-weight:500; background:#f3f4f6; color:#374151; } + +/* ── Raffle ── */ +.rh-raffle-card { background:white; border-radius:8px; box-shadow:0 25px 50px rgba(0,0,0,.12); padding:48px; max-width:36rem; margin:0 auto; border-bottom:8px solid #818cf8; } +.rh-spin-btn { background:#4f46e5; color:white; font-size:1.5rem; font-weight:500; padding:20px 64px; border-radius:9999px; border:none; cursor:pointer; box-shadow:0 10px 25px rgba(99,102,241,.3); transition:all .2s; } +.rh-spin-btn:hover { background:#4338ca; } +.rh-spin-btn:active { transform:scale(.95); } +@keyframes rhSpin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } } +.rh-spin-emoji { display:inline-block; font-size:6rem; animation:rhSpin .8s linear infinite; margin-bottom:24px; } +@keyframes rhBounce { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-16px); } } +.rh-bounce-emoji { display:inline-block; font-size:6rem; animation:rhBounce 1s ease-in-out infinite; margin-bottom:24px; } + +/* ── Announcements ── */ +.rh-subtab-switch { display:flex; background:#eef2ff; padding:6px; border-radius:8px; border:1px solid #e0e7ff; } +.rh-subtab-btn { padding:10px 24px; border-radius:6px; font-weight:500; border:none; cursor:pointer; transition:all .3s; background:transparent; color:#818cf8; } +.rh-subtab-btn:hover { color:#4f46e5; } +.rh-subtab-active { background:white !important; color:#3730a3 !important; box-shadow:0 2px 8px rgba(0,0,0,.1) !important; } + +.rh-ann-grid { display:grid; grid-template-columns:1fr 1fr; gap:24px; } +@media (max-width:1024px) { .rh-ann-grid { grid-template-columns:1fr; } } + +.rh-btn-post { width:100%; padding:16px; background:#4f46e5; color:white; border:none; border-radius:8px; font-weight:500; font-size:1.2rem; cursor:pointer; box-shadow:0 10px 25px rgba(99,102,241,.25); } +.rh-btn-post:hover { background:#4338ca; } + +.rh-preview-panel { background:rgba(238,242,255,.3); padding:32px; border-radius:8px; border:2px dashed #e0e7ff; display:flex; flex-direction:column; justify-content:center; } +.rh-preview-card { background:white; padding:24px; border-radius:40px; border:1px solid #f3f4f6; box-shadow:0 10px 25px rgba(0,0,0,.08); display:flex; gap:24px; align-items:flex-start; } +.rh-preview-icon { padding:16px; background:#eef2ff; border-radius:16px; font-size:2.5rem; flex-shrink:0; border:1px solid #e0e7ff; display:flex; align-items:center; justify-content:center; min-width:80px; min-height:80px; } + +.rh-ann-item { padding:20px; border-radius:12px; border:2px solid; display:flex; gap:20px; align-items:flex-start; transition:all .2s; } +.rh-ann-active { background:white; border-color:#a5b4fc; } +.rh-ann-active:hover { border-color:#6366f1; } +.rh-ann-expired { background:#f9fafb; border-color:#f3f4f6; opacity:.7; } +.rh-ann-expired:hover { opacity:1; } +.rh-ann-icon { padding:16px; border-radius:12px; font-size:1.875rem; flex-shrink:0; border:1px solid; display:flex; align-items:center; justify-content:center; min-width:80px; min-height:80px; } +.rh-ann-icon-active { background:#eef2ff; border-color:#e0e7ff; } +.rh-ann-icon-expired { background:#f3f4f6; border-color:#e5e7eb; } +.rh-ann-pill { padding:2px 8px; border-radius:9999px; font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; } +.rh-ann-pill-green { background:#dcfce7; color:#15803d; border:1px solid #bbf7d0; } +.rh-ann-pill-gray { background:#e5e7eb; color:#6b7280; border:1px solid #d1d5db; } +.rh-ann-pill-neutral { background:#f3f4f6; color:#4b5563; } + +/* ── Modal ── */ +.rh-modal-backdrop { position:fixed; inset:0; z-index:300; display:flex; align-items:center; justify-content:center; padding:24px; } +.rh-modal-overlay { position:absolute; inset:0; background:rgba(15,23,42,.4); backdrop-filter:blur(4px); } +.rh-modal-box { position:relative; background:white; width:100%; max-width:42rem; border-radius:8px; box-shadow:0 25px 50px rgba(0,0,0,.25); display:flex; flex-direction:column; max-height:90vh; border-bottom:8px solid #4f46e5; overflow:hidden; } +.rh-modal-head { padding:24px; background:#eef2ff; border-bottom:1px solid #e0e7ff; display:flex; justify-content:space-between; align-items:center; } +.rh-modal-title { font-size:1.5rem; font-weight:900; color:#111827; } +.rh-modal-meta { font-size:12px; color:#9ca3af; font-weight:700; text-transform:uppercase; letter-spacing:.1em; margin-top:4px; } +.rh-modal-close { width:48px; height:48px; border-radius:8px; background:white; color:#9ca3af; display:flex; align-items:center; justify-content:center; border:none; cursor:pointer; } +.rh-modal-close:hover { color:#111827; transform:scale(1.1); } +.rh-modal-body { padding:24px; overflow-y:auto; display:flex; flex-direction:column; gap:24px; } +.rh-modal-foot { padding:24px; background:#f9fafb; border-top:1px solid #f3f4f6; display:flex; justify-content:flex-end; } +.rh-modal-step { padding:24px; border-radius:8px; border:2px solid #f9fafb; background:rgba(249,250,251,.5); display:flex; flex-direction:column; gap:12px; } +.rh-step-badge { display:inline-block; background:white; padding:4px 12px; border-radius:6px; font-size:10px; font-weight:900; text-transform:uppercase; letter-spacing:.1em; color:#6366f1; box-shadow:0 1px 3px rgba(0,0,0,.1); width:fit-content; } +.rh-step-text { font-size:1.1rem; font-weight:700; color:#111827; line-height:1.4; } +.rh-step-es { font-size:1rem; color:#6b7280; font-style:italic; font-weight:500; } +.rh-step-ref { padding:16px; background:white; border-radius:6px; border:1px solid #f3f4f6; } +.rh-step-ref-label { font-size:10px; font-weight:900; color:#d1d5db; text-transform:uppercase; letter-spacing:.1em; margin-bottom:4px; } +.rh-step-ref-val { font-size:14px; font-weight:700; color:#6366f1; } +.rh-step-choices { display:grid; grid-template-columns:1fr 1fr; gap:8px; } +.rh-step-choice { padding:12px; border-radius:8px; border:2px solid #f3f4f6; font-size:14px; font-weight:700; background:white; color:#9ca3af; opacity:.6; } +.rh-correct { background:#f0fdf4; border-color:#bbf7d0; color:#15803d; opacity:1; } +.rh-step-url code { font-size:12px; background:#f3f4f6; padding:4px 8px; border-radius:4px; color:#6b7280; word-break:break-all; } + +/* ── Transitions ── */ +.rh-fade-enter-active, .rh-fade-leave-active { transition:opacity .25s ease; } +.rh-fade-enter-from, .rh-fade-leave-to { opacity:0; } \ No newline at end of file diff --git a/app/layouts/admin.vue b/app/layouts/admin.vue new file mode 100644 index 0000000..24b0275 --- /dev/null +++ b/app/layouts/admin.vue @@ -0,0 +1,41 @@ + + + + + + \ No newline at end of file diff --git a/app/middleware/auth.global.ts b/app/middleware/auth.global.ts index ea8702f..159ecd1 100644 --- a/app/middleware/auth.global.ts +++ b/app/middleware/auth.global.ts @@ -1,15 +1,19 @@ -import { authClient } from '../utils/auth-client' +// import { authClient } from '../utils/auth-client' -export default defineNuxtRouteMiddleware(async (to) => { - const { data: session } = await authClient.useSession(useFetch) - - if (session.value) { - if (to.path === '/auth') { - return navigateTo('/') - } - } else { - if (to.path !== '/auth') { - return navigateTo('/auth') - } - } +export default defineNuxtRouteMiddleware(() => { + // auth disabled for now }) + +// export default defineNuxtRouteMiddleware(async (to) => { +// const { data: session } = await authClient.useSession(useFetch) + +// if (session.value) { +// if (to.path === '/auth') { +// return navigateTo('/') +// } +// } else { +// if (to.path !== '/auth') { +// return navigateTo('/') +// } +// } +// }) diff --git a/app/pages/admin/announcement.vue b/app/pages/admin/announcement.vue new file mode 100644 index 0000000..e69de29 diff --git a/app/pages/admin/announcements.vue b/app/pages/admin/announcements.vue new file mode 100644 index 0000000..96efcf0 --- /dev/null +++ b/app/pages/admin/announcements.vue @@ -0,0 +1,320 @@ + + +