113 lines
3.5 KiB
Vue
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>
|