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