From 11fc4b7a8d3ba7b780ffe94df6d272af213d4868 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 9 Jun 2026 12:06:45 -0500 Subject: [PATCH 1/2] C13/14 --- app/composables/useAdmin.ts | 117 ++++++++++++++++++++++++------------ app/pages/reader/forms.vue | 14 ++++- server/api/form/index.ts | 2 +- 3 files changed, 91 insertions(+), 42 deletions(-) diff --git a/app/composables/useAdmin.ts b/app/composables/useAdmin.ts index 685b6bbd..ece87003 100644 --- a/app/composables/useAdmin.ts +++ b/app/composables/useAdmin.ts @@ -8,12 +8,12 @@ export const useAdmin = () => { const callFormApi = async (method: 'GET' | 'POST' | 'PUT' | 'DELETE', params: Record = {}, body?: Record): Promise => { const queryString = method === 'GET' || method === 'DELETE' ? `?${new URLSearchParams(Object.entries(params).reduce((acc, [key, value]) => { - if (value !== undefined && value !== null) { - acc[key] = String(value) - } + if (value !== undefined && value !== null) { + acc[key] = String(value) + } - return acc - }, {} as Record)).toString()}` + return acc + }, {} as Record)).toString()}` : '' return await $fetch(`/api/form${queryString}`, { @@ -48,6 +48,13 @@ export const useAdmin = () => { return formatYmdLocal(parsed) } + //if the value is an ISO string, extract just the "YYYY-MM-DD" portion to avoid timezone shift + const isoMatch = value.match(/^(\d{4}-\d{2}-\d{2})/) + if (isoMatch && isoMatch[1]) { + const datePart = parseLocalDate(isoMatch[1]) + if (datePart) return formatYmdLocal(datePart) + } + const fallback = new Date(value) if (Number.isNaN(fallback.getTime())) { return '' @@ -104,10 +111,10 @@ 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', () => []) + 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() @@ -116,8 +123,8 @@ export const useAdmin = () => { mon.setDate(todayDate.getDate() - dayOff) const monStr = formatYmdLocal(mon) - const formWeekStart = useState('formWeekStart', () => monStr) - const formDays = useState('formDays', () => ['Monday']) + const formWeekStart = useState('formWeekStart', () => monStr) + const formDays = useState('formDays', () => ['Monday']) const historyWeekStart = useState('historyWeekStart', () => '') const historyStatusSelection = useState>('historyStatusSelection', () => ['published', 'unpublished']) const historyGroupStartDate = useState('historyGroupStartDate', () => '') @@ -182,11 +189,12 @@ export const useAdmin = () => { } 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: '', + { 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: true }, { text: '', correct: false }, { text: '', correct: false }, { text: '', correct: false }, @@ -295,7 +303,7 @@ export const useAdmin = () => { const q: any = { id: Date.now(), type, text: '', textEs: '', reference: '', referenceEs: '', url: '' } if (type === 'mcq') { q.choices = [ - { text: '', correct: true }, + { text: '', correct: true }, { text: '', correct: false }, { text: '', correct: false }, { text: '', correct: false }, @@ -305,12 +313,12 @@ export const useAdmin = () => { } const publishForm = async () => { - if (!formTitle.value) { alert('Please enter a title!'); return } + if (!formTitle.value) { alert('Please enter a title!'); return } if (!formDays.value.length) { alert('Please select at least one day!'); return } try { const weekStart = getLastMonday(formWeekStart.value || '') - const days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] + const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] if (editingFormId.value) { const targetDay = formDays.value[0] || 'Monday' @@ -374,6 +382,39 @@ export const useAdmin = () => { }) } + //create duplicate forms for any additional selected days + const additionalDays = formDays.value.slice(1) + for (const day of additionalDays) { + const dayIdx = days.indexOf(day) + if (dayIdx === -1) continue + + const extraStartDate = parseLocalDate(weekStart) + if (!extraStartDate) continue + extraStartDate.setDate(extraStartDate.getDate() + dayIdx) + + const createdFormResponse = await callFormApi('POST', {}, { + action: 'createForm', + startDate: formatYmdLocal(extraStartDate), + published: true, + title: formTitle.value, + }) + + const createdForm = createdFormResponse?.data + if (!createdForm?.id) continue + + for (let index = 0; index < questions.value.length; index++) { + const question = questions.value[index] + await callFormApi('POST', {}, { + action: 'createComponent', + form: createdForm.id, + order: index, + questionType: question.type, + questionText: toApiQuestionText(question, formTitle.value), + questionOptions: buildQuestionOptions(question), + }) + } + } + await loadPublishedForms() editingFormId.value = null builderSubTab.value = 'history' @@ -395,12 +436,12 @@ export const useAdmin = () => { startDate.setDate(startDate.getDate() + dayIndex) - const createdFormResponse = await callFormApi('POST', {}, { - action: 'createForm', - startDate: formatYmdLocal(startDate), - published: true, - title: formTitle.value, - }) + const createdFormResponse = await callFormApi('POST', {}, { + action: 'createForm', + startDate: formatYmdLocal(startDate), + published: true, + title: formTitle.value, + }) const createdForm = createdFormResponse?.data @@ -431,10 +472,10 @@ export const useAdmin = () => { } const editPublishedForm = (form: any) => { - formTitle.value = form.title + formTitle.value = form.title formWeekStart.value = form.weekStart || formWeekStart.value - formDays.value = [form.day || 'Monday'] - questions.value = JSON.parse(JSON.stringify(form.questions)) + formDays.value = [form.day || 'Monday'] + questions.value = JSON.parse(JSON.stringify(form.questions)) editingFormId.value = form.id builderSubTab.value = 'creation' navigateTo('/admin/builder') @@ -451,12 +492,12 @@ export const useAdmin = () => { // ── 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' }, + { 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 sortStudent = useState('sortStudent', () => 'tickets') const filteredAndSortedStudents = computed(() => { let res = students.value @@ -466,8 +507,8 @@ export const useAdmin = () => { } 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) + if (sortStudent.value === 'streak') return b.streak - a.streak + if (sortStudent.value === 'name') return a.name.localeCompare(b.name) return 0 }) }) @@ -476,11 +517,11 @@ export const useAdmin = () => { 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' }, + { 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', () => ({ @@ -499,7 +540,7 @@ export const useAdmin = () => { const isAnnouncementActive = (ann: any): boolean => { const now = new Date().toISOString().split('T')[0] - if(!now) return false // in case of invalid date + 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 diff --git a/app/pages/reader/forms.vue b/app/pages/reader/forms.vue index 374602a2..98f4d21f 100644 --- a/app/pages/reader/forms.vue +++ b/app/pages/reader/forms.vue @@ -5,6 +5,14 @@ const { student, settings, updateExp } = useCurrentStudent() const { FormGroup } = useCurrentFormGroup() const { tickets, completedFormIds, logFormSubmission, logSubmissionResponse } = useCurrentStudentProgress() +//parse a date string (ISO or YYYY-MM-DD) into a local-midnight Date to avoid timezone shift when displaying weekday/day of month +function toLocalDate(dateStr: string): Date { + //extract the YYYY-MM-DD + const ymd = dateStr.split('T')[0] + const [y, m, d] = ymd.split('-').map(Number) + return new Date(y, m - 1, d) +} + const stats = computed(() => ({ xp: student.value ? student.value.exp : 0, tickets: tickets.value? tickets.value : 0, @@ -266,14 +274,14 @@ function getBadgeClass(type: string) { ? 'background:#f59e0b; color:white' : 'background:rgba(224,96,77,0.1); color:var(--brand-indigo)'" > - {{ new Date(form.startDate).toLocaleDateString('en-US', {weekday:'short'}) }} - {{ new Date(form.startDate).getDate() }} + {{ toLocalDate(form.startDate).toLocaleDateString('en-US', {weekday:'short'}) }} + {{ toLocalDate(form.startDate).getDate() }}

{{ form.title }}

- {{ new Date(form.startDate).toLocaleDateString('en-US', {weekday:'long'}) }} • + {{ toLocalDate(form.startDate).toLocaleDateString('en-US', {weekday:'long'}) }} • {{ (FormGroup.formComponents[form.id] || []).length }} Steps

✓ Done diff --git a/server/api/form/index.ts b/server/api/form/index.ts index f78e1e00..2ae5cb68 100644 --- a/server/api/form/index.ts +++ b/server/api/form/index.ts @@ -113,7 +113,7 @@ const formatDayName = (value: Date | null | undefined) => { return '' } - return WEEKDAY_NAMES[value.getUTCDay()] + return WEEKDAY_NAMES[value.getDay()] } const formatDisplayDate = (value: Date | null | undefined) => { From c375b74b1ef42a083b68f1c42f6e46ffc0262201 Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 9 Jun 2026 12:46:42 -0500 Subject: [PATCH 2/2] G1 pre pnpm typecheck --- app/composables/useAdmin.ts | 132 ++++++++++++++++++++++++------ app/pages/admin/announcements.vue | 5 +- app/pages/admin/builder.vue | 6 +- app/pages/reader/forms.vue | 8 +- 4 files changed, 121 insertions(+), 30 deletions(-) diff --git a/app/composables/useAdmin.ts b/app/composables/useAdmin.ts index ece87003..8e27675c 100644 --- a/app/composables/useAdmin.ts +++ b/app/composables/useAdmin.ts @@ -2,8 +2,90 @@ // Place this at: app/composables/useAdmin.ts import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' +import type { FormModel, FormComponentModel, AnnouncementModel, StudentModel } from '~~/prisma/generated/models' dayjs.extend(utc) + +// ── UI-layer types (shapes after API → UI transformation) ── + +interface QuestionChoice { + text: string + correct: boolean +} + +interface QuestionOptions { + textEs?: string + reference?: string + referenceEs?: string + url?: string + choices?: QuestionChoice[] +} + +/** A question as represented in the builder UI (not the raw DB row). */ +export interface UiQuestion { + id: number + type: string + text: string + textEs: string + reference: string + referenceEs: string + url: string + choices?: QuestionChoice[] +} + +/** A form after `mapApiFormToUi` transforms the API response. */ +export interface UiForm { + id: number + weekStart: string + day: string + title: string + date: string + status: string + questions: UiQuestion[] +} + +/** Shape returned by the create-form API endpoint. */ +type FormApiCreateResponse = { + success: boolean + message: string + data: FormModel & { questions: FormComponentModel[] } +} + +/** A local announcement as managed in admin state. */ +interface UiAnnouncement { + id: number + title: string + content: string + icon: string + startDate: string + endDate: string + weekStart: string + day: string +} + +interface NewAnnouncement { + title: string + content: string + icon: string + startDate: string + endDate: string + weekStart: string + day: string +} + +/** A student row as used in the admin progress tab. */ +interface UiStudent { + id: number + name: string + initials: string + email: string + tickets: number + streak: number + lastActive: string +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- raw API responses have unpredictable shapes +type RawApiRecord = Record export const useAdmin = () => { const callFormApi = async (method: 'GET' | 'POST' | 'PUT' | 'DELETE', params: Record = {}, body?: Record): Promise => { const queryString = method === 'GET' || method === 'DELETE' @@ -63,7 +145,7 @@ export const useAdmin = () => { return formatYmdLocal(fallback) } - const buildQuestionOptions = (question: any) => ({ + const buildQuestionOptions = (question: UiQuestion): QuestionOptions => ({ textEs: question.textEs ?? '', reference: question.reference ?? '', referenceEs: question.referenceEs ?? '', @@ -71,7 +153,7 @@ export const useAdmin = () => { choices: Array.isArray(question.choices) ? question.choices : [], }) - const toApiQuestionText = (question: any, fallbackTitle: string) => { + const toApiQuestionText = (question: UiQuestion, fallbackTitle: string): string => { if (typeof question.text === 'string' && question.text.trim()) { return question.text.trim() } @@ -87,7 +169,7 @@ export const useAdmin = () => { return fallbackTitle || 'Untitled question' } - const mapApiFormToUi = (form: any) => { + const mapApiFormToUi = (form: RawApiRecord): UiForm => { const questionList = Array.isArray(form.questions) ? form.questions : [] return { @@ -97,7 +179,7 @@ export const useAdmin = () => { title: form.title || `Form ${form.id}`, date: form.date || formatDate(form.startDate || ''), status: form.status || (form.published ? 'Active' : 'Unpublished'), - questions: questionList.map((question: any, index: number) => ({ + questions: questionList.map((question: RawApiRecord, index: number) => ({ id: Number(question.id ?? index + 1), type: question.type || question.questionType || 'text', text: question.text || question.questionText || '', @@ -114,7 +196,7 @@ export const useAdmin = () => { const builderSubTab = useState<'history' | 'creation'>('builderSubTab', () => 'history') const formTitle = useState('formTitle', () => '') const editingFormId = useState('editingFormId', () => null) - const questions = useState('questions', () => []) + const questions = useState('questions', () => []) // Week/day pickers — default to current Monday const todayDate = new Date() @@ -129,7 +211,7 @@ export const useAdmin = () => { const historyStatusSelection = useState>('historyStatusSelection', () => ['published', 'unpublished']) const historyGroupStartDate = useState('historyGroupStartDate', () => '') const historyGroupEndDate = useState('historyGroupEndDate', () => '') - const selectedFormDetails = useState('selectedFormDetails', () => null) + const selectedFormDetails = useState('selectedFormDetails', () => null) const toggleHistoryStatus = (value: 'published' | 'unpublished') => { if (historyStatusSelection.value.includes(value)) { @@ -188,7 +270,7 @@ export const useAdmin = () => { }) } - const defaultQuestions = (): any[] => [ + const defaultQuestions = (): UiQuestion[] => [ { id: Date.now(), type: 'video', text: '', textEs: '', reference: '', referenceEs: '', url: '' }, { id: Date.now() + 1, type: 'text', text: '', textEs: '', reference: '', referenceEs: '', url: '' }, { @@ -203,7 +285,7 @@ export const useAdmin = () => { ] // ── Published forms ── - const publishedForms = useState('publishedForms', () => []) + const publishedForms = useState('publishedForms', () => []) const filteredPublishedForms = computed(() => publishedForms.value.filter((form) => { @@ -228,7 +310,7 @@ export const useAdmin = () => { const loadPublishedForms = async () => { try { - const forms = await callFormApi('GET', { + const forms = await callFormApi('GET', { action: 'listForms', weeklyDate: historyWeekStart.value || undefined, }) @@ -293,6 +375,7 @@ export const useAdmin = () => { const onDrop = (_e: DragEvent, index: number) => { if (draggedIdx.value === null) return const dragged = questions.value[draggedIdx.value] + if (!dragged) return questions.value.splice(draggedIdx.value, 1) questions.value.splice(index, 0, dragged) draggedIdx.value = null @@ -300,7 +383,7 @@ export const useAdmin = () => { // ── CRUD ── const addQuestion = (type: string) => { - const q: any = { id: Date.now(), type, text: '', textEs: '', reference: '', referenceEs: '', url: '' } + const q: UiQuestion = { id: Date.now(), type, text: '', textEs: '', reference: '', referenceEs: '', url: '' } if (type === 'mcq') { q.choices = [ { text: '', correct: true }, @@ -343,12 +426,13 @@ export const useAdmin = () => { const existingForm = publishedForms.value.find((form) => Number(form.id) === Number(editingFormId.value)) const existingComponentIds = new Set( (existingForm?.questions ?? []) - .map((question: any) => Number(question.id)) + .map((question: UiQuestion) => Number(question.id)) .filter((questionId: number) => Number.isInteger(questionId) && questionId > 0) ) for (let index = 0; index < questions.value.length; index++) { const question = questions.value[index] + if (!question) continue const numericQuestionId = Number(question.id) const isExistingComponent = Number.isInteger(numericQuestionId) && existingComponentIds.has(numericQuestionId) @@ -392,7 +476,7 @@ export const useAdmin = () => { if (!extraStartDate) continue extraStartDate.setDate(extraStartDate.getDate() + dayIdx) - const createdFormResponse = await callFormApi('POST', {}, { + const createdFormResponse = await callFormApi('POST', {}, { action: 'createForm', startDate: formatYmdLocal(extraStartDate), published: true, @@ -404,6 +488,7 @@ export const useAdmin = () => { for (let index = 0; index < questions.value.length; index++) { const question = questions.value[index] + if (!question) continue await callFormApi('POST', {}, { action: 'createComponent', form: createdForm.id, @@ -436,7 +521,7 @@ export const useAdmin = () => { startDate.setDate(startDate.getDate() + dayIndex) - const createdFormResponse = await callFormApi('POST', {}, { + const createdFormResponse = await callFormApi('POST', {}, { action: 'createForm', startDate: formatYmdLocal(startDate), published: true, @@ -451,6 +536,7 @@ export const useAdmin = () => { for (let index = 0; index < questions.value.length; index++) { const question = questions.value[index] + if (!question) continue await callFormApi('POST', {}, { action: 'createComponent', @@ -471,7 +557,7 @@ export const useAdmin = () => { } } - const editPublishedForm = (form: any) => { + const editPublishedForm = (form: UiForm) => { formTitle.value = form.title formWeekStart.value = form.weekStart || formWeekStart.value formDays.value = [form.day || 'Monday'] @@ -481,16 +567,16 @@ export const useAdmin = () => { navigateTo('/admin/builder') } - const toggleFormPublish = (form: any) => { + const toggleFormPublish = (form: UiForm) => { form.status = form.status === 'Active' ? 'Unpublished' : 'Active' } - const viewFormDetails = (form: any) => { + const viewFormDetails = (form: UiForm) => { selectedFormDetails.value = form } // ── Students / Progress ── - const students = useState('adminStudents', () => [ + 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' }, @@ -516,7 +602,7 @@ export const useAdmin = () => { // ── Announcements ── const announcementSubTab = useState<'creation' | 'history'>('announcementSubTab', () => 'creation') - const announcements = useState('announcements', () => [ + 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' }, @@ -524,9 +610,9 @@ export const useAdmin = () => { { 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', () => ({ + const newAnnouncement = useState('newAnnouncement', () => ({ title: '', content: '', icon: '🌟', - startDate: new Date().toISOString().split('T')[0], + startDate: new Date().toISOString().split('T')[0] || '', endDate: '', weekStart: monStr, day: 'Monday', })) @@ -538,7 +624,7 @@ export const useAdmin = () => { ) ) - const isAnnouncementActive = (ann: any): boolean => { + const isAnnouncementActive = (ann: UiAnnouncement): boolean => { const now = new Date().toISOString().split('T')[0] if (!now) return false // in case of invalid date if (ann.startDate > now) return false @@ -552,13 +638,13 @@ export const useAdmin = () => { announcements.value.push({ id: Date.now(), ...JSON.parse(JSON.stringify(na)) }) newAnnouncement.value = { title: '', content: '', icon: '🌟', - startDate: new Date().toISOString().split('T')[0], + 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) + announcements.value = announcements.value.filter((a) => a.id !== id) } return { diff --git a/app/pages/admin/announcements.vue b/app/pages/admin/announcements.vue index 694f2536..54d49ebc 100644 --- a/app/pages/admin/announcements.vue +++ b/app/pages/admin/announcements.vue @@ -118,7 +118,7 @@ async function loadHistory () { historyError.value = null try { // $fetch is Nuxt's HTTP utility (wraps native fetch with nice error handling). - allAnnouncements.value = await $fetch('/api/announcement') + allAnnouncements.value = await $fetch('/api/announcement') } catch (e: any) { //Capture the error message; fall back to a generic string if none exists historyError.value = e?.message ?? 'Failed to load announcements.' @@ -442,8 +442,7 @@ async function postAnnouncement () { {{ fmtDate(ann.postDate) }}{{ ann.expiryDate ? ' → ' + fmtDate(ann.expiryDate) : ' (Ongoing)' }} - - announcement's database integer ID. The icon-only + diff --git a/app/pages/reader/forms.vue b/app/pages/reader/forms.vue index 98f4d21f..923b9acd 100644 --- a/app/pages/reader/forms.vue +++ b/app/pages/reader/forms.vue @@ -9,7 +9,9 @@ const { tickets, completedFormIds, logFormSubmission, logSubmissionResponse } = function toLocalDate(dateStr: string): Date { //extract the YYYY-MM-DD const ymd = dateStr.split('T')[0] + if (!ymd) return new Date() const [y, m, d] = ymd.split('-').map(Number) + if (y === undefined || m === undefined || d === undefined) return new Date() return new Date(y, m - 1, d) } @@ -179,10 +181,14 @@ async function submitChallenge() { function onTicketDragStart(e: DragEvent) { e.dataTransfer?.setData('text/plain', 'ticket') } function onTicketDrop() { ticketOverBox.value = false; ticketDropped.value = true } -function onTicketTouchStart(e: TouchEvent) { touchStartY = e.touches[0].clientY } +function onTicketTouchStart(e: TouchEvent) { + const t = e.touches[0] + if (t) touchStartY = t.clientY +} function onTicketTouchMove(e: TouchEvent) { e.preventDefault() const t = e.touches[0] + if (!t) return const dy = t.clientY - touchStartY ticketStyle.value = { transform: `translate(-50%, ${dy}px)` } const box = document.getElementById('raffle-box')