Initial commit

This commit is contained in:
AI
2026-05-03 07:26:12 +00:00
commit 776d374b59
57 changed files with 15968 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
.git
.gitignore
.nuxt
.output
.vscode
coverage
node_modules
*.log
.env
.env.*
!.env.example
+9
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
24.14.1
+5
View File
@@ -0,0 +1,5 @@
.nuxt
.output
coverage
node_modules
pnpm-lock.yaml
+5
View File
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none"
}
+12
View File
@@ -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"
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Nuxt: dev",
"type": "node-terminal",
"request": "launch",
"command": "pnpm dev"
}
]
}
+72
View File
@@ -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
View File
@@ -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"]
+30
View File
@@ -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)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
+112
View File
@@ -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;
}
}
+16
View File
@@ -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%
);
}
+7
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
@use './tokens';
@use './theme';
@use './base';
+2
View File
@@ -0,0 +1,2 @@
@import "tailwindcss";
@import "@nuxt/ui";
+5
View File
@@ -0,0 +1,5 @@
import type { ApiClients } from '~/types/api'
export function useApi(): ApiClients {
return useNuxtApp().$api
}
+5
View File
@@ -0,0 +1,5 @@
import type { AuthClient } from '~/types/auth'
export function useAuth(): AuthClient {
return useNuxtApp().$auth
}
+16
View File
@@ -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 }
+86
View File
@@ -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>
+31
View File
@@ -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'
})
}
}
})
+164
View File
@@ -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>
+19
View File
@@ -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
}
}
})
+123
View File
@@ -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
}
}
})
+28
View File
@@ -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
}
}
})
+63
View File
@@ -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
}
})
+61
View File
@@ -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
}
})
+20
View File
@@ -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
}
+15
View File
@@ -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>
}
+21
View File
@@ -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 {}
+11
View File
@@ -0,0 +1,11 @@
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
public?: boolean
requiresAuth?: boolean
roles?: string[]
}
}
export {}
+20
View File
@@ -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']
}
]
}
}
])
+68
View File
@@ -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"
}
}
}
}
+68
View File
@@ -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"
}
}
}
}
+56
View File
@@ -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
}
})
+7
View File
@@ -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`.
+14
View File
@@ -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.
+39
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: './openapi-spec/example-api.yaml',
output: './openapi-client/example'
})
+50
View File
@@ -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"
}
}
+12035
View File
File diff suppressed because it is too large Load Diff
+978
View File
@@ -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
View File
@@ -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`
+700
View File
@@ -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
+39
View File
@@ -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
})
})
})
+114
View File
@@ -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'
})
})
})
+152
View File
@@ -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()
})
})
+67
View File
@@ -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('')
})
})
+47
View File
@@ -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
})
})
})
+29
View File
@@ -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)
})
})
+52
View File
@@ -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()
})
})
+69
View File
@@ -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([])
})
})
+24
View File
@@ -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)
})
})
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"types": ["node"]
}
}
+13
View File
@@ -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))
}
}
})