studium/frontend/src/views/CalendarView.vue
Antoine Pelletier 5e160ae11c wip
2026-09-29 09:47:27 +02:00

113 lines
3.5 KiB
Vue

<script setup lang="ts">
/**
* Deadlines of the courses and associations in a month calendar, next to everything left
* to do. Courses and associations can each be hidden, and tasks created from here.
*/
import { computed, ref, watch } from 'vue'
import { BookOpen, Plus, Users } from '@lucide/vue'
import TasksCalendar from '@/components/calendar/TasksCalendar.vue'
import TodoList from '@/components/calendar/TodoList.vue'
import TaskFormDialog from '@/components/course/TaskFormDialog.vue'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import type { CourseKind } from '@/utils/types'
const KINDS: { kind: CourseKind; icon: typeof BookOpen }[] = [
{ kind: 'course', icon: BookOpen },
{ kind: 'association', icon: Users },
]
// Remembered in the browser: only a convenience
const STORAGE_KEY = 'calendar-kinds'
function loadKinds(): CourseKind[] {
try {
const saved = JSON.parse(window.localStorage.getItem(STORAGE_KEY) ?? 'null')
if (Array.isArray(saved)) return saved.filter((k) => k === 'course' || k === 'association')
} catch {
// Unavailable or invalid: everything is shown
}
return ['course', 'association']
}
const kinds = ref<CourseKind[]>(loadKinds())
watch(kinds, (value) => {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value))
} catch {
// Not remembered
}
})
function toggle(kind: CourseKind) {
kinds.value = kinds.value.includes(kind)
? kinds.value.filter((k) => k !== kind)
: [...kinds.value, kind]
}
const formOpen = ref(false)
const defaultDeadline = ref<string>()
/** New task, due at the end of `day` when given */
function openForm(day?: Date) {
if (day) {
const end = new Date(day)
end.setHours(23, 59, 0, 0)
defaultDeadline.value = end.toISOString()
} else {
defaultDeadline.value = undefined
}
formOpen.value = true
}
const nothingShown = computed(() => !kinds.value.length)
</script>
<template>
<div class="mx-auto flex max-w-7xl flex-col gap-4">
<div class="flex flex-wrap items-center gap-2">
<div class="flex gap-2" role="group" :aria-label="$t('calendar.show')">
<button
v-for="{ kind, icon } in KINDS"
:key="kind"
type="button"
class="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors"
:class="
kinds.includes(kind)
? 'border-primary bg-primary/10 font-medium text-primary'
: 'text-muted-foreground hover:bg-accent'
"
:aria-pressed="kinds.includes(kind)"
@click="toggle(kind)"
>
<component :is="icon" class="size-4" />
{{ $t(`nav.${kind}s`) }}
</button>
</div>
<Button class="ml-auto" @click="openForm()">
<Plus />
{{ $t('tasks.new') }}
</Button>
</div>
<p v-if="nothingShown" class="text-sm text-muted-foreground">
{{ $t('calendar.nothing-shown') }}
</p>
<div class="grid items-start gap-4 lg:grid-cols-[minmax(0,1fr)_22rem]">
<TasksCalendar :kinds="kinds" class="min-w-0" @create="openForm" />
<Card class="lg:sticky lg:top-4 lg:max-h-[calc(100svh-6rem)] lg:overflow-y-auto">
<CardHeader>
<CardTitle class="text-base">{{ $t('calendar.todo.title') }}</CardTitle>
</CardHeader>
<CardContent>
<TodoList :kinds="kinds" />
</CardContent>
</Card>
</div>
<TaskFormDialog v-model:open="formOpen" :default-deadline="defaultDeadline" />
</div>
</template>