Initial commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.vscode
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
NUXT_PUBLIC_API_BASE_URL=/api
|
||||||
|
NUXT_PUBLIC_API_TIMEOUT_MS=10000
|
||||||
|
NUXT_PUBLIC_AUTH_MODE=disabled
|
||||||
|
NUXT_PUBLIC_AUTH_LOGIN_URL=/login
|
||||||
|
NUXT_PUBLIC_AUTH_LOGOUT_URL=/logout
|
||||||
|
NUXT_PUBLIC_AUTH_USERINFO_URL=/api/auth/me
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.cache
|
||||||
|
.pnpm-store
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.openapi-generator
|
||||||
|
openapi-client/**
|
||||||
|
!openapi-client/
|
||||||
|
!openapi-client/README.md
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"Vue.vscode-typescript-vue-plugin",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"Nuxt.mdc",
|
||||||
|
"lokalise.i18n-ally",
|
||||||
|
"vitest.explorer"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Nuxt: dev",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"request": "launch",
|
||||||
|
"command": "pnpm dev"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+72
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.nuxt": true,
|
||||||
|
"**/.output": true,
|
||||||
|
"**/.data": true,
|
||||||
|
"**/.cache": true,
|
||||||
|
"**/.pnpm-store": true,
|
||||||
|
"**/coverage": true,
|
||||||
|
"**/dist": true,
|
||||||
|
"**/node_modules": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.nuxt": true,
|
||||||
|
"**/.output": true,
|
||||||
|
"**/.data": true,
|
||||||
|
"**/.cache": true,
|
||||||
|
"**/.pnpm-store": true,
|
||||||
|
"**/coverage": true,
|
||||||
|
"**/dist": true,
|
||||||
|
"**/node_modules": true
|
||||||
|
},
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/.git/**": true,
|
||||||
|
"**/.nuxt/**": true,
|
||||||
|
"**/.output/**": true,
|
||||||
|
"**/.data/**": true,
|
||||||
|
"**/.cache/**": true,
|
||||||
|
"**/.pnpm-store/**": true,
|
||||||
|
"**/coverage/**": true,
|
||||||
|
"**/dist/**": true,
|
||||||
|
"**/node_modules/**": true
|
||||||
|
},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"js/ts.tsdk.path": "node_modules/typescript/lib",
|
||||||
|
"vue.server.hybridMode": false,
|
||||||
|
"json.schemaDownload.trustedDomains": {
|
||||||
|
"https://raw.githubusercontent.com/DavidAnson/markdownlint/": true
|
||||||
|
},
|
||||||
|
"i18n-ally.enabledFrameworks": [
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"i18n/locales"
|
||||||
|
],
|
||||||
|
"i18n-ally.pathMatcher": "{locale}.json",
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.namespace": false,
|
||||||
|
"i18n-ally.sourceLanguage": "sk",
|
||||||
|
"i18n-ally.displayLanguage": "sk",
|
||||||
|
"todo-tree.filtering.excludeGlobs": [
|
||||||
|
"**/.git/**",
|
||||||
|
"**/.nuxt/**",
|
||||||
|
"**/.output/**",
|
||||||
|
"**/.data/**",
|
||||||
|
"**/.cache/**",
|
||||||
|
"**/.pnpm-store/**",
|
||||||
|
"**/coverage/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"files.eol": "\n"
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
FROM node:24-bookworm-slim AS build
|
||||||
|
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/nodejs24-debian12:nonroot AS runtime
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NITRO_HOST=0.0.0.0
|
||||||
|
ENV NITRO_PORT=3000
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build --chown=65532:65532 /app/.output ./.output
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD [".output/server/index.mjs"]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type {
|
||||||
|
ApiRequestOptions,
|
||||||
|
ExampleApi,
|
||||||
|
ExampleHealthResponse,
|
||||||
|
ExampleWelcomeResponse
|
||||||
|
} from '~/types/api'
|
||||||
|
|
||||||
|
interface CreateExampleApiOptions {
|
||||||
|
baseURL: string
|
||||||
|
timeoutMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExampleApi(options: CreateExampleApiOptions): ExampleApi {
|
||||||
|
const request = async <T>(path: string, requestOptions?: ApiRequestOptions) => {
|
||||||
|
return await $fetch<T>(path, {
|
||||||
|
baseURL: options.baseURL,
|
||||||
|
signal: requestOptions?.signal,
|
||||||
|
timeout: options.timeoutMs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getHealth(options?: ApiRequestOptions): Promise<ExampleHealthResponse> {
|
||||||
|
return request<ExampleHealthResponse>('/health', options)
|
||||||
|
},
|
||||||
|
getWelcome(options?: ApiRequestOptions): Promise<ExampleWelcomeResponse> {
|
||||||
|
return request<ExampleWelcomeResponse>('/welcome', options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt([
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'padding-line-between-statements': [
|
||||||
|
'error',
|
||||||
|
{ blankLine: 'always', prev: 'import', next: '*' },
|
||||||
|
{ blankLine: 'any', prev: 'import', next: 'import' }
|
||||||
|
],
|
||||||
|
'vue/block-order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
order: ['template', 'script', 'style']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"badge": "Nuxt first template",
|
||||||
|
"title": "Starter for VS Code development",
|
||||||
|
"subtitle": "Template for local host development with i18n, logging, optional auth, and Nuxt UI components."
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"switch": "Locale"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"toggle": "Theme",
|
||||||
|
"preference": {
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"resolved": {
|
||||||
|
"light": "light",
|
||||||
|
"dark": "dark",
|
||||||
|
"system": "system"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"developer": "Developer",
|
||||||
|
"designer": "Designer",
|
||||||
|
"maintainer": "Maintainer"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"hero": {
|
||||||
|
"kicker": "Hello world",
|
||||||
|
"title": "The project is ready for extension",
|
||||||
|
"description": "Use this baseline for dashboards, portals, and admin consoles.",
|
||||||
|
"actions": {
|
||||||
|
"theme": "Cycle theme",
|
||||||
|
"locale": "Switch locale",
|
||||||
|
"log": "Write log"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"kicker": "Demo form",
|
||||||
|
"title": "Nuxt UI components are ready",
|
||||||
|
"fields": {
|
||||||
|
"name": "Name",
|
||||||
|
"role": "Role"
|
||||||
|
},
|
||||||
|
"submit": "Submit"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"kicker": "Environment status",
|
||||||
|
"title": "Runtime overview",
|
||||||
|
"theme": "Active theme",
|
||||||
|
"locale": "Active locale",
|
||||||
|
"logger": "Logger",
|
||||||
|
"loggerReady": "The logger plugin is ready for client-side runtime usage."
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"kicker": "Nuxt UI",
|
||||||
|
"title": "Standard UI layer for further development",
|
||||||
|
"description": "The template uses Nuxt UI as the default foundation for forms, buttons, cards, and alerts.",
|
||||||
|
"buttons": {
|
||||||
|
"small": "Small",
|
||||||
|
"medium": "Medium",
|
||||||
|
"secondary": "Secondary",
|
||||||
|
"ghost": "Ghost"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"badge": "Nuxt first template",
|
||||||
|
"title": "Zaklad pre vyvoj vo VS Code",
|
||||||
|
"subtitle": "Template pre lokalny vyvoj na hoste s i18n, loggerom, volitelnou autorizaciou a Nuxt UI komponentmi."
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"switch": "Jazyk"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"toggle": "Tema",
|
||||||
|
"preference": {
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"resolved": {
|
||||||
|
"light": "svetla",
|
||||||
|
"dark": "tmava",
|
||||||
|
"system": "podla systemu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"developer": "Vyvojar",
|
||||||
|
"designer": "Dizajner",
|
||||||
|
"maintainer": "Spravca"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"hero": {
|
||||||
|
"kicker": "Hello world",
|
||||||
|
"title": "Projekt je pripraveny na dalsie rozsiranie",
|
||||||
|
"description": "Pouzi tento zaklad pre dashboardy, portalove aplikacie aj administracne konzoly.",
|
||||||
|
"actions": {
|
||||||
|
"theme": "Zmenit temu",
|
||||||
|
"locale": "Prepnut jazyk",
|
||||||
|
"log": "Zapisat log"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"kicker": "Ukazkovy formular",
|
||||||
|
"title": "Nuxt UI komponenty su pripravene",
|
||||||
|
"fields": {
|
||||||
|
"name": "Meno",
|
||||||
|
"role": "Rola"
|
||||||
|
},
|
||||||
|
"submit": "Odoslat"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"kicker": "Stav prostredia",
|
||||||
|
"title": "Runtime prehlad",
|
||||||
|
"theme": "Aktivna tema",
|
||||||
|
"locale": "Aktivny jazyk",
|
||||||
|
"logger": "Logger",
|
||||||
|
"loggerReady": "Logger plugin je pripraveny pre klientsky runtime."
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"kicker": "Nuxt UI",
|
||||||
|
"title": "Standardna UI vrstva pre dalsi vyvoj",
|
||||||
|
"description": "Template pouziva Nuxt UI ako predvoleny zaklad pre formulare, tlacidla, karty a alerty.",
|
||||||
|
"buttons": {
|
||||||
|
"small": "Male",
|
||||||
|
"medium": "Stredne",
|
||||||
|
"secondary": "Secondary",
|
||||||
|
"ghost": "Ghost"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-10-23',
|
||||||
|
devtools: { enabled: process.env.NUXT_DEVTOOLS === 'true' },
|
||||||
|
|
||||||
|
modules: ['@nuxt/eslint', '@pinia/nuxt', '@nuxt/ui', '@nuxtjs/i18n'],
|
||||||
|
|
||||||
|
appConfig: {
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: 'blue',
|
||||||
|
neutral: 'slate'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
css: ['~/assets/styles/ui.css', '~/assets/styles/main.scss'],
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
title: 'Nuxt Workspace Template',
|
||||||
|
titleTemplate: '%s | Nuxt Workspace Template',
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
content: 'Nuxt-first template for local VS Code development on the host machine.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL ?? '/api',
|
||||||
|
apiTimeoutMs: Number.parseInt(process.env.NUXT_PUBLIC_API_TIMEOUT_MS ?? '10000', 10),
|
||||||
|
authMode: process.env.NUXT_PUBLIC_AUTH_MODE ?? 'disabled',
|
||||||
|
authLoginUrl: process.env.NUXT_PUBLIC_AUTH_LOGIN_URL ?? '/login',
|
||||||
|
authLogoutUrl: process.env.NUXT_PUBLIC_AUTH_LOGOUT_URL ?? '/logout',
|
||||||
|
authUserinfoUrl: process.env.NUXT_PUBLIC_AUTH_USERINFO_URL ?? '/api/auth/me'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'sk',
|
||||||
|
strategy: 'no_prefix',
|
||||||
|
locales: [
|
||||||
|
{ code: 'sk', name: 'Slovencina', file: 'sk.json' },
|
||||||
|
{ code: 'en', name: 'English', file: 'en.json' }
|
||||||
|
],
|
||||||
|
langDir: 'locales',
|
||||||
|
detectBrowserLanguage: false
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# OpenAPI Client
|
||||||
|
|
||||||
|
Do tohto adresara sa generuju klienti z OpenAPI specifikacii.
|
||||||
|
|
||||||
|
V template sa generuju cez `@hey-api/openapi-ts`.
|
||||||
|
|
||||||
|
Predvolene sa negituju rucne pisane zmeny do generovanych suborov. Ak potrebujes dodat custom spravanie, daj ho do `api/wrappers` alebo do `app/composables`.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# OpenAPI Spec
|
||||||
|
|
||||||
|
Do tohto adresara patria OpenAPI specifikacie alebo generator configy pre backend kontrakty.
|
||||||
|
|
||||||
|
Odporucany workflow:
|
||||||
|
|
||||||
|
1. uloz OpenAPI subor alebo odkaz na repo so specifikaciou
|
||||||
|
2. priprav `openapi-ts.config.ts`
|
||||||
|
3. spusti `pnpm generate:api`
|
||||||
|
4. v `api/wrappers` vytvor tenku vrstvu nad vygenerovanym klientom
|
||||||
|
|
||||||
|
Aktualny codegen pouziva `@hey-api/openapi-ts`, takze v dev containery nie je potrebna Java.
|
||||||
|
|
||||||
|
Komponenty a pages by nemali volat generovany klient priamo. Preferuj composables alebo wrappery, aby sa dalo API jednoducho menit bez zasahu do UI.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Example API
|
||||||
|
version: 1.0.0
|
||||||
|
servers:
|
||||||
|
- url: /api
|
||||||
|
paths:
|
||||||
|
/health:
|
||||||
|
get:
|
||||||
|
operationId: getHealth
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: API health status
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ok
|
||||||
|
/welcome:
|
||||||
|
get:
|
||||||
|
operationId: getWelcome
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Welcome message
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from '@hey-api/openapi-ts'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
input: './openapi-spec/example-api.yaml',
|
||||||
|
output: './openapi-client/example'
|
||||||
|
})
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "nuxt-workspace-template",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@10.33.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.12.0 || >=24"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "nuxt prepare && eslint .",
|
||||||
|
"lint:fix": "nuxt prepare && eslint . --fix",
|
||||||
|
"typecheck": "nuxt prepare && nuxt typecheck",
|
||||||
|
"test": "vitest --passWithNoTests",
|
||||||
|
"test:run": "vitest run --passWithNoTests",
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write .",
|
||||||
|
"generate:api": "pnpm generate:example-api",
|
||||||
|
"generate:example-api": "rimraf openapi-client/example && openapi-ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/ui": "^4.6.0",
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
"nuxt": "4.4.2",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "3.5.31",
|
||||||
|
"vue-router": "5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@hey-api/openapi-ts": "0.94.5",
|
||||||
|
"@types/node": "^24.5.2",
|
||||||
|
"@iconify-json/lucide": "^1.2.70",
|
||||||
|
"@nuxt/eslint": "1.15.2",
|
||||||
|
"@nuxtjs/i18n": "10.2.4",
|
||||||
|
"@vue/test-utils": "2.4.6",
|
||||||
|
"eslint": "9.39.1",
|
||||||
|
"prettier": "3.7.4",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
|
"sass": "1.98.0",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"typescript": "5.9.3",
|
||||||
|
"vitest": "4.1.2",
|
||||||
|
"vue-i18n": "11.3.0",
|
||||||
|
"vue-tsc": "3.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+12035
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,978 @@
|
|||||||
|
# Programming Guide
|
||||||
|
|
||||||
|
Tento dokument sluzi ako prakticky sprievodca template-om. Pri kazdej casti popisuje:
|
||||||
|
|
||||||
|
- co template poskytuje
|
||||||
|
- ci sa dana cast musi pouzit
|
||||||
|
- ako sa pouziva
|
||||||
|
- kratky priklad
|
||||||
|
- ako sa da rozsirit
|
||||||
|
|
||||||
|
Poznamka:
|
||||||
|
|
||||||
|
- vsetko v tomto template je navrhnute tak, aby sa dalo pouzit postupne
|
||||||
|
- projekt nemusis prijat ako jeden velky balik
|
||||||
|
- niektore casti su povinne len technicky, nie architektonicky
|
||||||
|
|
||||||
|
## Prehlad
|
||||||
|
|
||||||
|
Template poskytuje tieto hlavne casti:
|
||||||
|
|
||||||
|
- Nuxt 4 aplikaciu
|
||||||
|
- TypeScript
|
||||||
|
- Nuxt UI ako predvolenu UI vrstvu
|
||||||
|
- i18n pre `sk` a `en`
|
||||||
|
- Pinia store-y
|
||||||
|
- volitelnu auth kostru
|
||||||
|
- API vrstvu pripravenu na OpenAPI
|
||||||
|
- logger composable a plugin
|
||||||
|
- globalne SCSS styly a tokeny
|
||||||
|
- VS Code konfiguraciu
|
||||||
|
- lint, format, typecheck a test skripty
|
||||||
|
- Vitest test harness pre store-y, pluginy, middleware a locale konzistenciu
|
||||||
|
|
||||||
|
## Nuxt 4 Aplikacia
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- zakladnu strukturu Nuxt 4 projektu
|
||||||
|
- `app/` adresar pre pages, layouts, plugins, stores a composables
|
||||||
|
- `nuxt.config.ts` pre centralnu konfiguraciu
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- ano
|
||||||
|
- je to jadro celeho template-u
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- vytvaraj routy v `app/pages`
|
||||||
|
- vytvaraj spolocne layouty v `app/layouts`
|
||||||
|
- pridavaj runtime logiku cez `app/plugins` a `app/composables`
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- app/pages/about.vue -->
|
||||||
|
<template>
|
||||||
|
<div>About page</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- pridat dalsie Nuxt moduly do `nuxt.config.ts`
|
||||||
|
- doplnit server routes do `server/routes`
|
||||||
|
- doplnit middleware, plugins a composables podla potrieb projektu
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- typovanie pre komponenty, composables, stores a plugin injections
|
||||||
|
- centralne rozsirene Nuxt typy v `app/types`
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- prakticky ano
|
||||||
|
- template je na TypeScripte postaveny
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- zapisuj nove composables a utility v `.ts`
|
||||||
|
- definuj domenove typy do `app/types`
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface Customer {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- pridat dalsie domenove typy
|
||||||
|
- rozsirit `RouteMeta` alebo `NuxtApp` injekcie
|
||||||
|
- doplnit typy pre generovane OpenAPI klienty
|
||||||
|
|
||||||
|
Odporucanie:
|
||||||
|
|
||||||
|
- pri rozsireni `NuxtApp` injekcii alebo aliasov udrzuj v synchronizacii aj `vitest.config.ts`, aby sa dali nove pluginy testovat mimo Nuxt runtime
|
||||||
|
|
||||||
|
## Nuxt UI
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- standardnu komponentovu vrstvu pre formularove a bezne aplikacne UI
|
||||||
|
- komponenty ako `UApp`, `UButton`, `UCard`, `UInput`, `USelect`, `UFormField`, `UAlert`
|
||||||
|
- integrovany color mode a konzistentne API komponentov
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je to predvolena UI vrstva template-u
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- v page alebo komponente pouzi `U*` komponenty priamo
|
||||||
|
- nevytvaraj dalsiu vlastnu vrstvu zakladnych button/input/card komponentov, ak na to nie je silny dovod
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UCard variant="soft">
|
||||||
|
<template #header>
|
||||||
|
<h2>Profil</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UFormField label="Meno">
|
||||||
|
<UInput v-model="name" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton>Ulozit</UButton>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const name = ref('')
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- vytvarat vlastne feature komponenty nad Nuxt UI
|
||||||
|
- doplnit komplexne custom komponenty, napriklad diagramy alebo editory
|
||||||
|
- zmenit vzhlad cez app config, CSS a globalne styly
|
||||||
|
|
||||||
|
Odporucany pristup:
|
||||||
|
|
||||||
|
- bezne app UI nech riesi Nuxt UI
|
||||||
|
- specializovane vizualizacie nech riesia vlastne Vue komponenty alebo specializovane kniznice
|
||||||
|
|
||||||
|
## i18n
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- lokalizaciu pre `sk` a `en`
|
||||||
|
- preklady v `i18n/locales/sk.json` a `i18n/locales/en.json`
|
||||||
|
- default locale `sk`
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je uz zapojene a pripravene
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- texty zapisuj do locale JSON suborov
|
||||||
|
- v komponente pouzi `useI18n()`
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { t } = useI18n()
|
||||||
|
const title = t('app.title')
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "Moja aplikacia"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- pridat dalsie jazyky do `nuxt.config.ts`
|
||||||
|
- doplnit dalsie namespace v locale JSON suboroch
|
||||||
|
- zaviest konvencie pre preklady podla modulov, napriklad `users.list.title`
|
||||||
|
|
||||||
|
Automaticke overenie:
|
||||||
|
|
||||||
|
- template obsahuje test, ktory porovnava strukturu klucov v `i18n/locales/sk.json` a `i18n/locales/en.json`
|
||||||
|
- pri pridani noveho textu ho dopln do oboch suborov, inak test zlyha
|
||||||
|
|
||||||
|
## Pinia
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- centralny stav aplikacie
|
||||||
|
- pripraveny `auth` store v `app/stores/auth.ts`
|
||||||
|
- pripraveny `ui` store v `app/stores/ui.ts`
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je odporucany pre zdielany stav
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- lokalny stav formulara drz v komponente
|
||||||
|
- globalny alebo zdielany stav drz v store
|
||||||
|
|
||||||
|
Priklad auth store:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
if (authStore.isAuthenticated) {
|
||||||
|
console.log(authStore.displayName)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad ui store:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
|
||||||
|
uiStore.startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
uiStore.pushNotification({
|
||||||
|
title: 'Hotovo',
|
||||||
|
message: 'Data boli ulozene.',
|
||||||
|
tone: 'success'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
uiStore.stopLoading()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- pridat dalsie store-y, napriklad `filters`, `settings`, `drafts`
|
||||||
|
- napojit `ui` store na globalne toasty alebo loading overlay
|
||||||
|
- rozsirit `auth` store o session info alebo permission model
|
||||||
|
|
||||||
|
Automaticke overenie:
|
||||||
|
|
||||||
|
- existuju unit testy pre `auth` aj `ui` store, takze zmeny v ich API alebo vypocitavanych hodnotach vies chytit skor, nez sa dostanu do UI
|
||||||
|
|
||||||
|
## Autorizacia A Auth Kostra
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- vendor-neutral auth vrstvu
|
||||||
|
- `useAuth()` composable
|
||||||
|
- auth plugin v `app/plugins/auth.ts`
|
||||||
|
- globalny middleware v `app/middleware/auth.global.ts`
|
||||||
|
- route meta typy `public`, `requiresAuth`, `roles`
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- auth je volitelna
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- nastav mod cez `.env`
|
||||||
|
- route ochran len tam, kde to dava zmysel
|
||||||
|
|
||||||
|
Dostupne mody:
|
||||||
|
|
||||||
|
- `disabled`
|
||||||
|
- `mock`
|
||||||
|
- `userinfo`
|
||||||
|
|
||||||
|
Priklad `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NUXT_PUBLIC_AUTH_MODE=mock
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad v komponente:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const auth = useAuth()
|
||||||
|
|
||||||
|
await auth.ensureInitialized()
|
||||||
|
|
||||||
|
if (auth.isAuthenticated.value) {
|
||||||
|
console.log(auth.user.value?.email)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad ochrany route:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
definePageMeta({
|
||||||
|
requiresAuth: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad ochrany podla role:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
definePageMeta({
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['ADMIN']
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- napojit `userinfo` mod na realny backend alebo oauth2-proxy endpoint
|
||||||
|
- doplnit mapovanie backend user modelu
|
||||||
|
- pridat jemnejsi access model typu `permissions` alebo `hasAccess(user)`
|
||||||
|
- doplnit login alebo logout UI do layoutu
|
||||||
|
|
||||||
|
Automaticke overenie:
|
||||||
|
|
||||||
|
- auth plugin ma testy pre `disabled`, `mock` a `userinfo`
|
||||||
|
- middleware ma testy pre `public`, `requiresAuth` a `roles`
|
||||||
|
- to znamena, ze zmeny v auth kostre vies overit bez browser manualu
|
||||||
|
|
||||||
|
## API Vrstva
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- centralnu API konfiguraciu v `nuxt.config.ts`
|
||||||
|
- API plugin v `app/plugins/api.client.ts`
|
||||||
|
- `useApi()` composable
|
||||||
|
- wrapper vrstvu v `api/wrappers`
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je to odporucany sposob volania backendu
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- z komponentov volaj `useApi()`
|
||||||
|
- konkretne HTTP detaily drz vo wrapperoch
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const health = await api.example.getHealth()
|
||||||
|
const welcome = await api.example.getWelcome()
|
||||||
|
```
|
||||||
|
|
||||||
|
Zakladne env:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=/api
|
||||||
|
NUXT_PUBLIC_API_TIMEOUT_MS=10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- pridat dalsie API wrappery do `api/wrappers`
|
||||||
|
- rozdelit klientov podla domen, napriklad `users`, `projects`, `reports`
|
||||||
|
- doplnit error handling, retry alebo auth headers
|
||||||
|
|
||||||
|
Odporucanie:
|
||||||
|
|
||||||
|
- UI nema volat `fetch` napriamo, ak to nie je jednorazova drobnost
|
||||||
|
- preferuj centralnu API vrstvu pre konzistenciu
|
||||||
|
|
||||||
|
Automaticke overenie:
|
||||||
|
|
||||||
|
- ukazkovy wrapper `api/wrappers/example.ts` aj plugin `app/plugins/api.client.ts` maju Vitest testy
|
||||||
|
- pri pridani dalsieho wrappera je dobry standard doplnit rovnaky test hned s implementaciou
|
||||||
|
|
||||||
|
## OpenAPI Priprava
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- `openapi-spec` pre specifikacie a generator configy
|
||||||
|
- `openapi-client` pre generovanych klientov
|
||||||
|
- `openapi-ts.config.ts`
|
||||||
|
- skripty `pnpm generate:api` a `pnpm generate:example-api`
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- je to priprava pre projekty, kde FE alebo BE kontrakt existuje ako OpenAPI
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
1. vloz alebo nahrad OpenAPI specifikaciu v `openapi-spec`
|
||||||
|
2. uprav `openapi-ts.config.ts`
|
||||||
|
3. spusti generator
|
||||||
|
4. napoj vysledok cez `api/wrappers`
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm generate:api
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- generovat viac klientov pre viac backendov
|
||||||
|
- verzovat alebo neverzovat generovany kod podla timovej dohody
|
||||||
|
- doplnit wrappery, ktore skryju rozdiely medzi backendmi
|
||||||
|
|
||||||
|
Odporucanie:
|
||||||
|
|
||||||
|
- negenerovany kod nech ostane v `api/wrappers` a `app/composables`
|
||||||
|
- komponenty nech nevolaju generovany OpenAPI klient priamo
|
||||||
|
- generator je Node-native, takze workflow funguje aj bez Javy
|
||||||
|
|
||||||
|
## Logger
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- jednoduchy logger plugin
|
||||||
|
- `useLogger()` composable
|
||||||
|
- dostupnost cez Nuxt inject
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const logger = useLogger()
|
||||||
|
|
||||||
|
logger.info('User opened page', { section: 'dashboard' })
|
||||||
|
logger.error('Save failed', { id: '123' })
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- napojit logovanie na externe sluzby
|
||||||
|
- doplnit odlisne spravanie pre dev a production
|
||||||
|
- pridat korelacne ID alebo request context
|
||||||
|
|
||||||
|
Automaticke overenie:
|
||||||
|
|
||||||
|
- logger plugin ma test na format vystupu
|
||||||
|
- `useLogger()` ma test na spravne Nuxt inject spravanie
|
||||||
|
|
||||||
|
## Testovanie A Overenie
|
||||||
|
|
||||||
|
Co sa overuje automaticky:
|
||||||
|
|
||||||
|
- store-y
|
||||||
|
- pluginy
|
||||||
|
- middleware
|
||||||
|
- API wrappery
|
||||||
|
- i18n kluce
|
||||||
|
|
||||||
|
Zakladny verify workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm lint
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm test:run
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
Cielene spustenie infrastructure testov template-u:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm vitest run tests/auth.plugin.spec.ts tests/auth.middleware.spec.ts tests/i18n.locales.spec.ts tests/api.plugin.spec.ts tests/logger.plugin.spec.ts tests/use-logger.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Poznamka:
|
||||||
|
|
||||||
|
- testy pre pluginy a middleware pouzivaju `vitest.config.ts`, kde su nastavene aliasy `~`, `@`, `~~` a `@@`
|
||||||
|
- ak pridas nove runtime injekcie alebo novu rozsirenu Nuxt vrstvu, je dobre doplnit test este v tom istom PR
|
||||||
|
|
||||||
|
## Globalne Styly A Tokeny
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- `app/assets/styles/ui.css`
|
||||||
|
- `app/assets/styles/_tokens.scss`
|
||||||
|
- `app/assets/styles/_theme.scss`
|
||||||
|
- `app/assets/styles/_base.scss`
|
||||||
|
- `app/assets/styles/main.scss`
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- ciastocne ano
|
||||||
|
- stylova vrstva je sucast template-u, ale mozes ju zmensit alebo vymenit
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- `ui.css` nechaj ako vstup pre `tailwindcss` a `@nuxt/ui`
|
||||||
|
- layoutove a globalne upravy rob v `_base.scss`
|
||||||
|
- Nuxt UI farby skus najprv nastavit v `nuxt.config.ts` cez `appConfig.ui.colors`
|
||||||
|
- `_theme.scss` pouzi len na male override-y nad Nuxt UI tokenmi
|
||||||
|
- zakladne app-level hodnoty drz v `_tokens.scss`
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
appConfig: {
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: 'blue',
|
||||||
|
neutral: 'slate'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- doplnit branding projektu
|
||||||
|
- upravit typografiu, spacing a farby
|
||||||
|
- v pripade potreby jemne prepisat Nuxt UI tokeny bez budovania vlastneho design systemu
|
||||||
|
|
||||||
|
Odporucany mentalny model:
|
||||||
|
|
||||||
|
- Nuxt UI je zdroj pravdy pre komponentovy vizual
|
||||||
|
- `_tokens.scss` drzi male spolocne hodnoty pre app shell
|
||||||
|
- `_theme.scss` riesi rozdiel medzi light a dark vizualom
|
||||||
|
- `_base.scss` riesi vlastne layout triedy a app-specific CSS
|
||||||
|
|
||||||
|
### Co menit v ktorom subore
|
||||||
|
|
||||||
|
`nuxt.config.ts`
|
||||||
|
|
||||||
|
- semanticke farby pre Nuxt UI ako prvy krok
|
||||||
|
- ine Nuxt-level nastavenia
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
appConfig: {
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: 'blue',
|
||||||
|
neutral: 'slate'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/assets/styles/ui.css`
|
||||||
|
|
||||||
|
- nechaj len import Nuxt UI vrstvy
|
||||||
|
- tento subor nie je miesto na app-specific styling
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@nuxt/ui";
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/assets/styles/_tokens.scss`
|
||||||
|
|
||||||
|
- radius
|
||||||
|
- font family
|
||||||
|
- layout max width
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root {
|
||||||
|
--font-family-base: 'Segoe UI', sans-serif;
|
||||||
|
--page-max-width: 1320px;
|
||||||
|
--ui-radius: 1rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/assets/styles/_theme.scss`
|
||||||
|
|
||||||
|
- jemne prepisy Nuxt UI CSS tokenov
|
||||||
|
- light/dark rozdiely bez stavania vlastnej komponentovej kniznice
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root,
|
||||||
|
.light {
|
||||||
|
--ui-primary: var(--ui-color-primary-600);
|
||||||
|
--ui-bg: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--ui-primary: var(--ui-color-primary-400);
|
||||||
|
--ui-bg: #0b1220;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/assets/styles/_base.scss`
|
||||||
|
|
||||||
|
- vlastne wrapper triedy
|
||||||
|
- spacing a layout pre shell
|
||||||
|
- app-specific utility a responzivne pravidla
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.page-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ako funguje light, dark a system tema
|
||||||
|
|
||||||
|
Template pouziva `useColorMode()` a prepinac v `app/layouts/default.vue`.
|
||||||
|
|
||||||
|
Pouzivane hodnoty:
|
||||||
|
|
||||||
|
- `light`
|
||||||
|
- `dark`
|
||||||
|
- `system`
|
||||||
|
|
||||||
|
Priklad vlastneho prepinaca:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
function cycleTheme() {
|
||||||
|
colorMode.preference =
|
||||||
|
colorMode.preference === 'light'
|
||||||
|
? 'dark'
|
||||||
|
: colorMode.preference === 'dark'
|
||||||
|
? 'system'
|
||||||
|
: 'light'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UButton @click="cycleTheme">
|
||||||
|
Zmenit temu
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Kedy menit `preference` a kedy CSS premenne:
|
||||||
|
|
||||||
|
- `colorMode.preference` meni aktivny mod
|
||||||
|
- CSS premenne v `_theme.scss` menia, ako light a dark mod vyzera
|
||||||
|
|
||||||
|
### Typicke scenare uprav
|
||||||
|
|
||||||
|
1. Chces iny primary button color.
|
||||||
|
Najprv skus `appConfig.ui.colors` v `nuxt.config.ts`, a ak chces presnejsi vysledok, dolad `--ui-primary` v `_theme.scss`.
|
||||||
|
2. Chces tmavsi dark background a jemnejsie card borders.
|
||||||
|
Uprav `--ui-bg`, `--ui-bg-muted` a `--ui-border` v `_theme.scss`.
|
||||||
|
3. Chces vacsi radius pre buttony, inputs a cards.
|
||||||
|
Zmen `--ui-radius` v `_tokens.scss`.
|
||||||
|
4. Chces sirsi layout alebo iny spacing hero sekcie.
|
||||||
|
Uprav `_base.scss`.
|
||||||
|
|
||||||
|
### Priklady praktickych uprav
|
||||||
|
|
||||||
|
Zmena radiusu pre celu app:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root {
|
||||||
|
--ui-radius: 1rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Jemnejsi light mode:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root,
|
||||||
|
.light {
|
||||||
|
--ui-bg: #f8fafc;
|
||||||
|
--ui-bg-muted: #f1f5f9;
|
||||||
|
--ui-border: #dbe4ee;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Vyraznejsi dark mode:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.dark {
|
||||||
|
--ui-bg: #0b1220;
|
||||||
|
--ui-bg-muted: #111827;
|
||||||
|
--ui-border: #1f2937;
|
||||||
|
--ui-text: #e5eef9;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Silnejsia primarna farba v light mode a jemnejsia v dark mode:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root {
|
||||||
|
--ui-primary: var(--ui-color-primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--ui-primary: var(--ui-color-primary-400);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Release Image
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- [Dockerfile](/c:/Data/Development/MANET/prjtemplates/vue/Dockerfile)
|
||||||
|
- [.dockerignore](/c:/Data/Development/MANET/prjtemplates/vue/.dockerignore)
|
||||||
|
- produkcny image pre Nitro server
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je to odporucana cesta pre release a deploy
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- buildni image cez `docker build`
|
||||||
|
- spusti kontajner s namapovanym portom `3000`
|
||||||
|
- runtime premenne dodaj cez `-e`
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t nuxt-workspace-template:latest .
|
||||||
|
docker run --rm -p 3000:3000 nuxt-workspace-template:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad s runtime premennymi:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker run --rm -p 3000:3000 `
|
||||||
|
-e NUXT_PUBLIC_API_BASE_URL=https://api.example.com `
|
||||||
|
-e NUXT_PUBLIC_AUTH_MODE=userinfo `
|
||||||
|
nuxt-workspace-template:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Poznamky:
|
||||||
|
|
||||||
|
- build stage pouziva `node:24-bookworm-slim`
|
||||||
|
- runtime stage pouziva hardened `gcr.io/distroless/nodejs24-debian12:nonroot`
|
||||||
|
- kontajner spusta `.output/server/index.mjs`
|
||||||
|
- image je urceny pre release alebo deploy, nie pre hot-reload development
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- spolocny shell aplikacie v `app/layouts/default.vue`
|
||||||
|
- branding, prepinanie locale a prepinanie temy
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je to predvoleny layout
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- nechaj sem spolocne prvky aplikacie
|
||||||
|
- stranky sa do neho renderuju cez `<slot />`
|
||||||
|
|
||||||
|
Priklad rozsirenia:
|
||||||
|
|
||||||
|
- pridat navigation bar
|
||||||
|
- pridat user menu
|
||||||
|
- pridat globalne notifikacie alebo loading indikator
|
||||||
|
|
||||||
|
## Demo Stranka
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- ukazkovu domovsku stranku v `app/pages/index.vue`
|
||||||
|
- priklad pouzitia i18n, loggera, Nuxt UI a theme switchingu
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- je to iba demo start point
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- nahrad ju vlastnym dashboardom alebo landing page
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- prerobit na prehlad systemu
|
||||||
|
- pouzit ju ako interny style guide pre tim
|
||||||
|
- rozdelit ukazky do samostatnych routes
|
||||||
|
|
||||||
|
## VS Code
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- `.vscode` konfiguraciu
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je to odporucany editorovy setup pre konzistentny workflow
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
- otvor repo vo VS Code
|
||||||
|
- spusti `pnpm dev`
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- pridat dalsie VS Code extensions
|
||||||
|
- upravit Node verziu na hoste
|
||||||
|
- pridat lokalne helper sluzby podla potrieb projektu
|
||||||
|
|
||||||
|
## Kvalita Kodu
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- ESLint
|
||||||
|
- Prettier
|
||||||
|
- Typecheck
|
||||||
|
- Vitest
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- technicky nie
|
||||||
|
- prakticky ano, ak chces udrzatelny projekt
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm lint
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm test:run
|
||||||
|
pnpm format
|
||||||
|
```
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- doplnit unit testy
|
||||||
|
- doplnit component testy
|
||||||
|
- zapojit CI pipeline
|
||||||
|
- pridat pre-commit hooks
|
||||||
|
|
||||||
|
## Runtime Config
|
||||||
|
|
||||||
|
Co poskytuje:
|
||||||
|
|
||||||
|
- centralne public config hodnoty v `nuxt.config.ts`
|
||||||
|
- API a auth env premenne
|
||||||
|
|
||||||
|
Musi sa pouzit:
|
||||||
|
|
||||||
|
- nie
|
||||||
|
- ale je to odporucany sposob konfiguracie
|
||||||
|
|
||||||
|
Ako pouzit:
|
||||||
|
|
||||||
|
Priklad `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=/api
|
||||||
|
NUXT_PUBLIC_API_TIMEOUT_MS=10000
|
||||||
|
NUXT_PUBLIC_AUTH_MODE=disabled
|
||||||
|
NUXT_PUBLIC_AUTH_USERINFO_URL=/api/auth/me
|
||||||
|
```
|
||||||
|
|
||||||
|
Prakticky start:
|
||||||
|
|
||||||
|
1. skopiruj `.env.example` do `.env`
|
||||||
|
2. nastav `NUXT_PUBLIC_API_BASE_URL`
|
||||||
|
3. rozhodni `NUXT_PUBLIC_AUTH_MODE`
|
||||||
|
4. ak budes pouzivat `userinfo`, dopln aj login, logout a userinfo URL
|
||||||
|
|
||||||
|
Poznamka:
|
||||||
|
|
||||||
|
- public runtime config patri do `.env`, nie natvrdo do komponentov
|
||||||
|
|
||||||
|
Moznosti rozsirenia:
|
||||||
|
|
||||||
|
- pridat dalsie public a private env premenne
|
||||||
|
- rozdelit config podla modulov
|
||||||
|
- doplnit feature flags
|
||||||
|
|
||||||
|
## Co Je Povinny Zaklad A Co Je Volitelne
|
||||||
|
|
||||||
|
Povinny zaklad:
|
||||||
|
|
||||||
|
- Nuxt 4
|
||||||
|
- TypeScript
|
||||||
|
- zakladna adresarova struktura
|
||||||
|
- `nuxt.config.ts`
|
||||||
|
|
||||||
|
Silno odporucane, ale volitelne:
|
||||||
|
|
||||||
|
- Nuxt UI
|
||||||
|
- i18n
|
||||||
|
- Pinia
|
||||||
|
- API vrstva
|
||||||
|
- logger
|
||||||
|
- globalne styly
|
||||||
|
|
||||||
|
Volitelne casti:
|
||||||
|
|
||||||
|
- auth kostra
|
||||||
|
- OpenAPI generator workflow
|
||||||
|
- lokalny host workflow nad VS Code
|
||||||
|
- demo stranka v aktualnej podobe
|
||||||
|
|
||||||
|
## Odporucany Sposob Pouzitia Template-u
|
||||||
|
|
||||||
|
Ak chces z template-u spravit realny projekt, odporucany postup je:
|
||||||
|
|
||||||
|
1. uprav branding, title a preklady
|
||||||
|
2. rozhodni, ci pouzijes auth a v akom mode
|
||||||
|
3. nastav API base URL a priprav prve wrappery
|
||||||
|
4. ak mate OpenAPI kontrakt, nahraj specifikaciu a nastav generator
|
||||||
|
5. zacni stavat stranky priamo nad Nuxt UI komponentmi
|
||||||
|
6. zdielany stav presuvaj do Pinia store-ov
|
||||||
|
7. pred odovzdanim pravidelne pustaj lint, typecheck a testy
|
||||||
|
|
||||||
|
Ako spravit prvy krok prakticky:
|
||||||
|
|
||||||
|
- v `package.json` zmen `name`
|
||||||
|
- v `nuxt.config.ts` uprav `app.head.title`, `titleTemplate` a `description`
|
||||||
|
- v `i18n/locales/sk.json` a `i18n/locales/en.json` prepis demo texty
|
||||||
|
- v `app/pages/index.vue` nahrad ukazkovu domovsku stranku vlastnym obsahom
|
||||||
|
- v `nuxt.config.ts`, `app/assets/styles/_tokens.scss`, `_theme.scss` a `_base.scss` dolad semanticke farby, spacing a globalny vizual
|
||||||
|
|
||||||
|
## Odporucany Sposob Rozsirovania
|
||||||
|
|
||||||
|
Najrozumnejsie miesto pre rozsirovanie podla typu zmeny:
|
||||||
|
|
||||||
|
- nova route alebo obrazovka:
|
||||||
|
`app/pages`
|
||||||
|
- nova zdielana UI cast alebo zlozitejsi widget:
|
||||||
|
`app/components`
|
||||||
|
- nova zdielana logika:
|
||||||
|
`app/composables`
|
||||||
|
- novy globalny stav:
|
||||||
|
`app/stores`
|
||||||
|
- nove backend volania:
|
||||||
|
`api/wrappers`
|
||||||
|
- nova OpenAPI specifikacia:
|
||||||
|
`openapi-spec`
|
||||||
|
- nova plugin integracia:
|
||||||
|
`app/plugins`
|
||||||
|
- nove typy:
|
||||||
|
`app/types`
|
||||||
|
|
||||||
|
Priklad rozsirania o novu feature:
|
||||||
|
|
||||||
|
1. vytvor `app/pages/users/list.vue`
|
||||||
|
2. pridaj `api/wrappers/users.ts`
|
||||||
|
3. dopln typy do `app/types/api.ts` alebo pouzi generovany klient
|
||||||
|
4. ak treba, pridaj `app/stores/users.ts`
|
||||||
|
5. texty dopln do `i18n/locales/*.json`
|
||||||
|
|
||||||
|
## Zaverecne Odporucanie
|
||||||
|
|
||||||
|
Tento template nie je monolit, ktory musis pouzit cely. Je to pripraveny zaklad, z ktoreho si mozes zobrat:
|
||||||
|
|
||||||
|
- len Nuxt UI a i18n
|
||||||
|
- len API a OpenAPI workflow
|
||||||
|
- len auth skeleton
|
||||||
|
- alebo kompletne vsetky vrstvy spolu
|
||||||
|
|
||||||
|
Najlepsie funguje vtedy, ked:
|
||||||
|
|
||||||
|
- bezne UI stavas priamo s Nuxt UI
|
||||||
|
- backend komunikaciu drzis v wrapperoch
|
||||||
|
- zdielany stav davas do Pinie
|
||||||
|
- auth zapinas len tam, kde ho naozaj potrebujes
|
||||||
+293
@@ -0,0 +1,293 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
Tento dokument je rychly start po vytvoreni projektu z template.
|
||||||
|
|
||||||
|
Ak nechces citat celu dokumentaciu, chod podla tychto krokov.
|
||||||
|
|
||||||
|
## 1. Spusti Projekt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack enable
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Poznamka:
|
||||||
|
|
||||||
|
- projekt ocakava Node `^22.12.0 || >=24`
|
||||||
|
|
||||||
|
Ak chces rovno pripravit release image namiesto lokalneho dev servera:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t nuxt-workspace-template:latest .
|
||||||
|
docker run --rm -p 3000:3000 nuxt-workspace-template:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Potom otvor `http://localhost:3000`.
|
||||||
|
|
||||||
|
Poznamka:
|
||||||
|
|
||||||
|
- runtime image je distroless Node 24 image urceny pre release a deploy
|
||||||
|
|
||||||
|
## Co Spravit Hned Po Spusteni
|
||||||
|
|
||||||
|
Po tom, co aplikacia bezi, urob aspon tieto zakladne kroky:
|
||||||
|
|
||||||
|
1. vytvor `.env` z `.env.example` a nastav minimalne API a auth hodnoty
|
||||||
|
2. uprav branding v `package.json`, `nuxt.config.ts` a textoch v `i18n/locales`
|
||||||
|
3. rozhodni auth mod: `disabled`, `mock` alebo `userinfo`
|
||||||
|
4. rozhodni, ci pojdes cez vlastne wrappery v `api/wrappers`, alebo cez OpenAPI codegen
|
||||||
|
5. nahrad demo obsah v `app/pages/index.vue` prvou realnou obrazovkou alebo dashboardom
|
||||||
|
6. skontroluj globalne styly v `app/assets/styles`
|
||||||
|
7. pred vacsou zmenou pust `pnpm lint`, `pnpm typecheck`, `pnpm test:run` a `pnpm build`
|
||||||
|
|
||||||
|
Kde to najdes podrobne:
|
||||||
|
|
||||||
|
- `.env` a runtime config: nizsie v tomto dokumente a v `programing_guide.md`
|
||||||
|
- branding a preklady: sekcia `Uprav Branding`
|
||||||
|
- auth: sekcia `Rozhodni Sa O Auth`
|
||||||
|
- API a OpenAPI: sekcia `Rozhodni Sa O API Integracii`
|
||||||
|
- prva feature: sekcia `Vytvor Prvu Feature`
|
||||||
|
|
||||||
|
## 2. Vytvor `.env`
|
||||||
|
|
||||||
|
Skopiruj `.env.example` do `.env` a uprav hodnoty podla projektu.
|
||||||
|
|
||||||
|
Minimalny start:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NUXT_PUBLIC_API_BASE_URL=/api
|
||||||
|
NUXT_PUBLIC_API_TIMEOUT_MS=10000
|
||||||
|
NUXT_PUBLIC_AUTH_MODE=disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak chces lokalny mock auth:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NUXT_PUBLIC_AUTH_MODE=mock
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Uprav Branding
|
||||||
|
|
||||||
|
Najcastejsie prve zmeny:
|
||||||
|
|
||||||
|
- nazov projektu v `package.json`
|
||||||
|
- title a meta v `nuxt.config.ts`
|
||||||
|
- texty v `i18n/locales/sk.json`
|
||||||
|
- texty v `i18n/locales/en.json`
|
||||||
|
- obsah domovskej stranky v `app/pages/index.vue`
|
||||||
|
|
||||||
|
Odporucanie:
|
||||||
|
|
||||||
|
- nenechaj template texty v produkcnom projekte
|
||||||
|
|
||||||
|
## 4. Rozhodni Sa O Auth
|
||||||
|
|
||||||
|
Template podporuje volitelny auth skeleton.
|
||||||
|
|
||||||
|
Moznosti:
|
||||||
|
|
||||||
|
- `disabled`
|
||||||
|
pouzi, ak aplikacia auth nepotrebuje
|
||||||
|
- `mock`
|
||||||
|
pouzi pri lokalnom vyvoji bez realneho prihlasenia
|
||||||
|
- `userinfo`
|
||||||
|
pouzi, ak backend alebo proxy vracia user info endpoint
|
||||||
|
|
||||||
|
Priklad ochrany route:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
definePageMeta({
|
||||||
|
requiresAuth: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad s rolou:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
definePageMeta({
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['ADMIN']
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Rozhodni Sa O API Integracii
|
||||||
|
|
||||||
|
Mas 2 hlavne cesty:
|
||||||
|
|
||||||
|
- bez OpenAPI
|
||||||
|
pouzivaj `useApi()` a doplnaj vlastne wrappery do `api/wrappers`
|
||||||
|
- s OpenAPI
|
||||||
|
nahraj specifikaciu do `openapi-spec` a vygeneruj klienta
|
||||||
|
|
||||||
|
Priklad pouzitia API:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const api = useApi()
|
||||||
|
const health = await api.example.getHealth()
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak mas OpenAPI:
|
||||||
|
|
||||||
|
1. nahrad `openapi-spec/example-api.yaml`
|
||||||
|
2. uprav `openapi-ts.config.ts`
|
||||||
|
3. spusti:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm generate:api
|
||||||
|
```
|
||||||
|
|
||||||
|
Poznamka:
|
||||||
|
|
||||||
|
- generator bezi cez `@hey-api/openapi-ts`, takze netreba Javu
|
||||||
|
|
||||||
|
## 6. Stavaj UI S Nuxt UI
|
||||||
|
|
||||||
|
Template pouziva Nuxt UI ako predvolenu komponentovu vrstvu.
|
||||||
|
|
||||||
|
Pouzivaj najma:
|
||||||
|
|
||||||
|
- `UButton`
|
||||||
|
- `UCard`
|
||||||
|
- `UInput`
|
||||||
|
- `USelect`
|
||||||
|
- `UFormField`
|
||||||
|
- `UAlert`
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h2>Nova stranka</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UFormField label="Nazov">
|
||||||
|
<UInput v-model="name" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton>Ulozit</UButton>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const name = ref('')
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Odporucanie:
|
||||||
|
|
||||||
|
- nebuduj novu vrstvu `BaseButton`, `BaseInput`, `BaseCard`, ak na to nie je silny dovod
|
||||||
|
|
||||||
|
## 7. Pouzivaj Pinia Len Na Zdielany Stav
|
||||||
|
|
||||||
|
Pripravene store-y:
|
||||||
|
|
||||||
|
- `useAuthStore()`
|
||||||
|
- `useUiStore()`
|
||||||
|
|
||||||
|
Pouzivaj ich na:
|
||||||
|
|
||||||
|
- user/session stav
|
||||||
|
- loading
|
||||||
|
- notifikacie
|
||||||
|
|
||||||
|
Nepouzivaj ich na:
|
||||||
|
|
||||||
|
- kazdy maly lokalny input
|
||||||
|
- docasny stav jedneho formulara, ak ho nepotrebuje viac casti appky
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
|
||||||
|
uiStore.pushNotification({
|
||||||
|
title: 'Ulozene',
|
||||||
|
message: 'Operacia prebehla uspesne.',
|
||||||
|
tone: 'success'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Vytvor Prvu Feature
|
||||||
|
|
||||||
|
Odporucany postup pri novej feature:
|
||||||
|
|
||||||
|
1. vytvor route v `app/pages`
|
||||||
|
2. vytvor alebo dopln API wrapper v `api/wrappers`
|
||||||
|
3. ak treba, vytvor store v `app/stores`
|
||||||
|
4. dopln preklady do `i18n/locales`
|
||||||
|
5. postav UI pomocou Nuxt UI
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
- `app/pages/users/list.vue`
|
||||||
|
- `api/wrappers/users.ts`
|
||||||
|
- `app/stores/users.ts`
|
||||||
|
|
||||||
|
## 9. Over Kvalitu
|
||||||
|
|
||||||
|
Pred vacsou zmenou alebo commitom pustaj:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm lint
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm test:run
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak menis formatovanie:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm format:fix
|
||||||
|
```
|
||||||
|
|
||||||
|
Template uz obsahuje automaticke testy pre:
|
||||||
|
|
||||||
|
- `auth` a `ui` store-y
|
||||||
|
- auth plugin a auth middleware
|
||||||
|
- API wrapper a API plugin
|
||||||
|
- logger plugin a `useLogger()`
|
||||||
|
- konzistenciu i18n klucov
|
||||||
|
|
||||||
|
Ak chces rychly smoke check len pre testovaciu kostru template-u, pouzi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm vitest run tests/auth.plugin.spec.ts tests/auth.middleware.spec.ts tests/i18n.locales.spec.ts tests/api.plugin.spec.ts tests/logger.plugin.spec.ts tests/use-logger.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Poznamka:
|
||||||
|
|
||||||
|
- Vitest aliasy pre Nuxt importy su nastavene vo `vitest.config.ts`
|
||||||
|
|
||||||
|
## 10. Co Zmenit Ako Prve V Realnom Projekte
|
||||||
|
|
||||||
|
Najcastejsie prve upravy:
|
||||||
|
|
||||||
|
- branding a preklady
|
||||||
|
- title a meta udaje
|
||||||
|
- `.env` konfiguracia
|
||||||
|
- auth mod
|
||||||
|
- API base URL
|
||||||
|
- vlastna domovska stranka
|
||||||
|
- prvy realny API wrapper
|
||||||
|
- prva chranena route, ak treba auth
|
||||||
|
|
||||||
|
## Odporucane Minimalne Rozhodnutia Na Start
|
||||||
|
|
||||||
|
Predtym, nez zacnes feature vyvoj, rozhodni aspon toto:
|
||||||
|
|
||||||
|
1. bude auth `disabled`, `mock` alebo `userinfo`?
|
||||||
|
2. bude FE/BE kontrakt rieseny cez OpenAPI?
|
||||||
|
3. budete generovat klientov do `openapi-client`?
|
||||||
|
4. budete drzat globalny stav v Pinia store-och?
|
||||||
|
5. zostavate pri Nuxt UI ako hlavnej UI vrstve?
|
||||||
|
|
||||||
|
## Ked Chces Ist Dalej
|
||||||
|
|
||||||
|
Podrobnejsi popis najdes v:
|
||||||
|
|
||||||
|
- `readme.md`
|
||||||
|
- `programing_guide.md`
|
||||||
|
- `openapi-spec/README.md`
|
||||||
@@ -0,0 +1,700 @@
|
|||||||
|
# Nuxt Workspace Template
|
||||||
|
|
||||||
|
Nuxt 4 template pre lokalny vyvoj vo VS Code na hoste. Repozitar obsahuje zakladnu aplikaciu s i18n, Nuxt UI, jednoduchym loggerom, volitelnou auth kostrou, OpenAPI-ready API vrstvou a predpripravenym workspace nastavenim pre lokalny vyvoj.
|
||||||
|
|
||||||
|
Dalsia dokumentacia:
|
||||||
|
|
||||||
|
- `quick_start.md`: rychly postup po vytvoreni projektu z template
|
||||||
|
- `programing_guide.md`: podrobny prehlad toho, co template poskytuje, ako sa to pouziva a ako sa to da rozsirovat
|
||||||
|
|
||||||
|
## Co je pripravene
|
||||||
|
|
||||||
|
- Nuxt `4.4.2`
|
||||||
|
- TypeScript `5.9.3`
|
||||||
|
- `pnpm` ako package manager
|
||||||
|
- `@nuxtjs/i18n` s jazykmi `sk` a `en`
|
||||||
|
- light, dark a system tema cez Nuxt color mode
|
||||||
|
- klientsky logger dostupny cez composable
|
||||||
|
- volitelna auth kostra s modmi `disabled`, `mock` a `userinfo`
|
||||||
|
- OpenAPI-ready API vrstva s generator skriptmi a wrappermi
|
||||||
|
- Nuxt UI ako predvolena komponentova vrstva
|
||||||
|
- `.vscode` konfiguracia pre vyvoj vo VS Code
|
||||||
|
- ESLint, Prettier a Vitest
|
||||||
|
|
||||||
|
## Pre koho je template
|
||||||
|
|
||||||
|
Template je vhodny ako vychodiskovy bod pre:
|
||||||
|
|
||||||
|
- dashboardy a interne aplikacie
|
||||||
|
- portalove aplikacie
|
||||||
|
- administracne rozhrania
|
||||||
|
- mensie a stredne Nuxt projekty, kde chces zacat rychlo, ale nie od nuly
|
||||||
|
|
||||||
|
## Struktura projektu
|
||||||
|
|
||||||
|
Najdolezitejsie casti repozitara:
|
||||||
|
|
||||||
|
- `app/pages`:
|
||||||
|
routovatelne stranky aplikacie
|
||||||
|
- `app/layouts`:
|
||||||
|
spolocny layout aplikacie
|
||||||
|
- `app/components`:
|
||||||
|
miesto pre vlastne feature alebo komplexne komponenty nad Nuxt UI
|
||||||
|
- `app/composables`:
|
||||||
|
zdielana klientska logika, napr. tema a logger
|
||||||
|
- `app/plugins`:
|
||||||
|
Nuxt pluginy dostupne v runtime
|
||||||
|
- `tests`:
|
||||||
|
Vitest testy pre store-y, pluginy, middleware a i18n konzistenciu
|
||||||
|
- `app/assets/styles`:
|
||||||
|
globalne SCSS styly, tokeny a tema
|
||||||
|
- `i18n/locales`:
|
||||||
|
preklady pre jednotlive jazyky
|
||||||
|
- `.vscode`:
|
||||||
|
odporucane rozsirrenia, settings a debug konfiguracia
|
||||||
|
- `nuxt.config.ts`:
|
||||||
|
hlavna konfiguracia Nuxtu, modulov, globalnych stylov a i18n
|
||||||
|
- `vitest.config.ts`:
|
||||||
|
Vitest aliasy pre `~`, `@`, `~~` a `@@`, aby sa dali testovat Nuxt subory mimo runtime
|
||||||
|
|
||||||
|
## Ako pouzit tento template
|
||||||
|
|
||||||
|
### 1. Vytvor projekt z template
|
||||||
|
|
||||||
|
Pouzi tento repozitar ako zaklad a uprav si aspon:
|
||||||
|
|
||||||
|
- nazov projektu v `package.json`
|
||||||
|
- titulok, meta a popis v `nuxt.config.ts`
|
||||||
|
- texty v `i18n/locales/sk.json` a `i18n/locales/en.json`
|
||||||
|
- uvodnu stranku v `app/pages/index.vue`
|
||||||
|
- vizualny styl v `app/assets/styles`
|
||||||
|
|
||||||
|
### 2. Nastav prostredie
|
||||||
|
|
||||||
|
Skopiruj `.env.example` do `.env` a dopln vlastne hodnoty podla potreby.
|
||||||
|
|
||||||
|
Priklady:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spustenie projektu
|
||||||
|
|
||||||
|
### Lokalny vyvoj na hoste
|
||||||
|
|
||||||
|
Pozadovane je:
|
||||||
|
|
||||||
|
- Node.js `^22.12.0 || >=24`
|
||||||
|
- `corepack`
|
||||||
|
|
||||||
|
Spustenie:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack enable
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Potom otvor `http://localhost:3000`.
|
||||||
|
|
||||||
|
Poznamky:
|
||||||
|
|
||||||
|
- pri prvom starte moze Nuxt v dev rezime buildit pomalsie
|
||||||
|
- ak je port `3000` obsadeny, Nuxt pouzije iny port, typicky `3001`
|
||||||
|
- po otvoreni stranky sa moze prvy Vite build spustit az na prvy request
|
||||||
|
|
||||||
|
### Produkcny Docker image
|
||||||
|
|
||||||
|
Template obsahuje pripraveny:
|
||||||
|
|
||||||
|
- [Dockerfile](/c:/Data/Development/MANET/prjtemplates/vue/Dockerfile)
|
||||||
|
- [.dockerignore](/c:/Data/Development/MANET/prjtemplates/vue/.dockerignore)
|
||||||
|
|
||||||
|
Build image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t nuxt-workspace-template:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Spustenie kontajnera:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -p 3000:3000 nuxt-workspace-template:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Kontajner spusti hotovy Nitro server z `.output/server/index.mjs` a aplikacia bude dostupna na `http://localhost:3000`.
|
||||||
|
|
||||||
|
Ak potrebujes odovzdat runtime premenne:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker run --rm -p 3000:3000 `
|
||||||
|
-e NUXT_PUBLIC_API_BASE_URL=https://api.example.com `
|
||||||
|
-e NUXT_PUBLIC_AUTH_MODE=userinfo `
|
||||||
|
nuxt-workspace-template:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Poznamky:
|
||||||
|
|
||||||
|
- image je urceny pre release a deploy, nie pre hot-reload vyvoj
|
||||||
|
- build stage pouziva `pnpm install --frozen-lockfile` a `pnpm build`
|
||||||
|
- runtime stage pouziva hardened distroless Node 24 image a publikuje port `3000`
|
||||||
|
- runtime image neobsahuje shell ani balast navyse, co pomaha znizit attack surface
|
||||||
|
|
||||||
|
## Zakladne skripty
|
||||||
|
|
||||||
|
- `pnpm dev`: spusti vyvojovy server
|
||||||
|
- `pnpm build`: vytvori produkcny build
|
||||||
|
- `pnpm preview`: spusti preview produkcneho buildu
|
||||||
|
- `pnpm generate:api`: spusti pripraveny OpenAPI codegen workflow
|
||||||
|
- `pnpm generate:example-api`: vygeneruje ukazkoveho klienta do `openapi-client/example` cez `@hey-api/openapi-ts`
|
||||||
|
- `pnpm lint`: spusti ESLint
|
||||||
|
- `pnpm lint:fix`: opravi cast lint problemov automaticky
|
||||||
|
- `pnpm typecheck`: spusti Nuxt typecheck
|
||||||
|
- `pnpm test`: spusti Vitest v beznom rezime
|
||||||
|
- `pnpm test:run`: spusti Vitest jednorazovo
|
||||||
|
- `pnpm format`: skontroluje formatovanie cez Prettier
|
||||||
|
- `pnpm format:fix`: naformatuje subory cez Prettier
|
||||||
|
|
||||||
|
## Automaticke Overenie
|
||||||
|
|
||||||
|
Template ma pripraveny automaticky test coverage pre najdolezitejsie stavebne casti:
|
||||||
|
|
||||||
|
- store-y `auth` a `ui`
|
||||||
|
- auth plugin pre mody `disabled`, `mock` a `userinfo`
|
||||||
|
- auth middleware pre `public`, `requiresAuth` a `roles`
|
||||||
|
- API wrapper a API plugin
|
||||||
|
- logger plugin a `useLogger()`
|
||||||
|
- konzistenciu i18n klucov medzi `sk` a `en`
|
||||||
|
|
||||||
|
Odporucany minimalny verify postup pred vacsou zmenou:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm lint
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm test:run
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak chces spustit len novejsie infrastrukturalne testy template-u:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm vitest run tests/auth.plugin.spec.ts tests/auth.middleware.spec.ts tests/i18n.locales.spec.ts tests/api.plugin.spec.ts tests/logger.plugin.spec.ts tests/use-logger.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ako v tomto template vyvijat
|
||||||
|
|
||||||
|
### Kde upravovat stranky
|
||||||
|
|
||||||
|
Kazdy subor v `app/pages` reprezentuje route.
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
- `app/pages/index.vue`: domovska stranka `/`
|
||||||
|
|
||||||
|
Ak chces pridat novu stranku, vytvor novy `.vue` subor v `app/pages`.
|
||||||
|
|
||||||
|
### Kde upravovat spolocny layout
|
||||||
|
|
||||||
|
Hlavny layout aplikacie je v:
|
||||||
|
|
||||||
|
- `app/layouts/default.vue`
|
||||||
|
|
||||||
|
Tu sa nachadza spolocna hlavicka, prepinac jazyka a prepinac temy. Vsetky stranky renderovane cez `NuxtPage` sa zobrazia v tomto layoute.
|
||||||
|
|
||||||
|
Layout je postaveny na Nuxt UI komponentoch, najma `UBadge` a `USelect`. Korektne obalenie aplikacie cez `UApp` je v `app/app.vue`.
|
||||||
|
|
||||||
|
### Kde upravovat preklady
|
||||||
|
|
||||||
|
Preklady su v:
|
||||||
|
|
||||||
|
- `i18n/locales/sk.json`
|
||||||
|
- `i18n/locales/en.json`
|
||||||
|
|
||||||
|
Projekt pouziva `@nuxtjs/i18n` s jazykmi:
|
||||||
|
|
||||||
|
- `sk` ako default
|
||||||
|
- `en` ako druhy jazyk
|
||||||
|
|
||||||
|
Ak pridas novy text do komponentu, dopln ho do oboch JSON suborov.
|
||||||
|
|
||||||
|
### Kde upravovat temu a vizual
|
||||||
|
|
||||||
|
Globalne styly su nacitane cez `nuxt.config.ts` z:
|
||||||
|
|
||||||
|
- `app/assets/styles/ui.css`
|
||||||
|
- `app/assets/styles/main.scss`
|
||||||
|
|
||||||
|
Subory stylov:
|
||||||
|
|
||||||
|
- `app/assets/styles/ui.css`: nacitanie Tailwind CSS a Nuxt UI vrstvy
|
||||||
|
- `app/assets/styles/_tokens.scss`: lahke app-level tokeny, napr. font, radius a layout rozmery
|
||||||
|
- `app/assets/styles/_theme.scss`: volitelne theme override-y nad Nuxt UI tokenmi
|
||||||
|
- `app/assets/styles/_base.scss`: globalne layout a app-specific styly
|
||||||
|
|
||||||
|
Theme runtime logika je v:
|
||||||
|
|
||||||
|
- Nuxt UI a `useColorMode()`
|
||||||
|
|
||||||
|
Template rozlisuje:
|
||||||
|
|
||||||
|
- preferenciu temy: `light`, `dark`, `system`
|
||||||
|
- vyslednu aktivnu temu: `light` alebo `dark`
|
||||||
|
|
||||||
|
Odporucany pristup je nemenit cely design system rucne, ale nechat Nuxt UI ako zdroj pravdy a upravovat len:
|
||||||
|
|
||||||
|
- `nuxt.config.ts` cez `appConfig.ui.colors` ako prvy krok pre semanticke farby ako `primary` a `neutral`
|
||||||
|
- `_theme.scss` pre male vizualne doladenia nad Nuxt UI tokenmi
|
||||||
|
|
||||||
|
Prakticky to znamena:
|
||||||
|
|
||||||
|
- v `ui.css` nechaj iba `@import "tailwindcss"` a `@import "@nuxt/ui"`
|
||||||
|
- v `nuxt.config.ts` zacni semantickymi farbami a inymi Nuxt nastaveniami
|
||||||
|
- v `_tokens.scss` men spolocne hodnoty ako `--ui-radius`, font alebo layout sirku
|
||||||
|
- v `_theme.scss` men light/dark rozdiely cez Nuxt UI CSS premenne
|
||||||
|
- v `_base.scss` rob app-level layout a globalne utility pre vlastne bloky ako `.shell`, `.page-grid` a podobne
|
||||||
|
|
||||||
|
### Ako funguje prepnutie temy
|
||||||
|
|
||||||
|
Runtime prepnutie temy je postavene na `useColorMode()` a prepinaci v `app/layouts/default.vue`.
|
||||||
|
|
||||||
|
Aktualna preferencia moze byt:
|
||||||
|
|
||||||
|
- `light`
|
||||||
|
- `dark`
|
||||||
|
- `system`
|
||||||
|
|
||||||
|
Pouzitie v komponente:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
colorMode.preference = 'dark'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak chces spravit vlastne tlacidlo na prepnutie temy:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UButton @click="toggleTheme">
|
||||||
|
Prepnut temu
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ako menit vizual bez vlastneho design systemu
|
||||||
|
|
||||||
|
Najcastejsie scenare:
|
||||||
|
|
||||||
|
- chces inu primarnu farbu buttonov a aktivnych prvkov:
|
||||||
|
najprv skus `appConfig.ui.colors` v `nuxt.config.ts`
|
||||||
|
- chces jemnejsie texty, tmavsie pozadie alebo iny border v dark mode:
|
||||||
|
zmen Nuxt UI CSS premenne v `_theme.scss`
|
||||||
|
- chces iny radius alebo sirsu app shell:
|
||||||
|
zmen hodnoty v `_tokens.scss`
|
||||||
|
- chces iny layout alebo spacing vlastnych blokov:
|
||||||
|
uprav `_base.scss`
|
||||||
|
|
||||||
|
Priklad zmeny radiusu a layoutu:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root {
|
||||||
|
--ui-radius: 1rem;
|
||||||
|
--page-max-width: 1320px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad jemneho doladenia backgroundu a borderov:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root,
|
||||||
|
.light {
|
||||||
|
--ui-bg: #f8fafc;
|
||||||
|
--ui-bg-muted: #f1f5f9;
|
||||||
|
--ui-border: #dbe4ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--ui-bg: #0b1220;
|
||||||
|
--ui-bg-muted: #111827;
|
||||||
|
--ui-border: #1f2937;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Priklad zmeny intenzity primarnej farby medzi light a dark modom:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
:root {
|
||||||
|
--ui-primary: var(--ui-color-primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--ui-primary: var(--ui-color-primary-400);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To je vhodne hlavne vtedy, ked chces nechat Nuxt UI komponenty a ich logiku bez zmeny, ale potrebujes jemne upravit kontrast alebo ton farby.
|
||||||
|
|
||||||
|
### Priklad konfiguracie v tomto template
|
||||||
|
|
||||||
|
Aktualny template drzi semanticke farby v `nuxt.config.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
appConfig: {
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: 'blue',
|
||||||
|
neutral: 'slate'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Tento zapis hovori Nuxt UI, aku paletu ma ma preferovat pre:
|
||||||
|
|
||||||
|
- `color="primary"`
|
||||||
|
- `color="neutral"`
|
||||||
|
- utility ako `text-primary`, `bg-primary`, `text-muted`, `bg-default` a podobne
|
||||||
|
|
||||||
|
Ak by si pri konkretnej verzii Nuxt UI potreboval jemnejsiu kontrolu nad realnym vysledkom, najspolahlivejsia cesta je doplnit CSS tokeny v `_theme.scss`, napr. `--ui-primary`, `--ui-bg` alebo `--ui-border`.
|
||||||
|
|
||||||
|
### Kde upravovat komponenty
|
||||||
|
|
||||||
|
Template pouziva standardne Nuxt UI komponenty ako predvolenu UI vrstvu.
|
||||||
|
|
||||||
|
Najcastejsie pouzite v template:
|
||||||
|
|
||||||
|
- `UButton`
|
||||||
|
- `UCard`
|
||||||
|
- `UInput`
|
||||||
|
- `USelect`
|
||||||
|
- `UFormField`
|
||||||
|
- `UAlert`
|
||||||
|
- `UBadge`
|
||||||
|
|
||||||
|
Pouzitie v komponente:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<UCard variant="soft">
|
||||||
|
<template #header>
|
||||||
|
<h2>Nadpis</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UFormField label="Meno">
|
||||||
|
<UInput v-model="name" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton>Ulozit</UButton>
|
||||||
|
</UCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak budes chciet rozsirit UI o vlastne komplexne komponenty, odporucany postup je skladat ich nad Nuxt UI, nie vytvarat paralelnu mini kniznicu zakladnych button/input/card komponentov.
|
||||||
|
|
||||||
|
### Kde upravovat logger
|
||||||
|
|
||||||
|
Logger je pripraveny ako Nuxt plugin a composable:
|
||||||
|
|
||||||
|
- `app/plugins/logger.ts`
|
||||||
|
- `app/composables/useLogger.ts`
|
||||||
|
|
||||||
|
Pouzitie v komponente:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const logger = useLogger()
|
||||||
|
|
||||||
|
logger.info('Akcia vykonana', { section: 'home' })
|
||||||
|
```
|
||||||
|
|
||||||
|
Logy z klienta idu do konzoly prehliadaca.
|
||||||
|
|
||||||
|
Logger ma aj automaticke testy, ktore overuju prefix, timestamp a spravanie s payloadom aj bez neho.
|
||||||
|
|
||||||
|
### Kde upravovat API a OpenAPI kontrakt
|
||||||
|
|
||||||
|
Zaklad API vrstvy je v:
|
||||||
|
|
||||||
|
- `app/plugins/api.client.ts`
|
||||||
|
- `app/composables/useApi.ts`
|
||||||
|
- `app/types/api.ts`
|
||||||
|
- `api/wrappers`
|
||||||
|
|
||||||
|
OpenAPI priprava je v:
|
||||||
|
|
||||||
|
- `openapi-spec`
|
||||||
|
- `openapi-client`
|
||||||
|
- `openapi-ts.config.ts`
|
||||||
|
|
||||||
|
Template funguje aj bez generovaneho klienta. Predvoleny wrapper `api/wrappers/example.ts` pouziva bezny `fetch` styl cez `$fetch`, ale ma rovnaky tvar ako vrstva, ktoru vies neskor napojit na vygenerovany OpenAPI client.
|
||||||
|
|
||||||
|
API plugin a ukazkovy wrapper maju samostatne Vitest testy, takze zmeny v `runtimeConfig` alebo wrapper rozhrani vies zachytit automaticky.
|
||||||
|
|
||||||
|
Pouzitie v stranke alebo komponente:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const health = await api.example.getHealth()
|
||||||
|
const welcome = await api.example.getWelcome()
|
||||||
|
```
|
||||||
|
|
||||||
|
Odporucany vzor:
|
||||||
|
|
||||||
|
- komponent alebo page vola `useApi()`
|
||||||
|
- `useApi()` vracia klientov injectnutych z `app/plugins/api.client.ts`
|
||||||
|
- konkretne HTTP volania alebo OpenAPI klient je schovany vo `api/wrappers`
|
||||||
|
|
||||||
|
Tymto sposobom ostane UI oddelene od detailov backend komunikacie.
|
||||||
|
|
||||||
|
Zakladne env pre API:
|
||||||
|
|
||||||
|
- `NUXT_PUBLIC_API_BASE_URL`
|
||||||
|
- `NUXT_PUBLIC_API_TIMEOUT_MS`
|
||||||
|
|
||||||
|
Zakladny generator prikaz:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm generate:api
|
||||||
|
```
|
||||||
|
|
||||||
|
Odporucany postup:
|
||||||
|
|
||||||
|
1. nahrad `openapi-spec/example-api.yaml` vlastnou specifikaciou
|
||||||
|
2. uprav `openapi-ts.config.ts`
|
||||||
|
3. spusti `pnpm generate:api`
|
||||||
|
4. napoj vygenerovaneho klienta cez `api/wrappers`
|
||||||
|
|
||||||
|
Ak uz mas backend kontrakt cez OpenAPI, odporucany sposob integracie je:
|
||||||
|
|
||||||
|
1. pridaj alebo aktualizuj `.yaml` specifikaciu v `openapi-spec`
|
||||||
|
2. vygeneruj klienta do `openapi-client`
|
||||||
|
3. v `api/wrappers` vytvor tenku vrstvu, ktora premapuje volania na domenove funkcie
|
||||||
|
4. z komponentov volaj iba `useApi()`, nie vygenerovany klient priamo
|
||||||
|
|
||||||
|
Poznamka ku gitu:
|
||||||
|
|
||||||
|
- obsah `openapi-client` je v template ignorovany, pretoze ide o generovane subory
|
||||||
|
- v gite ostava len `openapi-client/README.md`, aby bolo jasne, na co adresar sluzi
|
||||||
|
- ak bude tvoj tim chciet verzovat aj generovanych klientov, uprav `.gitignore`
|
||||||
|
|
||||||
|
### Kde upravovat autorizaciu
|
||||||
|
|
||||||
|
Auth vrstva je zamerne volitelna. Ak ju projekt nepotrebuje, ostava vypnuta.
|
||||||
|
|
||||||
|
Zakladne casti su v:
|
||||||
|
|
||||||
|
- `app/plugins/auth.ts`
|
||||||
|
- `app/composables/useAuth.ts`
|
||||||
|
- `app/middleware/auth.global.ts`
|
||||||
|
- `app/stores/auth.ts`
|
||||||
|
- `app/types/auth.ts`
|
||||||
|
|
||||||
|
Rezim auth sa nastavuje cez `.env`:
|
||||||
|
|
||||||
|
- `NUXT_PUBLIC_AUTH_MODE=disabled`: auth je vypnuta a middleware nic nevynucuje
|
||||||
|
- `NUXT_PUBLIC_AUTH_MODE=mock`: lokalny vyvoj s mock userom
|
||||||
|
- `NUXT_PUBLIC_AUTH_MODE=userinfo`: nacitanie usera z endpointu `NUXT_PUBLIC_AUTH_USERINFO_URL`
|
||||||
|
|
||||||
|
Pouzitie v komponente:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const auth = useAuth()
|
||||||
|
|
||||||
|
await auth.ensureInitialized()
|
||||||
|
|
||||||
|
if (auth.isAuthenticated.value) {
|
||||||
|
console.log(auth.user.value?.email)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pouzitie cez store:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
if (authStore.hasRole('ADMIN')) {
|
||||||
|
// zobraz admin cast rozhrania
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ak chces ochranit konkretnu stranku, pouzi route meta:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
definePageMeta({
|
||||||
|
requiresAuth: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Alebo s rolami:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
definePageMeta({
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['ADMIN']
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Bezny scenar:
|
||||||
|
|
||||||
|
- pocas lokalneho vyvoja nastav `NUXT_PUBLIC_AUTH_MODE=mock`
|
||||||
|
- pre projekt bez prihlasenia nechaj `NUXT_PUBLIC_AUTH_MODE=disabled`
|
||||||
|
- pre realne SSO alebo proxy endpoint pouzi `NUXT_PUBLIC_AUTH_MODE=userinfo`
|
||||||
|
|
||||||
|
Auth middleware sa spusta globalne, ale zasahuje len na routach s `requiresAuth` alebo `roles`.
|
||||||
|
|
||||||
|
### Kde upravovat globalny stav cez Pinia
|
||||||
|
|
||||||
|
Pinia sa hodi na stav, ktory je zdielany vo viacerych castiach aplikacie.
|
||||||
|
|
||||||
|
Pripravene store-y:
|
||||||
|
|
||||||
|
- `app/stores/auth.ts`: user, role, session stav
|
||||||
|
- `app/stores/ui.ts`: loading a notifikacie
|
||||||
|
|
||||||
|
Pouzitie `ui` store:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
|
||||||
|
uiStore.startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
uiStore.pushNotification({
|
||||||
|
title: 'Ulozene',
|
||||||
|
message: 'Zmena bola uspesne ulozena.',
|
||||||
|
tone: 'success'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
uiStore.stopLoading()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pouzitie `auth` store:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const userName = authStore.displayName
|
||||||
|
const isLoggedIn = authStore.isAuthenticated
|
||||||
|
```
|
||||||
|
|
||||||
|
Odporucanie:
|
||||||
|
|
||||||
|
- lokalny stav formulára nechaj v komponente
|
||||||
|
- zdielany stav typu user, loading, notifikacie alebo filtre dava zmysel drzat v store
|
||||||
|
|
||||||
|
### Ako pouzit Nuxt UI v tomto template
|
||||||
|
|
||||||
|
Nuxt UI je zapojene ako Nuxt modul a je predvolenou UI vrstvou template.
|
||||||
|
|
||||||
|
Zakladne pouzitie:
|
||||||
|
|
||||||
|
1. do page alebo komponentu pouzi `U*` komponenty priamo
|
||||||
|
2. nebuduj dalsiu vrstvu `BaseButton`, `BaseInput`, `BaseCard`, ak na to nie je silny dovod
|
||||||
|
3. vlastne komplikovane komponenty, napriklad diagramy alebo specialne editory, stavaj ako bezne Vue komponenty a Nuxt UI pouzi na ich okolie, toolbar, formulare a akcie
|
||||||
|
|
||||||
|
Priklad:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="stack">
|
||||||
|
<p class="section-kicker">Demo</p>
|
||||||
|
<h2>Nuxt UI komponent</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
color="info"
|
||||||
|
variant="soft"
|
||||||
|
title="Stav"
|
||||||
|
description="Komponent je pripraveny."
|
||||||
|
/>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kde upravovat hlavnu konfiguraciu
|
||||||
|
|
||||||
|
Hlavne nastavenia su v:
|
||||||
|
|
||||||
|
- `nuxt.config.ts`
|
||||||
|
|
||||||
|
Tu upravis napr.:
|
||||||
|
|
||||||
|
- globalne CSS
|
||||||
|
- Nuxt moduly
|
||||||
|
- title a meta tagy
|
||||||
|
- i18n konfiguraciu
|
||||||
|
- devtools spravanie
|
||||||
|
|
||||||
|
## Odporucany workflow
|
||||||
|
|
||||||
|
Pri beznej praci sa osvedci tento postup:
|
||||||
|
|
||||||
|
1. Spusti `pnpm dev`.
|
||||||
|
2. Otvor aplikaciu v prehliadaci.
|
||||||
|
3. Men stranky v `app/pages`, layout v `app/layouts` a texty v `i18n/locales`.
|
||||||
|
4. Priebežne kontroluj typy a lint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm lint
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Pred odovzdanim alebo commitom over:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm format
|
||||||
|
pnpm test:run
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## VS Code
|
||||||
|
|
||||||
|
Repozitar obsahuje:
|
||||||
|
|
||||||
|
- `.vscode/settings.json`: workspace nastavenia
|
||||||
|
- `.vscode/extensions.json`: odporucane rozsirrenia
|
||||||
|
- `.vscode/launch.json`: jednoduche spustenie `pnpm dev` z VS Code
|
||||||
|
|
||||||
|
Predinstalovane a odporucane rozsirrenia su zamerane najma na:
|
||||||
|
|
||||||
|
- Vue a TypeScript
|
||||||
|
- ESLint a Prettier
|
||||||
|
- i18n preklady
|
||||||
|
- Vitest
|
||||||
|
|
||||||
|
## Poznamky k dev rezimu
|
||||||
|
|
||||||
|
- `pnpm install` spusta `postinstall`, ktory vola `nuxt prepare`
|
||||||
|
- OpenAPI codegen ide cez `@hey-api/openapi-ts`, takze netreba Javu
|
||||||
|
- prvy start po cistom install moze byt pomalsi
|
||||||
|
- prvy Vite build sa moze spustit az pri prvom otvoreni stranky
|
||||||
|
- ak sa objavi problem s viacerymi servermi, ukonci beziace Node alebo Nuxt procesy vo svojom terminali alebo cez Spravcu uloh a spusti iba jeden `pnpm dev`
|
||||||
|
|
||||||
|
## Co typicky zmenit ako prve
|
||||||
|
|
||||||
|
Po vytvoreni noveho projektu z template sa obvykle meni:
|
||||||
|
|
||||||
|
- branding a texty v prekladoch
|
||||||
|
- titulky a meta udaje
|
||||||
|
- farby, spacing a tokeny v stylech
|
||||||
|
- obsah `app/pages/index.vue`
|
||||||
|
- pouzitie a konfiguracia Nuxt UI komponentov podla potrieb projektu
|
||||||
|
- vlastne feature komponenty v `app/components`
|
||||||
|
- doplnenie dalsich stranok, API endpointov a testov
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
async function loadPlugin() {
|
||||||
|
vi.resetModules()
|
||||||
|
vi.stubGlobal('defineNuxtPlugin', (setup: unknown) => setup)
|
||||||
|
|
||||||
|
return (await import('../app/plugins/api.client')).default
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('api plugin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates API clients from runtime config values', async () => {
|
||||||
|
vi.stubGlobal('useRuntimeConfig', () => ({
|
||||||
|
public: {
|
||||||
|
apiBaseUrl: 'https://api.example.test',
|
||||||
|
apiTimeoutMs: 4321
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({ status: 'ok' })
|
||||||
|
vi.stubGlobal('$fetch', fetchMock)
|
||||||
|
|
||||||
|
const response = await provide.api.example.getHealth()
|
||||||
|
|
||||||
|
expect(response).toEqual({ status: 'ok' })
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/health', {
|
||||||
|
baseURL: 'https://api.example.test',
|
||||||
|
signal: undefined,
|
||||||
|
timeout: 4321
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { useAuthStore } from '../app/stores/auth'
|
||||||
|
|
||||||
|
async function loadMiddleware() {
|
||||||
|
vi.resetModules()
|
||||||
|
vi.stubGlobal('defineNuxtRouteMiddleware', (setup: unknown) => setup)
|
||||||
|
|
||||||
|
return (await import('../app/middleware/auth.global')).default
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAuthClient(overrides: Partial<ReturnType<typeof buildAuthClient>> = {}) {
|
||||||
|
return {
|
||||||
|
...buildAuthClient(),
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAuthClient() {
|
||||||
|
return {
|
||||||
|
isEnabled: true,
|
||||||
|
isAuthenticated: computed(() => false),
|
||||||
|
ensureInitialized: vi.fn().mockResolvedValue(undefined),
|
||||||
|
login: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('auth middleware', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
vi.stubGlobal('useAuthStore', useAuthStore)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips public routes and disabled auth', async () => {
|
||||||
|
const publicAuth = createAuthClient()
|
||||||
|
vi.stubGlobal('useAuth', () => publicAuth)
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware()
|
||||||
|
|
||||||
|
await expect(middleware({ meta: { public: true } })).resolves.toBeUndefined()
|
||||||
|
expect(publicAuth.ensureInitialized).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const disabledAuth = createAuthClient({ isEnabled: false })
|
||||||
|
vi.stubGlobal('useAuth', () => disabledAuth)
|
||||||
|
|
||||||
|
await expect(middleware({ meta: { requiresAuth: true } })).resolves.toBeUndefined()
|
||||||
|
expect(disabledAuth.ensureInitialized).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requests login for unauthenticated protected routes', async () => {
|
||||||
|
const auth = createAuthClient({
|
||||||
|
isAuthenticated: computed(() => false)
|
||||||
|
})
|
||||||
|
vi.stubGlobal('useAuth', () => auth)
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware()
|
||||||
|
|
||||||
|
await expect(middleware({ meta: { requiresAuth: true } })).resolves.toBeUndefined()
|
||||||
|
|
||||||
|
expect(auth.ensureInitialized).toHaveBeenCalledOnce()
|
||||||
|
expect(auth.login).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows authenticated users with a matching role', async () => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.setUser({
|
||||||
|
id: 'user-1',
|
||||||
|
roles: ['ADMIN']
|
||||||
|
})
|
||||||
|
|
||||||
|
const auth = createAuthClient({
|
||||||
|
isAuthenticated: computed(() => true)
|
||||||
|
})
|
||||||
|
vi.stubGlobal('useAuth', () => auth)
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware()
|
||||||
|
|
||||||
|
await expect(middleware({ meta: { roles: ['ADMIN'] } })).resolves.toBeUndefined()
|
||||||
|
|
||||||
|
expect(auth.ensureInitialized).toHaveBeenCalledOnce()
|
||||||
|
expect(auth.login).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws a 403 error for authenticated users without a required role', async () => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.setUser({
|
||||||
|
id: 'user-2',
|
||||||
|
roles: ['USER']
|
||||||
|
})
|
||||||
|
|
||||||
|
const createErrorMock = vi.fn((error: { statusCode: number; statusMessage: string }) => error)
|
||||||
|
vi.stubGlobal('createError', createErrorMock)
|
||||||
|
|
||||||
|
const auth = createAuthClient({
|
||||||
|
isAuthenticated: computed(() => true)
|
||||||
|
})
|
||||||
|
vi.stubGlobal('useAuth', () => auth)
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware()
|
||||||
|
|
||||||
|
await expect(middleware({ meta: { roles: ['ADMIN'] } })).rejects.toMatchObject({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Forbidden'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createErrorMock).toHaveBeenCalledWith({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Forbidden'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { useAuthStore, type AuthUser } from '../app/stores/auth'
|
||||||
|
|
||||||
|
type AuthPlugin = typeof import('../app/plugins/auth').default
|
||||||
|
|
||||||
|
function createRuntimeConfig(authMode: string | undefined = undefined) {
|
||||||
|
return {
|
||||||
|
public: {
|
||||||
|
authMode,
|
||||||
|
authLoginUrl: '/login',
|
||||||
|
authLogoutUrl: '/logout',
|
||||||
|
authUserinfoUrl: '/api/auth/me'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlugin() {
|
||||||
|
vi.resetModules()
|
||||||
|
vi.stubGlobal('defineNuxtPlugin', (setup: AuthPlugin) => setup)
|
||||||
|
|
||||||
|
return (await import('../app/plugins/auth')).default
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('auth plugin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes a disabled auth client and keeps auth turned off', async () => {
|
||||||
|
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig())
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
const auth = provide.auth
|
||||||
|
|
||||||
|
expect(auth.mode).toBe('disabled')
|
||||||
|
expect(auth.isEnabled).toBe(false)
|
||||||
|
|
||||||
|
await auth.ensureInitialized()
|
||||||
|
|
||||||
|
const store = useAuthStore()
|
||||||
|
expect(auth.isReady.value).toBe(true)
|
||||||
|
expect(auth.isAuthenticated.value).toBe(false)
|
||||||
|
expect(store.isInitialized).toBe(true)
|
||||||
|
expect(store.user).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a mock user in mock mode and supports login and logout locally', async () => {
|
||||||
|
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('mock'))
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
const auth = provide.auth
|
||||||
|
|
||||||
|
await auth.ensureInitialized()
|
||||||
|
|
||||||
|
const store = useAuthStore()
|
||||||
|
expect(auth.mode).toBe('mock')
|
||||||
|
expect(auth.isEnabled).toBe(true)
|
||||||
|
expect(auth.isAuthenticated.value).toBe(true)
|
||||||
|
expect(store.user).toMatchObject({
|
||||||
|
id: 'mock-user',
|
||||||
|
email: 'mock@example.com',
|
||||||
|
roles: ['USER']
|
||||||
|
})
|
||||||
|
|
||||||
|
auth.logout()
|
||||||
|
expect(store.user).toBeNull()
|
||||||
|
|
||||||
|
auth.login()
|
||||||
|
expect(store.user).toMatchObject({
|
||||||
|
id: 'mock-user',
|
||||||
|
email: 'mock@example.com'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads the current user from the userinfo endpoint in userinfo mode', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'john@example.com',
|
||||||
|
roles: ['USER']
|
||||||
|
} satisfies AuthUser)
|
||||||
|
|
||||||
|
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('userinfo'))
|
||||||
|
vi.stubGlobal('$fetch', fetchMock)
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
const auth = provide.auth
|
||||||
|
|
||||||
|
await auth.ensureInitialized()
|
||||||
|
|
||||||
|
const store = useAuthStore()
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/api/auth/me', {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
expect(auth.isAuthenticated.value).toBe(true)
|
||||||
|
expect(store.user).toMatchObject({
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'john@example.com'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to an anonymous session when userinfo loading fails', async () => {
|
||||||
|
const fetchMock = vi.fn().mockRejectedValue(new Error('network error'))
|
||||||
|
|
||||||
|
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('userinfo'))
|
||||||
|
vi.stubGlobal('$fetch', fetchMock)
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
const auth = provide.auth
|
||||||
|
|
||||||
|
await auth.ensureInitialized()
|
||||||
|
|
||||||
|
const store = useAuthStore()
|
||||||
|
expect(auth.isReady.value).toBe(true)
|
||||||
|
expect(auth.isAuthenticated.value).toBe(false)
|
||||||
|
expect(store.isInitialized).toBe(true)
|
||||||
|
expect(store.user).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects login and logout in userinfo mode', async () => {
|
||||||
|
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('userinfo'))
|
||||||
|
vi.stubGlobal('$fetch', vi.fn().mockResolvedValue(null))
|
||||||
|
vi.stubGlobal('window', {
|
||||||
|
location: {
|
||||||
|
href: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
const auth = provide.auth
|
||||||
|
|
||||||
|
auth.login()
|
||||||
|
expect(window.location.href).toBe('/login')
|
||||||
|
|
||||||
|
const store = useAuthStore()
|
||||||
|
store.setUser({
|
||||||
|
id: 'user-2',
|
||||||
|
roles: ['USER']
|
||||||
|
})
|
||||||
|
|
||||||
|
auth.logout()
|
||||||
|
expect(window.location.href).toBe('/logout')
|
||||||
|
expect(store.user).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { useAuthStore, type AuthUser } from '../app/stores/auth'
|
||||||
|
|
||||||
|
describe('useAuthStore', () => {
|
||||||
|
const baseUser: AuthUser = {
|
||||||
|
id: 'user-1',
|
||||||
|
email: 'john@example.com',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
roles: ['USER', 'ADMIN']
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes authenticated session state from a user', () => {
|
||||||
|
const store = useAuthStore()
|
||||||
|
|
||||||
|
store.initialize(baseUser)
|
||||||
|
|
||||||
|
expect(store.isInitialized).toBe(true)
|
||||||
|
expect(store.isAuthenticated).toBe(true)
|
||||||
|
expect(store.user).toEqual(baseUser)
|
||||||
|
expect(store.roles).toEqual(['USER', 'ADMIN'])
|
||||||
|
expect(store.displayName).toBe('John Doe')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to email or id when full name is not available', () => {
|
||||||
|
const store = useAuthStore()
|
||||||
|
|
||||||
|
store.setUser({
|
||||||
|
id: 'user-2',
|
||||||
|
email: 'fallback@example.com',
|
||||||
|
roles: ['USER']
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(store.displayName).toBe('fallback@example.com')
|
||||||
|
|
||||||
|
store.setUser({
|
||||||
|
id: 'user-3',
|
||||||
|
roles: ['USER']
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(store.displayName).toBe('user-3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('checks roles and clears session safely', () => {
|
||||||
|
const store = useAuthStore()
|
||||||
|
|
||||||
|
store.initialize(baseUser)
|
||||||
|
|
||||||
|
expect(store.hasRole('ADMIN')).toBe(true)
|
||||||
|
expect(store.hasRole('GUEST')).toBe(false)
|
||||||
|
expect(store.hasAnyRole(['GUEST', 'ADMIN'])).toBe(true)
|
||||||
|
expect(store.hasAnyRole(['GUEST'])).toBe(false)
|
||||||
|
|
||||||
|
store.clearSession()
|
||||||
|
|
||||||
|
expect(store.isInitialized).toBe(true)
|
||||||
|
expect(store.isAuthenticated).toBe(false)
|
||||||
|
expect(store.user).toBeNull()
|
||||||
|
expect(store.roles).toEqual([])
|
||||||
|
expect(store.displayName).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createExampleApi } from '../api/wrappers/example'
|
||||||
|
|
||||||
|
describe('createExampleApi', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the expected endpoint configuration for getHealth', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({ status: 'ok' })
|
||||||
|
vi.stubGlobal('$fetch', fetchMock)
|
||||||
|
|
||||||
|
const api = createExampleApi({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeoutMs: 10_000
|
||||||
|
})
|
||||||
|
const controller = new AbortController()
|
||||||
|
|
||||||
|
const response = await api.getHealth({ signal: controller.signal })
|
||||||
|
|
||||||
|
expect(response).toEqual({ status: 'ok' })
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/health', {
|
||||||
|
baseURL: '/api',
|
||||||
|
signal: controller.signal,
|
||||||
|
timeout: 10_000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the expected endpoint configuration for getWelcome', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({ message: 'Welcome' })
|
||||||
|
vi.stubGlobal('$fetch', fetchMock)
|
||||||
|
|
||||||
|
const api = createExampleApi({
|
||||||
|
baseURL: 'https://example.test',
|
||||||
|
timeoutMs: 5_000
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await api.getWelcome()
|
||||||
|
|
||||||
|
expect(response).toEqual({ message: 'Welcome' })
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/welcome', {
|
||||||
|
baseURL: 'https://example.test',
|
||||||
|
signal: undefined,
|
||||||
|
timeout: 5_000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import en from '../i18n/locales/en.json'
|
||||||
|
import sk from '../i18n/locales/sk.json'
|
||||||
|
|
||||||
|
type JsonValue = string | number | boolean | null | JsonObject | JsonValue[]
|
||||||
|
type JsonObject = Record<string, JsonValue>
|
||||||
|
|
||||||
|
function collectLeafPaths(value: JsonValue, prefix = ''): string[] {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item, index) => collectLeafPaths(item, `${prefix}[${index}]`))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && typeof value === 'object') {
|
||||||
|
return Object.entries(value as JsonObject).flatMap(([key, nestedValue]) =>
|
||||||
|
collectLeafPaths(nestedValue, prefix ? `${prefix}.${key}` : key)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [prefix]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('i18n locale files', () => {
|
||||||
|
it('keep the same translation key structure in slovak and english', () => {
|
||||||
|
const skPaths = collectLeafPaths(sk as JsonObject).sort()
|
||||||
|
const enPaths = collectLeafPaths(en as JsonObject).sort()
|
||||||
|
|
||||||
|
expect(skPaths).toEqual(enPaths)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
async function loadPlugin() {
|
||||||
|
vi.resetModules()
|
||||||
|
vi.stubGlobal('defineNuxtPlugin', (setup: unknown) => setup)
|
||||||
|
|
||||||
|
return (await import('../app/plugins/logger')).default
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('logger plugin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs messages with the expected prefix and payload', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date('2026-03-29T12:00:00.000Z'))
|
||||||
|
|
||||||
|
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
|
||||||
|
provide.logger.info('Action completed', { section: 'home' })
|
||||||
|
|
||||||
|
expect(infoSpy).toHaveBeenCalledWith(
|
||||||
|
'[template:info] 2026-03-29T12:00:00.000Z Action completed',
|
||||||
|
{ section: 'home' }
|
||||||
|
)
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs messages without payload as a single console argument', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date('2026-03-29T12:00:00.000Z'))
|
||||||
|
|
||||||
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||||
|
|
||||||
|
const plugin = await loadPlugin()
|
||||||
|
const { provide } = plugin()
|
||||||
|
|
||||||
|
provide.logger.warn('Missing optional data')
|
||||||
|
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
|
'[template:warn] 2026-03-29T12:00:00.000Z Missing optional data'
|
||||||
|
)
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { useUiStore } from '../app/stores/ui'
|
||||||
|
|
||||||
|
describe('useUiStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tracks page loading state across nested requests', () => {
|
||||||
|
const store = useUiStore()
|
||||||
|
|
||||||
|
expect(store.isPageLoading).toBe(false)
|
||||||
|
|
||||||
|
store.startLoading()
|
||||||
|
store.startLoading()
|
||||||
|
|
||||||
|
expect(store.isPageLoading).toBe(true)
|
||||||
|
|
||||||
|
store.stopLoading()
|
||||||
|
expect(store.isPageLoading).toBe(true)
|
||||||
|
|
||||||
|
store.stopLoading()
|
||||||
|
store.stopLoading()
|
||||||
|
expect(store.isPageLoading).toBe(false)
|
||||||
|
|
||||||
|
store.setLoading(true)
|
||||||
|
expect(store.isPageLoading).toBe(true)
|
||||||
|
|
||||||
|
store.setLoading(false)
|
||||||
|
expect(store.isPageLoading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates notifications with a default tone and removes them by id', () => {
|
||||||
|
const store = useUiStore()
|
||||||
|
|
||||||
|
vi.spyOn(crypto, 'randomUUID').mockReturnValue('generated-id')
|
||||||
|
|
||||||
|
const id = store.pushNotification({
|
||||||
|
title: 'Saved',
|
||||||
|
message: 'Changes were stored.'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(id).toBe('generated-id')
|
||||||
|
expect(store.notifications).toEqual([
|
||||||
|
{
|
||||||
|
id: 'generated-id',
|
||||||
|
title: 'Saved',
|
||||||
|
message: 'Changes were stored.',
|
||||||
|
tone: 'info'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
store.pushNotification({
|
||||||
|
id: 'custom-id',
|
||||||
|
title: 'Warning',
|
||||||
|
tone: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(store.notifications).toHaveLength(2)
|
||||||
|
|
||||||
|
store.removeNotification('generated-id')
|
||||||
|
expect(store.notifications.map((notification) => notification.id)).toEqual(['custom-id'])
|
||||||
|
|
||||||
|
store.clearNotifications()
|
||||||
|
expect(store.notifications).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { useLogger } from '../app/composables/useLogger'
|
||||||
|
|
||||||
|
describe('useLogger', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the logger injected into the Nuxt app', () => {
|
||||||
|
const logger = {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.stubGlobal('useNuxtApp', () => ({
|
||||||
|
$logger: logger
|
||||||
|
}))
|
||||||
|
|
||||||
|
expect(useLogger()).toBe(logger)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': fileURLToPath(new URL('./app', import.meta.url)),
|
||||||
|
'@': fileURLToPath(new URL('./app', import.meta.url)),
|
||||||
|
'~~': fileURLToPath(new URL('./', import.meta.url)),
|
||||||
|
'@@': fileURLToPath(new URL('./', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user