Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 79 additions & 38 deletions app/composables/useAdmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ export const useAdmin = () => {
const callFormApi = async <T>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', params: Record<string, unknown> = {}, body?: Record<string, unknown>): Promise<T> => {
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<string, string>)).toString()}`
return acc
}, {} as Record<string, string>)).toString()}`
: ''

return await $fetch<T>(`/api/form${queryString}`, {
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this even used?

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 ''
Expand Down Expand Up @@ -104,10 +111,10 @@ export const useAdmin = () => {
}

// ── Builder state ──
const builderSubTab = useState<'history' | 'creation'>('builderSubTab', () => 'history')
const formTitle = useState('formTitle', () => '')
const editingFormId = useState<number | null>('editingFormId', () => null)
const questions = useState<any[]>('questions', () => [])
const builderSubTab = useState<'history' | 'creation'>('builderSubTab', () => 'history')
const formTitle = useState('formTitle', () => '')
const editingFormId = useState<number | null>('editingFormId', () => null)
const questions = useState<any[]>('questions', () => [])

// Week/day pickers — default to current Monday
const todayDate = new Date()
Expand All @@ -116,8 +123,8 @@ export const useAdmin = () => {
mon.setDate(todayDate.getDate() - dayOff)
const monStr = formatYmdLocal(mon)

const formWeekStart = useState('formWeekStart', () => monStr)
const formDays = useState<string[]>('formDays', () => ['Monday'])
const formWeekStart = useState('formWeekStart', () => monStr)
const formDays = useState<string[]>('formDays', () => ['Monday'])
const historyWeekStart = useState('historyWeekStart', () => '')
const historyStatusSelection = useState<Array<'published' | 'unpublished'>>('historyStatusSelection', () => ['published', 'unpublished'])
const historyGroupStartDate = useState('historyGroupStartDate', () => '')
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 },
Expand All @@ -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'
Expand Down Expand Up @@ -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<any>('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'
Expand All @@ -395,12 +436,12 @@ export const useAdmin = () => {

startDate.setDate(startDate.getDate() + dayIndex)

const createdFormResponse = await callFormApi<any>('POST', {}, {
action: 'createForm',
startDate: formatYmdLocal(startDate),
published: true,
title: formTitle.value,
})
const createdFormResponse = await callFormApi<any>('POST', {}, {
action: 'createForm',
startDate: formatYmdLocal(startDate),
published: true,
title: formTitle.value,
})

const createdForm = createdFormResponse?.data

Expand Down Expand Up @@ -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')
Expand All @@ -451,12 +492,12 @@ export const useAdmin = () => {
// ── Students / Progress ──
const students = useState<any[]>('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
Expand All @@ -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
})
})
Expand All @@ -476,11 +517,11 @@ export const useAdmin = () => {
const announcementSubTab = useState<'creation' | 'history'>('announcementSubTab', () => 'creation')

const announcements = useState<any[]>('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<any>('newAnnouncement', () => ({
Expand All @@ -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
Expand Down
14 changes: 11 additions & 3 deletions app/pages/reader/forms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do this with cleaner with dayjs library. dayjs(date).format('YYYY-MM-DD').

Also, this function (with dayjs implemented) is already used in useAdmin from Swarna's incoming pr. It should be pulled from there. I'll make her extract the date logic to its own composable for clearer use across both reader and admin

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^^^essentially, wait for date logic changed pr to be merged into main and then use those functions here

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,
Expand Down Expand Up @@ -266,14 +274,14 @@ function getBadgeClass(type: string) {
? 'background:#f59e0b; color:white'
: 'background:rgba(224,96,77,0.1); color:var(--brand-indigo)'"
>
<span class="text-[9px] uppercase leading-none">{{ new Date(form.startDate).toLocaleDateString('en-US', {weekday:'short'}) }}</span>
<span class="text-lg font-black leading-tight">{{ new Date(form.startDate).getDate() }}</span>
<span class="text-[9px] uppercase leading-none">{{ toLocalDate(form.startDate).toLocaleDateString('en-US', {weekday:'short'}) }}</span>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure to update the actual calls to the function once other work implemented

<span class="text-lg font-black leading-tight">{{ toLocalDate(form.startDate).getDate() }}</span>
</div>
<div>
<h4 class="font-heading text-lg font-bold" style="color:var(--brand-dark)">{{ form.title }}</h4>
<div class="flex items-center gap-2 mt-0.5">
<p class="text-xs font-bold text-gray-400">
{{ new Date(form.startDate).toLocaleDateString('en-US', {weekday:'long'}) }} •
{{ toLocalDate(form.startDate).toLocaleDateString('en-US', {weekday:'long'}) }} •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as previous comment

{{ (FormGroup.formComponents[form.id] || []).length }} Steps
</p>
<span v-if="completedFormIds.includes(form.id)" class="text-xs font-black px-2 py-0.5 rounded-full" style="color:var(--brand-mint); background:rgba(45,212,191,0.15)">✓ Done</span>
Expand Down
2 changes: 1 addition & 1 deletion server/api/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down