Skip to content
Merged
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
83 changes: 52 additions & 31 deletions app/components/SidebarTodayEvents.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { motion, AnimatePresence } from "motion-v"
import type { TEventType } from "~/types/calendar"

const scheduleStore = useScheduleStore()
const calendarStore = useCalendarStore()
const { selectedSchedule } = storeToRefs(scheduleStore)
const { allEvents } = storeToRefs(calendarStore)
const { formatTime, formatDate } = useEventFormatting()
const { effectiveTimezone } = useTimezone()

Expand All @@ -24,13 +22,33 @@ const formattedDate = computed(() => {
return `${day}, ${date}`
})

// Filter the already-loaded full-year events to the preview date — no extra network request.
// 3.3 fix: guard auditorium?.name — auditorium can be null for online lessons.
// Own query pinned to today's academic year — independent of the calendar's
// navigation date. TanStack dedupes when the user is in the same academic year
// (zero extra requests); diverges only when they navigate to a different year.
const scheduleId = computed(() => selectedSchedule.value?.id)
const todayRange = getAcademicYearRange(new Date())
const todayStart = computed(() => {
if (!selectedSchedule.value) return undefined
return Math.floor(todayRange.start.getTime() / 1000)
})
const todayEnd = computed(() => {
if (!selectedSchedule.value) return undefined
return Math.floor(todayRange.end.getTime() / 1000)
})

const { data: todayRangeEvents, isLoading } = useScheduleQuery(scheduleId, todayStart, todayEnd)

// Filter the year-range events down to just previewDate — no extra network request.
// dayKey drives the outer AnimatePresence key so changing date triggers a full
// wait-mode swap: old list exits, then new list enters (no stacking).
const dayKey = computed(() =>
formatInTimeZone(previewDate.value, effectiveTimezone.value, "yyyy-MM-dd")
)
const todayEvents = computed(() => {
if (!todayRangeEvents.value) return []
const tz = effectiveTimezone.value
const targetKey = formatInTimeZone(previewDate.value, tz, "yyyy-MM-dd")
return allEvents.value.filter(
(e) => formatInTimeZone(new Date(e.startedAt * 1000), tz, "yyyy-MM-dd") === targetKey
return todayRangeEvents.value.filter(
(e) => formatInTimeZone(new Date(e.startedAt * 1000), tz, "yyyy-MM-dd") === dayKey.value
)
})

Expand Down Expand Up @@ -86,39 +104,42 @@ const hasEvents = computed(() => todayEvents.value.length > 0)
</div>
</template>
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
<UiScrollArea class="min-h-0 flex-1">
<!-- Loading skeleton while today's query is in-flight -->
<div v-if="isLoading" class="flex flex-col gap-3">
<UiSkeleton v-for="i in 3" :key="i" class="h-16 w-full rounded-md" />
</div>
<UiScrollArea v-else class="min-h-0 flex-1">
<AnimatePresence mode="wait">
<!-- Event list state -->
<!-- Event list state — keyed by dayKey so the outer mode="wait" triggers
a full sequential swap: old list exits completely, then new list enters.
No inner AnimatePresence needed; per-card stagger on enter is enough. -->
<motion.div
v-if="hasActiveSchedule && hasEvents"
key="event-list"
:key="`event-list-${dayKey}`"
:initial="{ opacity: 0, y: 6 }"
:animate="{ opacity: 1, y: 0 }"
:exit="{ opacity: 0, y: -6 }"
:transition="{ duration: 0.18 }"
>
<div class="flex flex-col gap-3">
<AnimatePresence>
<motion.div
v-for="(event, index) in todayEvents"
:key="event.id"
:initial="{ opacity: 0, y: 10 }"
:animate="{ opacity: 1, y: 0 }"
:exit="{ opacity: 0 }"
:transition="{
duration: 0.2,
delay: Math.min(index * 0.05, 0.2),
}"
>
<SidebarEvent
:start-time="formatTime(event.startedAt)"
:end-time="formatTime(event.endedAt)"
:auditorium="event.auditorium?.name ?? 'Не вказана'"
:type="event.type as TEventType"
:name="event.subject.title"
/>
</motion.div>
</AnimatePresence>
<motion.div
v-for="(event, index) in todayEvents"
:key="event.id"
:initial="{ opacity: 0, y: 10 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{
duration: 0.2,
delay: Math.min(index * 0.05, 0.2),
}"
>
<SidebarEvent
:start-time="formatTime(event.startedAt)"
:end-time="formatTime(event.endedAt)"
:auditorium="event.auditorium?.name ?? 'Не вказана'"
:type="event.type as TEventType"
:name="event.subject.title"
/>
</motion.div>
</div>
</motion.div>

Expand Down
3 changes: 2 additions & 1 deletion app/components/calendar/month/DayCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ const { trackEvent } = useAnalytics()
// SSR serializer puts object keys first; client patchClass appends them after static.
const dayClasses = computed(() =>
cn("flex size-6 items-center justify-center rounded-full text-xs font-medium", {
"text-muted-foreground/50": !props.cell.currentMonth,
"text-muted-foreground/50": !props.cell.currentMonth && !props.isDateToday,
"text-muted-foreground": props.cell.currentMonth && !props.isDateToday,
"bg-primary": props.isDateToday && props.cell.currentMonth,
"bg-primary/40": props.isDateToday && !props.cell.currentMonth,
})
)

Expand Down
12 changes: 0 additions & 12 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,6 @@ useSeo({
const hasActiveSchedule = computed(() => !!selectedSchedule.value)
const scheduleId = computed(() => selectedSchedule.value?.id)

const getAcademicYearRange = (date: Date) => {
// Academic year: Sept 1 → Sept 1. Pad ±7 days so month/week grid cells
// that overhang the boundary (up to 6 days in either direction) still have
// events fetched. Without the pad, September cells in August's grid and
// August cells in September's grid render empty.
const startYear = date.getMonth() < 8 ? date.getFullYear() - 1 : date.getFullYear()
return {
start: new Date(startYear, 8, 1 - 7, 0, 0, 0, 0),
end: new Date(startYear + 1, 8, 1 + 7, 23, 59, 59, 999),
}
}

const dateRange = computed(() => getAcademicYearRange(selectedDate.value))

const startTimestamp = computed(() => {
Expand Down
13 changes: 13 additions & 0 deletions app/utils/academic-year.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Returns the academic year date range covering the given date.
* Academic year: Sept 1 → Sept 1. Padded ±7 days so month/week grid cells
* that overhang the boundary (up to 6 days in either direction) still have
* events fetched.
*/
export const getAcademicYearRange = (date: Date) => {
const startYear = date.getMonth() < 8 ? date.getFullYear() - 1 : date.getFullYear()
return {
start: new Date(startYear, 8, 1 - 7, 0, 0, 0, 0),
end: new Date(startYear + 1, 8, 1 + 7, 23, 59, 59, 999),
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mindenit-schedule",
"version": "1.2.1",
"version": "1.2.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
Loading