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
+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 {}