Initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UApp>
|
||||
</template>
|
||||
@@ -0,0 +1,112 @@
|
||||
html {
|
||||
font-family: var(--font-family-base);
|
||||
background: var(--ui-bg);
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
var(--page-accent),
|
||||
linear-gradient(180deg, var(--ui-bg), var(--ui-bg-muted));
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(calc(100% - 2rem), var(--page-max-width));
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 0 3rem;
|
||||
}
|
||||
|
||||
.shell__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.shell__brand,
|
||||
.stack {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shell__badge,
|
||||
.shell__eyebrow,
|
||||
.section-kicker {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.shell__eyebrow,
|
||||
.section-kicker {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 0.75rem;
|
||||
color: var(--ui-text-muted);
|
||||
}
|
||||
|
||||
.shell__title {
|
||||
font-size: clamp(2rem, 4vw, 3.2rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.shell__subtitle {
|
||||
max-width: 54rem;
|
||||
color: var(--ui-text-muted);
|
||||
}
|
||||
|
||||
.shell__actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.shell__select {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.page-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.hero-actions,
|
||||
.component-showcase {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shell__header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shell__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.page-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
:root,
|
||||
.light {
|
||||
--page-accent: radial-gradient(
|
||||
circle at top right,
|
||||
color-mix(in srgb, var(--ui-primary) 18%, transparent),
|
||||
transparent 34%
|
||||
);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--page-accent: radial-gradient(
|
||||
circle at top right,
|
||||
color-mix(in srgb, var(--ui-primary) 22%, transparent),
|
||||
transparent 32%
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
:root {
|
||||
--font-family-base: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
--page-max-width: 1200px;
|
||||
--ui-radius: 0.75rem;
|
||||
--shadow-soft: 0 20px 45px rgba(15, 23, 42, 0.12);
|
||||
--transition-base: 180ms ease;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@use './tokens';
|
||||
@use './theme';
|
||||
@use './base';
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ApiClients } from '~/types/api'
|
||||
|
||||
export function useApi(): ApiClients {
|
||||
return useNuxtApp().$api
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { AuthClient } from '~/types/auth'
|
||||
|
||||
export function useAuth(): AuthClient {
|
||||
return useNuxtApp().$auth
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||
|
||||
type LoggerPayload = Record<string, unknown> | undefined
|
||||
|
||||
type LoggerApi = {
|
||||
debug: (message: string, payload?: LoggerPayload) => void
|
||||
info: (message: string, payload?: LoggerPayload) => void
|
||||
warn: (message: string, payload?: LoggerPayload) => void
|
||||
error: (message: string, payload?: LoggerPayload) => void
|
||||
}
|
||||
|
||||
export function useLogger(): LoggerApi {
|
||||
return useNuxtApp().$logger
|
||||
}
|
||||
|
||||
export type { LoggerApi, LoggerPayload, LogLevel }
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="shell">
|
||||
<header class="shell__header">
|
||||
<div class="shell__brand">
|
||||
<UBadge color="neutral" variant="subtle" class="shell__badge">
|
||||
{{ t('app.badge') }}
|
||||
</UBadge>
|
||||
<h1 class="shell__title">{{ t('app.title') }}</h1>
|
||||
<p class="shell__subtitle">{{ t('app.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="shell__actions">
|
||||
<USelect
|
||||
v-model="currentLocale"
|
||||
:items="localeOptions"
|
||||
size="sm"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="shell__select"
|
||||
/>
|
||||
<ClientOnly>
|
||||
<USelect
|
||||
v-model="themePreference"
|
||||
:items="themeOptions"
|
||||
size="sm"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="shell__select"
|
||||
/>
|
||||
<template #fallback>
|
||||
<USelect
|
||||
:model-value="defaultThemePreference"
|
||||
:items="themeOptions"
|
||||
size="sm"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="shell__select"
|
||||
disabled
|
||||
/>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="shell__main">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type LocaleCode = 'sk' | 'en'
|
||||
type ThemePreference = 'light' | 'dark' | 'system'
|
||||
|
||||
const { locale, setLocale, t } = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const localeOptions = computed(() => [
|
||||
{ label: 'SK', value: 'sk' },
|
||||
{ label: 'EN', value: 'en' }
|
||||
])
|
||||
|
||||
const themeOptions = computed(() => [
|
||||
{ label: `${t('theme.toggle')}: ${t('theme.preference.light')}`, value: 'light' },
|
||||
{ label: `${t('theme.toggle')}: ${t('theme.preference.dark')}`, value: 'dark' },
|
||||
{ label: `${t('theme.toggle')}: ${t('theme.preference.system')}`, value: 'system' }
|
||||
])
|
||||
|
||||
const defaultThemePreference: ThemePreference = 'system'
|
||||
|
||||
const currentLocale = computed({
|
||||
get: () => locale.value as LocaleCode,
|
||||
set: (value: LocaleCode) => {
|
||||
if (value && value !== locale.value) {
|
||||
setLocale(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const themePreference = computed({
|
||||
get: () => colorMode.preference as ThemePreference,
|
||||
set: (value: ThemePreference) => {
|
||||
colorMode.preference = value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const auth = useAuth()
|
||||
|
||||
if (!auth.isEnabled || to.meta.public) {
|
||||
return
|
||||
}
|
||||
|
||||
const requiresAuth = Boolean(to.meta.requiresAuth || (to.meta.roles && to.meta.roles.length > 0))
|
||||
|
||||
if (!requiresAuth) {
|
||||
return
|
||||
}
|
||||
|
||||
await auth.ensureInitialized()
|
||||
|
||||
if (!auth.isAuthenticated.value) {
|
||||
await auth.login()
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles?.length) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (!authStore.hasAnyRole(to.meta.roles)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Forbidden'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="page-grid">
|
||||
<UCard variant="soft">
|
||||
<template #header>
|
||||
<div class="stack">
|
||||
<p class="section-kicker">{{ t('home.hero.kicker') }}</p>
|
||||
<h2>{{ t('home.hero.title') }}</h2>
|
||||
<p>{{ t('home.hero.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="hero-actions">
|
||||
<UButton @click="cycleTheme">
|
||||
{{ t('home.hero.actions.theme') }}
|
||||
</UButton>
|
||||
<UButton color="neutral" variant="soft" @click="toggleLocale">
|
||||
{{ t('home.hero.actions.locale') }}
|
||||
</UButton>
|
||||
<UButton color="neutral" variant="outline" @click="writeLog">
|
||||
{{ t('home.hero.actions.log') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard variant="soft">
|
||||
<template #header>
|
||||
<div class="stack">
|
||||
<p class="section-kicker">{{ t('home.form.kicker') }}</p>
|
||||
<h2>{{ t('home.form.title') }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form class="stack" @submit.prevent="submitDemo">
|
||||
<UFormField :label="t('home.form.fields.name')">
|
||||
<UInput
|
||||
v-model="form.name"
|
||||
name="name"
|
||||
autocomplete="name"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField :label="t('home.form.fields.role')">
|
||||
<USelect
|
||||
v-model="form.role"
|
||||
:items="roleOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UButton type="submit">{{ t('home.form.submit') }}</UButton>
|
||||
</form>
|
||||
</UCard>
|
||||
|
||||
<UCard variant="soft">
|
||||
<template #header>
|
||||
<div class="stack">
|
||||
<p class="section-kicker">{{ t('home.status.kicker') }}</p>
|
||||
<h2>{{ t('home.status.title') }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="stack">
|
||||
<UAlert
|
||||
color="info"
|
||||
variant="soft"
|
||||
:title="t('home.status.theme')"
|
||||
:description="resolvedThemeLabel"
|
||||
/>
|
||||
<UAlert
|
||||
color="success"
|
||||
variant="soft"
|
||||
:title="t('home.status.locale')"
|
||||
:description="locale.toUpperCase()"
|
||||
/>
|
||||
<UAlert
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
:title="t('home.status.logger')"
|
||||
:description="t('home.status.loggerReady')"
|
||||
/>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard variant="soft">
|
||||
<template #header>
|
||||
<div class="stack">
|
||||
<p class="section-kicker">{{ t('home.components.kicker') }}</p>
|
||||
<h2>{{ t('home.components.title') }}</h2>
|
||||
<p>{{ t('home.components.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="component-showcase">
|
||||
<UButton size="sm">{{ t('home.components.buttons.small') }}</UButton>
|
||||
<UButton>{{ t('home.components.buttons.medium') }}</UButton>
|
||||
<UButton color="neutral" variant="soft">{{ t('home.components.buttons.secondary') }}</UButton>
|
||||
<UButton color="neutral" variant="outline">{{ t('home.components.buttons.ghost') }}</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { locale, setLocale, t } = useI18n()
|
||||
const logger = useLogger()
|
||||
const colorMode = useColorMode()
|
||||
const hydrated = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
role: 'developer'
|
||||
})
|
||||
|
||||
const roleOptions = computed(() => [
|
||||
{ label: t('roles.developer'), value: 'developer' },
|
||||
{ label: t('roles.designer'), value: 'designer' },
|
||||
{ label: t('roles.maintainer'), value: 'maintainer' }
|
||||
])
|
||||
|
||||
const resolvedThemeLabel = computed(() => {
|
||||
const safePreference = hydrated.value ? colorMode.preference : 'system'
|
||||
const preferenceText = t(`theme.preference.${safePreference}`)
|
||||
const resolvedMode = safePreference === 'system' ? 'system' : colorMode.value
|
||||
const resolvedText = t(`theme.resolved.${resolvedMode}`)
|
||||
|
||||
return `${preferenceText} / ${resolvedText}`
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
hydrated.value = true
|
||||
})
|
||||
|
||||
function toggleLocale() {
|
||||
const nextLocale = locale.value === 'sk' ? 'en' : 'sk'
|
||||
|
||||
setLocale(nextLocale)
|
||||
logger.info('Locale changed', { locale: nextLocale })
|
||||
}
|
||||
|
||||
function cycleTheme() {
|
||||
const nextTheme =
|
||||
colorMode.preference === 'light'
|
||||
? 'dark'
|
||||
: colorMode.preference === 'dark'
|
||||
? 'system'
|
||||
: 'light'
|
||||
|
||||
colorMode.preference = nextTheme
|
||||
|
||||
logger.info('Theme changed', { theme: nextTheme })
|
||||
}
|
||||
|
||||
function writeLog() {
|
||||
logger.debug('Template action executed', {
|
||||
locale: locale.value,
|
||||
theme: colorMode.value
|
||||
})
|
||||
}
|
||||
|
||||
function submitDemo() {
|
||||
logger.info('Demo form submitted', { ...form })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createExampleApi } from '~~/api/wrappers/example'
|
||||
import type { ApiClients } from '~/types/api'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const api: ApiClients = {
|
||||
example: createExampleApi({
|
||||
baseURL: runtimeConfig.public.apiBaseUrl,
|
||||
timeoutMs: runtimeConfig.public.apiTimeoutMs
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
provide: {
|
||||
api
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAuthStore, type AuthUser } from '~/stores/auth'
|
||||
import type { AuthClient, AuthMode } from '~/types/auth'
|
||||
|
||||
function normalizeAuthMode(value: string | undefined): AuthMode {
|
||||
if (value === 'mock' || value === 'userinfo') {
|
||||
return value
|
||||
}
|
||||
|
||||
return 'disabled'
|
||||
}
|
||||
|
||||
function createMockUser(): AuthUser {
|
||||
return {
|
||||
id: 'mock-user',
|
||||
email: 'mock@example.com',
|
||||
firstName: 'Mock',
|
||||
lastName: 'User',
|
||||
roles: ['USER']
|
||||
}
|
||||
}
|
||||
|
||||
function canUseWindow() {
|
||||
return typeof window !== 'undefined'
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const authStore = useAuthStore()
|
||||
const { isAuthenticated, isInitialized, user } = storeToRefs(authStore)
|
||||
|
||||
const mode = normalizeAuthMode(runtimeConfig.public.authMode)
|
||||
const ready = ref(false)
|
||||
|
||||
const initialize = async () => {
|
||||
if (ready.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'disabled') {
|
||||
authStore.initialize(null)
|
||||
ready.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'mock') {
|
||||
authStore.initialize(createMockUser())
|
||||
ready.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const authUser = await $fetch<AuthUser | null>(runtimeConfig.public.authUserinfoUrl, {
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
authStore.initialize(authUser)
|
||||
} catch {
|
||||
authStore.initialize(null)
|
||||
}
|
||||
|
||||
ready.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
isInitialized,
|
||||
(value) => {
|
||||
if (value) {
|
||||
ready.value = true
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const login = () => {
|
||||
if (mode === 'disabled') {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'mock') {
|
||||
authStore.setUser(createMockUser())
|
||||
return
|
||||
}
|
||||
|
||||
if (canUseWindow()) {
|
||||
window.location.href = runtimeConfig.public.authLoginUrl
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
authStore.clearSession()
|
||||
|
||||
if (mode === 'disabled') {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'mock') {
|
||||
return
|
||||
}
|
||||
|
||||
if (canUseWindow()) {
|
||||
window.location.href = runtimeConfig.public.authLogoutUrl
|
||||
}
|
||||
}
|
||||
|
||||
const auth: AuthClient = {
|
||||
mode,
|
||||
isEnabled: mode !== 'disabled',
|
||||
isReady: ready,
|
||||
isAuthenticated: computed(() => isAuthenticated.value),
|
||||
user,
|
||||
ensureInitialized: initialize,
|
||||
login,
|
||||
logout
|
||||
}
|
||||
|
||||
return {
|
||||
provide: {
|
||||
auth
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { LoggerApi, LoggerPayload, LogLevel } from '~/composables/useLogger'
|
||||
|
||||
function write(level: LogLevel, message: string, payload?: LoggerPayload) {
|
||||
const timestamp = new Date().toISOString()
|
||||
const prefix = `[template:${level}] ${timestamp} ${message}`
|
||||
|
||||
if (payload) {
|
||||
console[level](prefix, payload)
|
||||
return
|
||||
}
|
||||
|
||||
console[level](prefix)
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const logger: LoggerApi = {
|
||||
debug: (message, payload) => write('debug', message, payload),
|
||||
info: (message, payload) => write('info', message, payload),
|
||||
warn: (message, payload) => write('warn', message, payload),
|
||||
error: (message, payload) => write('error', message, payload)
|
||||
}
|
||||
|
||||
return {
|
||||
provide: {
|
||||
logger
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface AuthUser {
|
||||
id: string
|
||||
email?: string | null
|
||||
firstName?: string | null
|
||||
lastName?: string | null
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const isInitialized = ref(false)
|
||||
const user = ref<AuthUser | null>(null)
|
||||
|
||||
const roles = computed(() => user.value?.roles ?? [])
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
const displayName = computed(() => {
|
||||
if (!user.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const fullName = [user.value.firstName, user.value.lastName].filter(Boolean).join(' ').trim()
|
||||
|
||||
return fullName || user.value.email || user.value.id
|
||||
})
|
||||
|
||||
function initialize(nextUser: AuthUser | null = null) {
|
||||
user.value = nextUser
|
||||
isInitialized.value = true
|
||||
}
|
||||
|
||||
function setUser(nextUser: AuthUser) {
|
||||
user.value = nextUser
|
||||
isInitialized.value = true
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
user.value = null
|
||||
isInitialized.value = true
|
||||
}
|
||||
|
||||
function hasRole(role: string) {
|
||||
return roles.value.includes(role)
|
||||
}
|
||||
|
||||
function hasAnyRole(requiredRoles: string[]) {
|
||||
return requiredRoles.some((role) => roles.value.includes(role))
|
||||
}
|
||||
|
||||
return {
|
||||
displayName,
|
||||
hasAnyRole,
|
||||
hasRole,
|
||||
initialize,
|
||||
isAuthenticated,
|
||||
isInitialized,
|
||||
roles,
|
||||
setUser,
|
||||
clearSession,
|
||||
user
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export type UiNotificationTone = 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
export interface UiNotification {
|
||||
id: string
|
||||
title: string
|
||||
message?: string
|
||||
tone?: UiNotificationTone
|
||||
}
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const activeRequests = ref(0)
|
||||
const notifications = ref<UiNotification[]>([])
|
||||
|
||||
const isPageLoading = computed(() => activeRequests.value > 0)
|
||||
|
||||
function setLoading(value: boolean) {
|
||||
activeRequests.value = value ? 1 : 0
|
||||
}
|
||||
|
||||
function startLoading() {
|
||||
activeRequests.value += 1
|
||||
}
|
||||
|
||||
function stopLoading() {
|
||||
activeRequests.value = Math.max(0, activeRequests.value - 1)
|
||||
}
|
||||
|
||||
function pushNotification(notification: Omit<UiNotification, 'id'> & { id?: string }) {
|
||||
const nextNotification = {
|
||||
...notification,
|
||||
id: notification.id ?? crypto.randomUUID(),
|
||||
tone: notification.tone ?? 'info'
|
||||
} satisfies UiNotification
|
||||
|
||||
notifications.value = [...notifications.value, nextNotification]
|
||||
|
||||
return nextNotification.id
|
||||
}
|
||||
|
||||
function removeNotification(id: string) {
|
||||
notifications.value = notifications.value.filter((notification) => notification.id !== id)
|
||||
}
|
||||
|
||||
function clearNotifications() {
|
||||
notifications.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
clearNotifications,
|
||||
isPageLoading,
|
||||
notifications,
|
||||
pushNotification,
|
||||
removeNotification,
|
||||
setLoading,
|
||||
startLoading,
|
||||
stopLoading
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface ApiRequestOptions {
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export interface ExampleHealthResponse {
|
||||
status: 'ok'
|
||||
}
|
||||
|
||||
export interface ExampleWelcomeResponse {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ExampleApi {
|
||||
getHealth: (options?: ApiRequestOptions) => Promise<ExampleHealthResponse>
|
||||
getWelcome: (options?: ApiRequestOptions) => Promise<ExampleWelcomeResponse>
|
||||
}
|
||||
|
||||
export interface ApiClients {
|
||||
example: ExampleApi
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import type { AuthUser } from '~/stores/auth'
|
||||
|
||||
export type AuthMode = 'disabled' | 'mock' | 'userinfo'
|
||||
|
||||
export interface AuthClient {
|
||||
mode: AuthMode
|
||||
isEnabled: boolean
|
||||
isReady: Ref<boolean>
|
||||
isAuthenticated: ComputedRef<boolean>
|
||||
user: Ref<AuthUser | null>
|
||||
ensureInitialized: () => Promise<void>
|
||||
login: () => void | Promise<void>
|
||||
logout: () => void | Promise<void>
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
import type { LoggerApi } from '~/composables/useLogger'
|
||||
import type { AuthClient } from '~/types/auth'
|
||||
import type { ApiClients } from '~/types/api'
|
||||
|
||||
declare module '#app' {
|
||||
interface NuxtApp {
|
||||
$api: ApiClients
|
||||
$auth: AuthClient
|
||||
$logger: LoggerApi
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$api: ApiClients
|
||||
$auth: AuthClient
|
||||
$logger: LoggerApi
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
public?: boolean
|
||||
requiresAuth?: boolean
|
||||
roles?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
Reference in New Issue
Block a user