diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4816602 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "[vue]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue" + ] +} diff --git a/app/app.vue b/app/app.vue index bc45a20..ed9e94c 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,8 +1,8 @@ diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png new file mode 100644 index 0000000..52f1bff Binary files /dev/null and b/app/assets/images/logo.png differ diff --git a/app/assets/scss/layout/_core.scss b/app/assets/scss/layout/_core.scss new file mode 100644 index 0000000..e040f53 --- /dev/null +++ b/app/assets/scss/layout/_core.scss @@ -0,0 +1,24 @@ +html { + height: 100%; + font-size: 14px; + line-height: 1.2; +} + +body { + font-family: Ubuntu, Arial, Helvetica, sans-serif; + color: var(--text-color); + background-color: var(--surface-ground); + margin: 0; + padding: 0; + min-height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + text-decoration: none; +} + +.layout-wrapper { + min-height: 100vh; +} diff --git a/app/assets/scss/layout/_footer.scss b/app/assets/scss/layout/_footer.scss new file mode 100644 index 0000000..8f96833 --- /dev/null +++ b/app/assets/scss/layout/_footer.scss @@ -0,0 +1,8 @@ +.layout-footer { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 0 1rem 0; + gap: 0.5rem; + border-top: 1px solid var(--surface-border); +} diff --git a/app/assets/scss/layout/_main.scss b/app/assets/scss/layout/_main.scss new file mode 100644 index 0000000..459bbdd --- /dev/null +++ b/app/assets/scss/layout/_main.scss @@ -0,0 +1,14 @@ +.layout-main-container { + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; + padding: 6rem 2rem 0 2rem; + background-color: var(--surface-ground); + transition: margin-left var(--layout-section-transition-duration); +} + +.layout-main { + flex: 1 1 auto; + padding-bottom: 2rem; +} diff --git a/app/assets/scss/layout/_menu.scss b/app/assets/scss/layout/_menu.scss new file mode 100644 index 0000000..a4a9642 --- /dev/null +++ b/app/assets/scss/layout/_menu.scss @@ -0,0 +1,160 @@ +@use "mixins" as *; + +.layout-sidebar { + position: fixed; + width: 20rem; + height: calc(100vh - 8rem); + z-index: 999; + overflow-y: auto; + user-select: none; + top: 6rem; + left: 2rem; + transition: + transform var(--layout-section-transition-duration), + left var(--layout-section-transition-duration); + background-color: var(--surface-overlay); + border-radius: var(--content-border-radius); + padding: 0.5rem 1.5rem; +} + +.layout-menu { + margin: 0; + padding: 0; + list-style-type: none; + + .layout-root-menuitem { + > .layout-menuitem-root-text { + font-size: 0.857rem; + text-transform: uppercase; + font-weight: 700; + color: var(--text-color); + margin: 0.75rem 0; + } + + > a { + display: none; + } + } + + a { + user-select: none; + + &.active-menuitem { + > .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + li.active-menuitem { + > a { + .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + ul { + margin: 0; + padding: 0; + list-style-type: none; + + a { + display: flex; + align-items: center; + position: relative; + outline: 0 none; + color: var(--text-color); + cursor: pointer; + padding: 0.75rem 1rem; + border-radius: var(--content-border-radius); + transition: + background-color var(--element-transition-duration), + box-shadow var(--element-transition-duration); + + .layout-menuitem-icon { + margin-right: 0.5rem; + } + + .layout-submenu-toggler { + font-size: 75%; + margin-left: auto; + transition: transform var(--element-transition-duration); + } + + &.active-route { + font-weight: 700; + color: var(--primary-color); + } + + &:hover { + background-color: var(--surface-hover); + } + + &:focus { + @include focused-inset(); + } + } + + ul { + overflow: hidden; + border-radius: var(--content-border-radius); + + li { + a { + margin-left: 1rem; + } + + li { + a { + margin-left: 2rem; + } + + li { + a { + margin-left: 2.5rem; + } + + li { + a { + margin-left: 3rem; + } + + li { + a { + margin-left: 3.5rem; + } + + li { + a { + margin-left: 4rem; + } + } + } + } + } + } + } + } + } +} + +.layout-submenu-enter-from, +.layout-submenu-leave-to { + max-height: 0; +} + +.layout-submenu-enter-to, +.layout-submenu-leave-from { + max-height: 1000px; +} + +.layout-submenu-leave-active { + overflow: hidden; + transition: max-height 0.45s cubic-bezier(0, 1, 0, 1); +} + +.layout-submenu-enter-active { + overflow: hidden; + transition: max-height 1s ease-in-out; +} diff --git a/app/assets/scss/layout/_mixins.scss b/app/assets/scss/layout/_mixins.scss new file mode 100644 index 0000000..6256ad9 --- /dev/null +++ b/app/assets/scss/layout/_mixins.scss @@ -0,0 +1,15 @@ +@mixin focused() { + outline-width: var(--focus-ring-width); + outline-style: var(--focus-ring-style); + outline-color: var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + box-shadow: var(--focus-ring-shadow); + transition: + box-shadow var(--transition-duration), + outline-color var(--transition-duration); +} + +@mixin focused-inset() { + outline-offset: -1px; + box-shadow: inset var(--focus-ring-shadow); +} diff --git a/app/assets/scss/layout/_preloading.scss b/app/assets/scss/layout/_preloading.scss new file mode 100644 index 0000000..fd3c0d1 --- /dev/null +++ b/app/assets/scss/layout/_preloading.scss @@ -0,0 +1,48 @@ +.preloader { + position: fixed; + z-index: 999999; + background: #edf1f5; + width: 100%; + height: 100%; +} +.preloader-content { + border: 0 solid transparent; + border-radius: 50%; + width: 150px; + height: 150px; + position: absolute; + top: calc(50vh - 75px); + left: calc(50vw - 75px); +} + +.preloader-content:before, +.preloader-content:after { + content: ""; + border: 1em solid var(--primary-color); + border-radius: 50%; + width: inherit; + height: inherit; + position: absolute; + top: 0; + left: 0; + animation: loader 2s linear infinite; + opacity: 0; +} + +.preloader-content:before { + animation-delay: 0.5s; +} + +@keyframes loader { + 0% { + transform: scale(0); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0; + } +} diff --git a/app/assets/scss/layout/_responsive.scss b/app/assets/scss/layout/_responsive.scss new file mode 100644 index 0000000..6b14bcb --- /dev/null +++ b/app/assets/scss/layout/_responsive.scss @@ -0,0 +1,110 @@ +@media screen and (min-width: 1960px) { + .layout-main, + .landing-wrapper { + width: 1504px; + margin-left: auto !important; + margin-right: auto !important; + } +} + +@media (min-width: 992px) { + .layout-wrapper { + &.layout-overlay { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-right: 1px solid var(--surface-border); + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + } + + &.layout-overlay-active { + .layout-sidebar { + transform: translateX(0); + } + } + } + + &.layout-static { + .layout-main-container { + margin-left: 22rem; + } + + &.layout-static-inactive { + .layout-sidebar { + transform: translateX(-100%); + left: 0; + } + + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + } + } + + .layout-mask { + display: none; + } + } +} + +@media (max-width: 991px) { + .blocked-scroll { + overflow: hidden; + } + + .layout-wrapper { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + } + + .layout-mask { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 998; + width: 100%; + height: 100%; + background-color: var(--maskbg); + } + + &.layout-mobile-active { + .layout-sidebar { + transform: translateX(0); + } + + .layout-mask { + display: block; + } + } + } +} diff --git a/app/assets/scss/layout/_topbar.scss b/app/assets/scss/layout/_topbar.scss new file mode 100644 index 0000000..06cbe76 --- /dev/null +++ b/app/assets/scss/layout/_topbar.scss @@ -0,0 +1,201 @@ +@use "mixins" as *; + +.layout-topbar { + position: fixed; + height: 4rem; + z-index: 997; + left: 0; + top: 0; + width: 100%; + padding: 0 2rem; + background-color: var(--surface-card); + transition: left var(--layout-section-transition-duration); + display: flex; + align-items: center; + + .layout-topbar-logo-container { + width: 20rem; + display: flex; + align-items: center; + } + + .layout-topbar-logo { + display: inline-flex; + align-items: center; + font-size: 1.5rem; + border-radius: var(--content-border-radius); + color: var(--text-color); + font-weight: 500; + gap: 0.5rem; + + img { + width: 3rem; + } + + &:focus-visible { + @include focused(); + } + } + + .layout-topbar-action { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + color: var(--text-color); + transition: background-color var(--element-transition-duration); + cursor: pointer; + + &:hover { + background-color: var(--surface-hover); + } + + &:focus-visible { + @include focused(); + } + + i { + font-size: 1.25rem; + } + + span { + font-size: 1rem; + display: none; + } + + &.layout-topbar-action-highlight { + background-color: var(--primary-color); + color: var(--primary-contrast-color); + } + } + + .layout-menu-button { + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: none; + } + + .layout-topbar-actions { + margin-left: auto; + display: flex; + gap: 1rem; + } + + .layout-topbar-menu-content { + display: flex; + gap: 1rem; + } + + .layout-config-menu { + display: flex; + gap: 1rem; + } +} + +@media (max-width: 991px) { + .layout-topbar { + padding: 0 2rem; + + .layout-topbar-logo-container { + width: auto; + } + + .layout-menu-button { + margin-left: 0; + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: inline-flex; + } + + .layout-topbar-menu { + position: absolute; + background-color: var(--surface-overlay); + transform-origin: top; + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + border-radius: var(--content-border-radius); + padding: 1rem; + right: 2rem; + top: 4rem; + min-width: 15rem; + border: 1px solid var(--surface-border); + + .layout-topbar-menu-content { + gap: 0.5rem; + } + + .layout-topbar-action { + display: flex; + width: 100%; + height: auto; + justify-content: flex-start; + border-radius: var(--content-border-radius); + padding: 0.5rem 1rem; + + i { + font-size: 1rem; + margin-right: 0.5rem; + } + + span { + font-weight: medium; + display: block; + } + } + } + + .layout-topbar-menu-content { + flex-direction: column; + } + } +} + +.config-panel { + .config-panel-label { + font-size: 0.875rem; + color: var(--text-secondary-color); + font-weight: 600; + line-height: 1; + } + + .config-panel-colors { + > div { + padding-top: 0.5rem; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: space-between; + + button { + border: none; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + padding: 0; + cursor: pointer; + outline-color: transparent; + outline-width: 2px; + outline-style: solid; + outline-offset: 1px; + + &.active-color { + outline-color: var(--primary-color); + } + } + } + } + + .config-panel-settings { + display: flex; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/app/assets/scss/layout/_typography.scss b/app/assets/scss/layout/_typography.scss new file mode 100644 index 0000000..be58cec --- /dev/null +++ b/app/assets/scss/layout/_typography.scss @@ -0,0 +1,68 @@ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.5rem 0 1rem 0; + font-family: inherit; + font-weight: 700; + line-height: 1.5; + color: var(--text-color); + + &:first-child { + margin-top: 0; + } +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +mark { + background: #fff8e1; + padding: 0.25rem 0.4rem; + border-radius: var(--content-border-radius); + font-family: monospace; +} + +blockquote { + margin: 1rem 0; + padding: 0 2rem; + border-left: 4px solid #90a4ae; +} + +hr { + border-top: solid var(--surface-border); + border-width: 1px 0 0 0; + margin: 1rem 0; +} + +p { + margin: 0 0 1rem 0; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/app/assets/scss/layout/_utils.scss b/app/assets/scss/layout/_utils.scss new file mode 100644 index 0000000..c5221d3 --- /dev/null +++ b/app/assets/scss/layout/_utils.scss @@ -0,0 +1,25 @@ +/* Utils */ +.clearfix:after { + content: " "; + display: block; + clear: both; +} + +.card { + background: var(--surface-card); + padding: 2rem; + margin-bottom: 2rem; + border-radius: var(--content-border-radius); + + &:last-child { + margin-bottom: 0; + } +} + +.p-toast { + &.p-toast-top-right, + &.p-toast-top-left, + &.p-toast-top-center { + top: 100px; + } +} diff --git a/app/assets/scss/layout/layout.scss b/app/assets/scss/layout/layout.scss new file mode 100644 index 0000000..faf39c3 --- /dev/null +++ b/app/assets/scss/layout/layout.scss @@ -0,0 +1,11 @@ +@use "./variables/_common"; +@use "./_mixins"; +@use "./_preloading"; +@use "./_core"; +@use "./_main"; +@use "./_topbar"; +@use "./_menu"; +@use "./_footer"; +@use "./_responsive"; +@use "./_utils"; +@use "./_typography"; diff --git a/app/assets/scss/layout/variables/_common.scss b/app/assets/scss/layout/variables/_common.scss new file mode 100644 index 0000000..5af2c5c --- /dev/null +++ b/app/assets/scss/layout/variables/_common.scss @@ -0,0 +1,26 @@ +:root { + --primary-color: var(--p-primary-color); + --primary-contrast-color: var(--p-primary-contrast-color); + --text-color: var(--p-text-color); + --text-color-secondary: var(--p-text-muted-color); + --surface-ground: var(--p-surface-100); + --surface-border: var(--p-content-border-color); + --surface-card: var(--p-content-background); + --surface-hover: var(--p-content-hover-background); + --surface-overlay: var(--p-overlay-popover-background); + --transition-duration: var(--p-transition-duration); + --maskbg: var(--p-mask-background); + --content-border-radius: var(--p-content-border-radius); + --layout-section-transition-duration: 0.2s; + --element-transition-duration: var(--p-transition-duration); + --focus-ring-width: var(--p-focus-ring-width); + --focus-ring-style: var(--p-focus-ring-style); + --focus-ring-color: var(--p-focus-ring-color); + --focus-ring-offset: var(--p-focus-ring-offset); + --focus-ring-shadow: var(--p-focus-ring-shadow); +} + +:root[class="app-dark"] { + --p-text-color: #ffffff; + --surface-ground: var(--p-surface-950); +} diff --git a/app/assets/scss/styles.scss b/app/assets/scss/styles.scss new file mode 100644 index 0000000..b434e58 --- /dev/null +++ b/app/assets/scss/styles.scss @@ -0,0 +1,3 @@ +@use "primeicons/primeicons.css"; +@use "~/assets/css/tailwind.css"; +@use "~/assets/scss/layout/layout.scss"; diff --git a/app/layouts/Default.vue b/app/layouts/Default.vue new file mode 100644 index 0000000..26b8245 --- /dev/null +++ b/app/layouts/Default.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/layouts/default/Footer.vue b/app/layouts/default/Footer.vue new file mode 100644 index 0000000..e14780f --- /dev/null +++ b/app/layouts/default/Footer.vue @@ -0,0 +1,9 @@ + + diff --git a/app/layouts/default/Sidebar.vue b/app/layouts/default/Sidebar.vue new file mode 100644 index 0000000..7f17150 --- /dev/null +++ b/app/layouts/default/Sidebar.vue @@ -0,0 +1,35 @@ + diff --git a/app/layouts/default/Topbar.vue b/app/layouts/default/Topbar.vue new file mode 100644 index 0000000..f7c1c0d --- /dev/null +++ b/app/layouts/default/Topbar.vue @@ -0,0 +1,56 @@ + + + diff --git a/app/pages/config/Account.vue b/app/pages/config/Account.vue new file mode 100644 index 0000000..556b876 --- /dev/null +++ b/app/pages/config/Account.vue @@ -0,0 +1,5 @@ + diff --git a/app/pages/config/Settings.vue b/app/pages/config/Settings.vue new file mode 100644 index 0000000..46d12c9 --- /dev/null +++ b/app/pages/config/Settings.vue @@ -0,0 +1,5 @@ + diff --git a/app/pages/index.vue b/app/pages/index.vue index 77130e0..b23f2a5 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,21 +1,5 @@ diff --git a/eslint.config.mjs b/eslint.config.mjs index bfbdce7..b801e08 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,6 +5,10 @@ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended" export default withNuxt(eslintPluginPrettierRecommended, { files: ["**/*.{ts,js,vue}"], rules: { + // Vue rules + "vue/no-multiple-template-root": "off", + + // Typescript rules "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-require-imports": "off", "@typescript-eslint/no-unused-vars": "warn", diff --git a/nuxt.config.ts b/nuxt.config.ts index 49bf60e..aa9f230 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,17 +1,77 @@ +import { definePreset } from "@primeuix/themes"; import Aura from "@primeuix/themes/aura"; -// https://nuxt.com/docs/api/configuration/nuxt-config -export default defineNuxtConfig({ - compatibilityDate: "2025-07-15", - devtools: { enabled: true }, - modules: ["@nuxt/eslint", "@nuxtjs/tailwindcss", "@primevue/nuxt-module"], - primevue: { - options: { - ripple: true, - theme: { - preset: Aura, - options: { - darkModeSelector: "system", +const defaultPreset = definePreset(Aura, { + semantic: { + primary: { + 50: "{teal.50}", + 100: "{teal.100}", + 200: "{teal.200}", + 300: "{teal.300}", + 400: "{teal.400}", + 500: "{teal.500}", + 600: "{teal.600}", + 700: "{teal.700}", + 800: "{teal.800}", + 900: "{teal.900}", + 950: "{teal.950}", + }, + colorScheme: { + light: { + surface: { + 0: "#ffffff", + 50: "{slate.50}", + 100: "{slate.100}", + 200: "{slate.200}", + 300: "{slate.300}", + 400: "{slate.400}", + 500: "{slate.500}", + 600: "{slate.600}", + 700: "{slate.700}", + 800: "{slate.800}", + 900: "{slate.900}", + 950: "{slate.950}", + }, + }, + dark: { + surface: { + 0: "#000000", + 50: "{slate.50}", + 100: "{slate.100}", + 200: "{slate.200}", + 300: "{slate.300}", + 400: "{slate.400}", + 500: "{slate.500}", + 600: "{slate.600}", + 700: "{slate.700}", + 800: "{slate.800}", + 900: "{slate.900}", + 950: "{slate.950}", + }, + }, + }, + }, +}); + +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: "2025-07-15", + devtools: { enabled: true }, + router: { + options: { + linkActiveClass: "active-route", + linkExactActiveClass: "exact-active-route", + }, + }, + modules: ["@nuxt/eslint", "@nuxtjs/tailwindcss", "@primevue/nuxt-module"], + css: ["~/assets/css/tailwind.css", "~/assets/scss/styles.scss"], + primevue: { + options: { + ripple: true, + theme: { + preset: defaultPreset, + options: { + darkModeSelector: ".app-dark", }, }, }, diff --git a/package-lock.json b/package-lock.json index d2562e7..5d959e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "@nuxtjs/tailwindcss": "^7.0.0-beta.1", "@primeuix/themes": "^1.2.5", "nuxt": "^4.1.3", + "primeicons": "^7.0.0", "primevue": "^4.4.1", "tailwindcss": "^4.1.16", "vue": "^3.5.22", @@ -26,6 +27,7 @@ "eslint-plugin-prettier": "^5.5.4", "happy-dom": "^20.0.8", "prettier": "^3.6.2", + "sass-embedded": "^1.93.3", "typescript": "^5.9.3", "vitest": "^4.0.1" } @@ -482,6 +484,13 @@ "node": ">=18" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz", + "integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==", + "devOptional": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@clack/core": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", @@ -5839,6 +5848,13 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "devOptional": true, + "license": "MIT/X11" + }, "node_modules/buffer-crc32": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", @@ -6365,6 +6381,13 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "license": "MIT" }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -8619,6 +8642,13 @@ "integrity": "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==", "license": "MIT" }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -11403,6 +11433,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, "node_modules/primevue": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz", @@ -11843,6 +11879,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -11863,6 +11909,374 @@ ], "license": "MIT" }, + "node_modules/sass": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.3.tgz", + "integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.3.tgz", + "integrity": "sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.93.3", + "sass-embedded-android-arm": "1.93.3", + "sass-embedded-android-arm64": "1.93.3", + "sass-embedded-android-riscv64": "1.93.3", + "sass-embedded-android-x64": "1.93.3", + "sass-embedded-darwin-arm64": "1.93.3", + "sass-embedded-darwin-x64": "1.93.3", + "sass-embedded-linux-arm": "1.93.3", + "sass-embedded-linux-arm64": "1.93.3", + "sass-embedded-linux-musl-arm": "1.93.3", + "sass-embedded-linux-musl-arm64": "1.93.3", + "sass-embedded-linux-musl-riscv64": "1.93.3", + "sass-embedded-linux-musl-x64": "1.93.3", + "sass-embedded-linux-riscv64": "1.93.3", + "sass-embedded-linux-x64": "1.93.3", + "sass-embedded-unknown-all": "1.93.3", + "sass-embedded-win32-arm64": "1.93.3", + "sass-embedded-win32-x64": "1.93.3" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.93.3.tgz", + "integrity": "sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.93.3" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.93.3.tgz", + "integrity": "sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.93.3.tgz", + "integrity": "sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.93.3.tgz", + "integrity": "sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.93.3.tgz", + "integrity": "sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.3.tgz", + "integrity": "sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.93.3.tgz", + "integrity": "sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.93.3.tgz", + "integrity": "sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.93.3.tgz", + "integrity": "sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.93.3.tgz", + "integrity": "sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.93.3.tgz", + "integrity": "sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.93.3.tgz", + "integrity": "sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.93.3.tgz", + "integrity": "sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.93.3.tgz", + "integrity": "sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.93.3.tgz", + "integrity": "sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.93.3.tgz", + "integrity": "sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==", + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.93.3" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.93.3.tgz", + "integrity": "sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.93.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.3.tgz", + "integrity": "sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -12444,6 +12858,29 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -12670,8 +13107,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "devOptional": true, + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -13148,6 +13585,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/vite": { "version": "7.1.11", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", diff --git a/package.json b/package.json index e9b64ae..4b88ba2 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@nuxtjs/tailwindcss": "^7.0.0-beta.1", "@primeuix/themes": "^1.2.5", "nuxt": "^4.1.3", + "primeicons": "^7.0.0", "primevue": "^4.4.1", "tailwindcss": "^4.1.16", "vue": "^3.5.22", @@ -34,6 +35,7 @@ "eslint-plugin-prettier": "^5.5.4", "happy-dom": "^20.0.8", "prettier": "^3.6.2", + "sass-embedded": "^1.93.3", "typescript": "^5.9.3", "vitest": "^4.0.1" } diff --git a/tests/layouts/Default.test.ts b/tests/layouts/Default.test.ts new file mode 100644 index 0000000..0b5f4f3 --- /dev/null +++ b/tests/layouts/Default.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; +import { mount, type VueWrapper } from "@vue/test-utils"; +import DefaultLayout from "~/layouts/Default.vue"; + +describe("Default.vue", () => { + const wrapper: VueWrapper = mount(DefaultLayout, {}); + + it("loads without crashing", () => { + expect(wrapper.exists()).toBe(true); + }); +}); diff --git a/tests/layouts/default/Footer.test.ts b/tests/layouts/default/Footer.test.ts new file mode 100644 index 0000000..5186608 --- /dev/null +++ b/tests/layouts/default/Footer.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { mount, type VueWrapper } from "@vue/test-utils"; +import Footer from "~/layouts/default/Footer.vue"; + +describe("Footer.vue", () => { + const RealDate = Date; + + const mockDate = (year: number) => { + globalThis.Date = class extends RealDate { + constructor() { + super(); + return new RealDate(`${year}-01-01`); + } + + static now() { + return new RealDate(`${year}-01-01`).getTime(); + } + }; + }; + + afterEach(() => { + globalThis.Date = RealDate; + }); + + it("loads without crashing", () => { + const wrapper: VueWrapper = mount(Footer, {}); + expect(wrapper.exists()).toBe(true); + expect(wrapper.find(".layout-footer").exists()).toBe(true); + expect(wrapper.find(".layout-footer").classes()).toContain("font-bold"); + }); + + it("displays only 'Glowing Fiesta 2025' when current year is 2025", () => { + mockDate(2025); + + const wrapper = mount(Footer); + expect(wrapper.text()).toBe("Glowing Fiesta 2025"); + expect(wrapper.text()).not.toContain(" - "); + }); + + it("displays 'Glowing Fiesta 2025 - 2034' when current year is 2034", () => { + mockDate(2034); + + const wrapper = mount(Footer); + expect(wrapper.text()).toBe("Glowing Fiesta 2025 - 2034"); + }); + + it("has proper structure / content", () => { + const wrapper = mount(Footer); + const footer = wrapper.find("footer"); + + expect(footer.exists()).toBe(true); + expect(footer.element.tagName).toBe("FOOTER"); + expect(wrapper.findAll("div")).toHaveLength(1); + }); +}); diff --git a/tests/layouts/default/Sidebar.test.ts b/tests/layouts/default/Sidebar.test.ts new file mode 100644 index 0000000..5b33d9a --- /dev/null +++ b/tests/layouts/default/Sidebar.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; +import { mount, type VueWrapper } from "@vue/test-utils"; +import Sidebar from "~/layouts/default/Sidebar.vue"; + +describe("Sidebar.vue", () => { + it("loads without crashing", () => { + const wrapper: VueWrapper = mount(Sidebar, {}); + expect(wrapper.exists()).toBe(true); + expect(wrapper.find(".layout-sidebar").exists()).toBe(true); + }); +}); diff --git a/tests/layouts/default/Topbar.test.ts b/tests/layouts/default/Topbar.test.ts new file mode 100644 index 0000000..4e851f6 --- /dev/null +++ b/tests/layouts/default/Topbar.test.ts @@ -0,0 +1,155 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mount, type VueWrapper } from "@vue/test-utils"; +import Topbar from "~/layouts/default/Topbar.vue"; + +describe("Topbar.vue", () => { + let wrapper: VueWrapper; + + beforeEach(() => { + document.documentElement.className = ""; + document.body.innerHTML = '
'; + wrapper = mount(Topbar); + }); + + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); + + describe("Component Rendering", () => { + it("loads without crashing", () => { + expect(wrapper.exists()).toBe(true); + expect(wrapper.find(".layout-topbar").exists()).toBe(true); + }); + + it("renders the logo image and text", () => { + const logo = wrapper.find(".layout-topbar-logo"); + expect(logo.exists()).toBe(true); + expect(logo.find("img").attributes("alt")).toBe("Two stick silhouettes admiring fireworks in the sky"); + expect(logo.text()).toContain("Glowing Fiesta"); + }); + + it("renders the menu toggle button", () => { + const menuButton = wrapper.find(".layout-menu-button"); + expect(menuButton.exists()).toBe(true); + expect(menuButton.find(".pi-bars").exists()).toBe(true); + }); + + it("renders the dark mode toggle button", () => { + const darkModeButton = wrapper.find(".layout-topbar-action-highlight"); + expect(darkModeButton.exists()).toBe(true); + }); + + it("renders the topbar menu items", () => { + const menuButtons = wrapper.findAll(".layout-topbar-menu button"); + expect(menuButtons).toHaveLength(3); + expect(menuButtons[0].text()).toContain("Calendar"); + expect(menuButtons[1].text()).toContain("Messages"); + expect(menuButtons[2].text()).toContain("Profile"); + }); + }); + + describe("Dark Mode Toggle", () => { + it("starts with sun icon (light mode)", () => { + const darkModeButton = wrapper.find(".layout-topbar-action-highlight"); + expect(darkModeButton.find(".pi-sun").exists()).toBe(true); + expect(darkModeButton.find(".pi-moon").exists()).toBe(false); + }); + + it("toggles to moon icon when clicked", async () => { + const darkModeButton = wrapper.find(".layout-topbar-action-highlight"); + await darkModeButton.trigger("click"); + + expect(darkModeButton.find(".pi-moon").exists()).toBe(true); + expect(darkModeButton.find(".pi-sun").exists()).toBe(false); + }); + + it("removes app-dark class when toggled off", async () => { + expect(document.documentElement.classList.contains("app-dark")).toBe(false); + const darkModeButton = wrapper.find(".layout-topbar-action-highlight"); + + // Toggle on + await darkModeButton.trigger("click"); + expect(document.documentElement.classList.contains("app-dark")).toBe(true); + + // Toggle off + await darkModeButton.trigger("click"); + expect(document.documentElement.classList.contains("app-dark")).toBe(false); + }); + + it("updates isDarkTheme ref when toggled", async () => { + const darkModeButton = wrapper.find(".layout-topbar-action-highlight"); + + expect(wrapper.vm.isDarkTheme).toBe(false); + + await darkModeButton.trigger("click"); + expect(wrapper.vm.isDarkTheme).toBe(true); + + await darkModeButton.trigger("click"); + expect(wrapper.vm.isDarkTheme).toBe(false); + }); + }); + + describe("Menu Toggle", () => { + it("toggles layout-static-inactive class on layout wrapper", async () => { + const menuButton = wrapper.find(".layout-menu-button"); + const layoutWrapper = document.querySelector(".layout-wrapper"); + + expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(false); + + await menuButton.trigger("click"); + expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(true); + + await menuButton.trigger("click"); + expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(false); + }); + + it("handles missing layout wrapper gracefully", async () => { + document.body.innerHTML = ""; + + const menuButton = wrapper.find(".layout-menu-button"); + + // Should not throw error + expect(() => menuButton.trigger("click")).not.toThrow(); + }); + }); + + describe("Logo Link", () => { + it("links to home page", () => { + const logoLink = wrapper.find(".layout-topbar-logo"); + expect(logoLink.exists()).toBe(true); + }); + + it("has correct image source", () => { + const img = wrapper.find(".layout-topbar-logo img"); + expect(img.attributes("src")).toContain("logo.png"); + }); + }); + + describe("Topbar Actions", () => { + it("renders Calendar action button with icon", () => { + const buttons = wrapper.findAll(".layout-topbar-menu button"); + const calendarButton = buttons[0]; + + expect(calendarButton.find(".pi-calendar").exists()).toBe(true); + expect(calendarButton.text()).toContain("Calendar"); + }); + + it("renders Messages action button with icon", () => { + const buttons = wrapper.findAll(".layout-topbar-menu button"); + const messagesButton = buttons[1]; + + expect(messagesButton.find(".pi-inbox").exists()).toBe(true); + expect(messagesButton.text()).toContain("Messages"); + }); + + it("renders Profile action button with icon", () => { + const buttons = wrapper.findAll(".layout-topbar-menu button"); + const profileButton = buttons[2]; + + expect(profileButton.find(".pi-user").exists()).toBe(true); + expect(profileButton.text()).toContain("Profile"); + }); + }); +}); diff --git a/tests/pages/config/Account.test.ts b/tests/pages/config/Account.test.ts new file mode 100644 index 0000000..96ed16d --- /dev/null +++ b/tests/pages/config/Account.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; +import { mount, type VueWrapper } from "@vue/test-utils"; +import AccountPage from "~/pages/config/Account.vue"; + +describe("Account.vue", () => { + const wrapper: VueWrapper = mount(AccountPage, {}); + + it("loads without crashing", () => { + expect(wrapper.exists()).toBe(true); + }); +}); diff --git a/tests/pages/config/Settings.test.ts b/tests/pages/config/Settings.test.ts new file mode 100644 index 0000000..acebecd --- /dev/null +++ b/tests/pages/config/Settings.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; +import { mount, type VueWrapper } from "@vue/test-utils"; +import SettingsPage from "~/pages/config/Settings.vue"; + +describe("Settings.vue", () => { + const wrapper: VueWrapper = mount(SettingsPage, {}); + + it("loads without crashing", () => { + expect(wrapper.exists()).toBe(true); + }); +}); diff --git a/tests/pages/index.test.ts b/tests/pages/index.test.ts index 9ad7de2..efc1a96 100644 --- a/tests/pages/index.test.ts +++ b/tests/pages/index.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { mount, type VueWrapper } from "@vue/test-utils"; -import IndexPage from "~/app/pages/index.vue"; +import IndexPage from "~/pages/index.vue"; describe("pages/index.vue", () => { const wrapper: VueWrapper = mount(IndexPage, {}); diff --git a/tests/setup.ts b/tests/setup.ts index d9f8d40..76db9b5 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -2,6 +2,7 @@ import { vi } from "vitest"; import { config } from "@vue/test-utils"; import PrimeVue from "primevue/config"; import Button from "primevue/button"; +import Ripple from "primevue/ripple"; Object.defineProperty(global, "import", { value: { @@ -13,5 +14,11 @@ Object.defineProperty(global, "import", { }); config.global.plugins = [PrimeVue]; -config.global.stubs = { NuxtPage: true }; +config.global.stubs = { + NuxtLayout: true, + NuxtPage: true, + Divider: true, + NuxtLink: { template: "" }, +}; config.global.components = { Button }; +config.global.directives = { Ripple }; diff --git a/vitest.config.ts b/vitest.config.ts index a787fb4..07f384e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -38,8 +38,8 @@ export default defineConfig({ }, resolve: { alias: { - "~": fileURLToPath(new URL("./", import.meta.url)), - "@": fileURLToPath(new URL("./", import.meta.url)), + "~": fileURLToPath(new URL("./app", import.meta.url)), + "@": fileURLToPath(new URL("./app", import.meta.url)), }, }, });