Initial commit

This commit is contained in:
2026-04-17 23:26:01 +00:00
commit 2ea4ca5d52
409 changed files with 63459 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
<template>
<UHeader :toggle="false">
<template #left>
<ULink to="/">
<NuxtImg
src="logo.png"
alt="Logo"
class="size-12"
/>
</ULink>
<ProfileLangSelect />
</template>
<template #right>
<UColorModeButton />
<AppNotifications v-if="isAuthenticated" />
<UDropdownMenu :items="items">
<ProfileAvatar
size="xl"
/>
</UDropdownMenu>
</template>
</UHeader>
</template>
<script setup lang="ts">
const { isAuthenticated } = storeToRefs(useUser())
const { signOut } = useUser()
const items = computed(() => {
if (isAuthenticated.value) {
return [[{
label: 'Logout',
icon: 'i-lucide-log-out',
onSelect: () => {
signOut()
navigateTo('/')
} }]
]
} else {
return [[{
label: 'Sign In',
icon: 'i-lucide-log-in',
onSelect: () => {
navigateTo('/login')
} }]
]
}
})
</script>

View File

@@ -0,0 +1,85 @@
<template>
<UTooltip
:text="$t('notifications')"
:shortcuts="['N']"
>
<UButton
color="neutral"
variant="ghost"
square
@click="drawerOpen = true"
>
<UChip
color="error"
:show="hasUnreadNotifications"
inset
>
<UIcon
name="i-lucide-bell"
class="size-5 shrink-0"
/>
</UChip>
</UButton>
</UTooltip>
<USlideover
v-model:open="drawerOpen"
:title="$t('notifications')"
>
<template #body>
<div
v-for="notification in notifications"
:key="notification.id"
v-on-visible="() => markAsRead(notification.id)"
class="px-3 py-2.5 rounded-md hover:bg-elevated/50 flex items-center gap-3 relative -mx-3 first:-mt-3 last:-mb-3"
>
<UChip
color="error"
:show="!notification.isRead"
inset
>
<UAvatar
alt="Nuxt"
size="md"
/>
</UChip>
<div class="text-sm flex-1">
<p class="flex items-center justify-between">
<span class="text-highlighted font-medium">{{ notification.title }}</span>
<time
:datetime="notification.created"
class="text-muted text-xs"
v-text="$d(new Date(notification.created), { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' })"
/>
</p>
<p class="text-dimmed">
{{ notification.body }}
</p>
</div>
</div>
</template>
</USlideover>
</template>
<script setup lang="ts">
const { fetchNotifications, subscribeToChanges, unsubscribe, markAsRead } = useNotifications()
const { notifications } = storeToRefs(useNotifications())
const drawerOpen = ref(false)
const hasUnreadNotifications = computed(() => notifications.value.some(n => !n.isRead))
// Fetch the notifications once on page load
fetchNotifications()
onMounted(() => {
// Subscribe to realtime changes of notifications
subscribeToChanges()
})
// Unsubscribe for cleanup
onUnmounted(() => {
unsubscribe()
})
</script>

View File

@@ -0,0 +1,51 @@
<template>
<UCard>
<div class="flex flex-col items-center justify-center gap-4 p-4">
<div class="flex items-center gap-4">
<UButton
icon="i-lucide-minus"
size="lg"
variant="outline"
@click="increment(-1)"
/>
<div class="text-4xl font-bold w-16 text-center">
{{ count }}
</div>
<UButton
icon="i-lucide-plus"
size="lg"
variant="outline"
@click="increment(1)"
/>
</div>
<UButton
v-if="recordId"
icon="i-lucide-rotate-ccw"
size="md"
variant="ghost"
@click="reset()"
>
{{ $t("counter.reset") }}
</UButton>
</div>
</UCard>
</template>
<script setup lang="ts">
const { fetchCurrentCount, subscribeToChanges, unsubscribe, reset, increment }
= useCounter()
const { count, recordId } = storeToRefs(useCounter())
// Fetch the current count once on page load
fetchCurrentCount()
onMounted(() => {
// Subscribe to realtime changes of the count
subscribeToChanges()
})
// Unsubscribe for cleanup
onUnmounted(() => {
unsubscribe()
})
</script>

View File

@@ -0,0 +1,21 @@
<template>
<UAvatar
v-bind="forwardedProps"
:src="src ?? undefined"
:alt="name ?? undefined"
:icon="icon"
/>
</template>
<script setup lang="ts">
import type { AvatarProps } from '@nuxt/ui'
const forwardedProps = defineProps<Omit<AvatarProps, 'src' | 'alt' | 'icon'>>()
const { name, src } = storeToRefs(useAvatar())
// show initials if name is present, otherwise show icon
const icon = computed(() => {
return !name.value ? 'i-lucide-user' : undefined
})
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div class="flex flex-wrap items-center gap-3">
<ProfileAvatar
size="lg"
/>
<UButton
:label="$t('profile.choose')"
color="neutral"
:loading="uploading"
@click="onFileClick"
/>
<input
ref="fileRef"
type="file"
class="hidden"
accept=".jpg, .jpeg, .png, .gif"
@change="onFileChange"
>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const toast = useToast()
const fileRef = ref<HTMLInputElement>()
const uploading = ref(false)
async function onFileChange(e: Event) {
const input = e.target as HTMLInputElement
const files = input.files
try {
uploading.value = true
if (!files || files.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = files[0]
await useAvatar().uploadAvatar(file!)
toast.add({
title: t('profile.success'),
description: t('profile.avatarUploaded'),
icon: 'i-lucide-check',
color: 'success'
})
} catch (error) {
toast.add({ color: 'error', description: (error as Error).message })
} finally {
uploading.value = false
}
}
function onFileClick() {
fileRef.value?.click()
}
</script>

View File

@@ -0,0 +1,25 @@
<template>
<ULocaleSelect
:model-value="locale"
:locales="availableLocales"
@update:model-value="setLanguage($event as LanguageCode)"
/>
</template>
<script setup lang="ts">
import * as allLocales from '@nuxt/ui/locale'
import type { LanguageCode } from '~/types/i18n.types'
import type { UsersLanguageOptions } from '~/types/pocketbase.types'
const { locale, locales, setLocale } = useI18n()
const availableLocales = computed(() =>
locales.value.map(l => allLocales[l.code])
)
const { updateUser } = useUser()
const setLanguage = (language: LanguageCode) => {
setLocale(language)
updateUser({ language: language as UsersLanguageOptions })
}
</script>