Compare commits

..

No commits in common. "b5b082bc5dd7f06e2dc2dbb04da2b6eb4ab46c56" and "866155e25e9d32ea540b1cb2b76682d3d8fb780b" have entirely different histories.

46 changed files with 1444 additions and 5515 deletions

View file

@ -1,11 +0,0 @@
## Description
### Proposed Changes
-
-
### Checklist before submitting
- [ ] I followed the guidelines in our [Contributing document](https://github.com/lburcusel/glowing-fiesta/blob/production/CONTRIBUTING.md)
- [ ] My submission pass all tests

View file

@ -1,36 +0,0 @@
name: Sonar
permissions:
contents: read
pull-requests: write
on:
push:
branches:
- production
pull_request:
branches:
- production
types: [opened, synchronize, reopened]
jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup node environment
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests and generate coverage
run: npm run coverage
continue-on-error: true
env:
CI: true
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View file

@ -1,8 +0,0 @@
{
"printWidth": 128,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "es5"
}

22
.vscode/settings.json vendored
View file

@ -1,22 +0,0 @@
{
"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"
]
}

View file

@ -1,49 +0,0 @@
# Security Policy
Glowing Fiesta is unreleased software still in early development, and so bugs and vulnerabilities in its code can be safely
disclosed publicly. The preference is to report security issues as
[GitHub issues](https://github.com/lburcusel/glowing-fiesta/issues/new?template=bug_report.md).
However, private vulnerability reporting is also enabled on the repository. If you find a security issue in Glowing Fiesta,
or in another package that you believe affects Glowing Fiesta, you may report it privately to the maintainers
using the [process outlined in GitHub documentation](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/creating-a-repository-security-advisory).
Issues reported and accepted through the private reporting process will be disclosed publicly once they are resolved,
and given a security advisory identifier. The maintainers may include regular contributors in the disposition and resolution
process as their expertise requires. Researchers who report security issues privately will be credited in the advisory.
The maintainers reserve the right to reject reports that are not security issues, or that are not in the scope of Glowing Fiesta.
For issues that are determined to not be security issues, please report them as a
[GitHub issue](https://github.com/lburcusel/glowing-fiesta/issues/new?template=bug_report.md) instead. If you choose not to
re-report the issue as a generic issue, the maintainers may do so themselves.
Glowing Fiesta does not offer bug bounties for security issues at this time.
## Scope of Security Issues
Many security features of the web platform are not yet implemented in Glowing Fiesta. Security reports regarding
incomplete features may be redirected to regular issues. The following are examples of issues that are not in scope
at this time:
- Cross-site request forgery
- Cross-site scripting
- Content Security Policy violations
- Cross-origin iframe sandboxing
The maintainers reserve the right to modify this list as the project matures and as security issues are reported.
Significant portions of the browser depend on third party libraries. Examples include image decoding, video decoding,
internationalization, and 2D graphics. Security issues in these libraries should be reported to the maintainers of the
respective libraries. The maintainers of Glowing Fiesta will work with the maintainers of these libraries to resolve the issue.
If a security issue relates more to the integration of the library into Glowing Fiesta, it should be reported via the same
methods as other security issues.
## Responsible Disclosure
The maintainers of Glowing Fiesta will work with security researchers to resolve security issues in a timely manner. A default
120-day disclosure timeline is in place for all security issues, but this may be extended if the maintainers and the reporter
agree that more time is needed to resolve the issue. The maintainers will keep the reporter informed of progress and
resolution steps throughout the process.
In the case that a security issue is also reported to other package vendors or OSS projects, the maintainers will work
with the longest disclosure timeline to ensure that all parties have sufficient time to resolve the issue.

View file

@ -1,8 +1,6 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<NuxtWelcome />
</div>
</template>

View file

@ -1 +0,0 @@
@import "tailwindcss";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,24 +0,0 @@
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;
}

View file

@ -1,8 +0,0 @@
.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);
}

View file

@ -1,14 +0,0 @@
.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;
}

View file

@ -1,160 +0,0 @@
@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;
}

View file

@ -1,15 +0,0 @@
@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);
}

View file

@ -1,48 +0,0 @@
.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;
}
}

View file

@ -1,110 +0,0 @@
@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;
}
}
}
}

View file

@ -1,201 +0,0 @@
@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;
}
}

View file

@ -1,68 +0,0 @@
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;
}
}

View file

@ -1,25 +0,0 @@
/* 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;
}
}

View file

@ -1,11 +0,0 @@
@use "./variables/_common";
@use "./_mixins";
@use "./_preloading";
@use "./_core";
@use "./_main";
@use "./_topbar";
@use "./_menu";
@use "./_footer";
@use "./_responsive";
@use "./_utils";
@use "./_typography";

View file

@ -1,26 +0,0 @@
: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);
}

View file

@ -1,3 +0,0 @@
@use "primeicons/primeicons.css";
@use "~/assets/css/tailwind.css";
@use "~/assets/scss/layout/layout.scss";

View file

@ -1,21 +0,0 @@
<template>
<div class="layout-wrapper layout-static">
<default-topbar />
<default-sidebar />
<div class="layout-main-container">
<div class="layout-main">
<slot />
</div>
<default-footer />
</div>
<div class="layout-mask animate-fadein"></div>
</div>
<Toast />
</template>
<script setup lang="ts">
import Toast from "primevue/toast";
import DefaultTopbar from "~/layouts/default/Topbar.vue";
import DefaultSidebar from "~/layouts/default/Sidebar.vue";
import DefaultFooter from "~/layouts/default/Footer.vue";
</script>

View file

@ -1,9 +0,0 @@
<script setup lang="ts">
const currentYear = new Date().getFullYear();
</script>
<template>
<footer class="layout-footer font-bold">
<div v-if="currentYear === 2025">Glowing Fiesta 2025</div>
<div v-else>Glowing Fiesta 2025 - {{ currentYear }}</div>
</footer>
</template>

View file

@ -1,35 +0,0 @@
<template>
<div class="layout-sidebar">
<ul class="layout-menu">
<li class="layout-root-menuitem">
<div class="layout-menuitem-root-text">Home</div>
<ul class="layout-submenu">
<li>
<nuxt-link to="/">
<i class="pi pi-home layout-menuitem-icon"></i>
<span class="layout-menuitem-text">Dashboard</span>
</nuxt-link>
</li>
</ul>
</li>
<li class="layout-root-menuitem">
<div class="layout-menuitem-root-text">Configuration</div>
<ul class="layout-submenu">
<li>
<nuxt-link to="/config/account">
<i class="pi pi-user layout-menuitem-icon"></i>
<span class="layout-menuitem-text">Account</span>
</nuxt-link>
</li>
<li>
<nuxt-link to="/config/settings">
<i class="pi pi-cog layout-menuitem-icon"></i>
<span class="layout-menuitem-text">Settings</span>
</nuxt-link>
</li>
</ul>
</li>
</ul>
</div>
</template>

View file

@ -1,56 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
const isDarkTheme = ref(false);
const toggleDarkMode = () => {
isDarkTheme.value = !isDarkTheme.value;
document.documentElement.classList.toggle("app-dark");
};
const toggleMenu = () => {
document.getElementsByClassName("layout-wrapper")[0]?.classList.toggle("layout-static-inactive");
};
</script>
<template>
<nav class="layout-topbar">
<div class="layout-topbar-logo-container">
<button class="layout-menu-button layout-topbar-action" @click="toggleMenu">
<i class="pi pi-bars"></i>
</button>
<nuxt-link to="/" class="layout-topbar-logo">
<img src="@/assets/images/logo.png" alt="Two stick silhouettes admiring fireworks in the sky" />
<span>Glowing Fiesta</span>
</nuxt-link>
</div>
<div class="layout-topbar-actions">
<div class="layout-config-menu">
<button type="button" class="layout-topbar-action layout-topbar-action-highlight" @click="toggleDarkMode">
<i :class="['pi', { 'pi-moon': isDarkTheme, 'pi-sun': !isDarkTheme }]"></i>
</button>
</div>
<Divider layout="vertical" />
<div class="layout-topbar-menu hidden lg:block">
<div class="layout-topbar-menu-content">
<button type="button" class="layout-topbar-action">
<i class="pi pi-calendar"></i>
<span>Calendar</span>
</button>
<button type="button" class="layout-topbar-action">
<i class="pi pi-inbox"></i>
<span>Messages</span>
</button>
<button type="button" class="layout-topbar-action">
<i class="pi pi-user"></i>
<span>Profile</span>
</button>
</div>
</div>
</div>
</nav>
</template>

View file

@ -1,5 +0,0 @@
<template>
<div class="card">
<h1>Account</h1>
</div>
</template>

View file

@ -1,5 +0,0 @@
<template>
<div class="card">
<h1>Settings</h1>
</div>
</template>

View file

@ -1,5 +0,0 @@
<template>
<div class="card">
<h1>Dashboard</h1>
</div>
</template>

View file

@ -1,16 +1,10 @@
// @ts-check
import withNuxt from "./.nuxt/eslint.config.mjs";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
export default withNuxt(eslintPluginPrettierRecommended, {
export default withNuxt({
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",
},
});

View file

@ -1,79 +1,6 @@
import { definePreset } from "@primeuix/themes";
import Aura from "@primeuix/themes/aura";
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",
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",
},
},
},
},
});
modules: ['@nuxt/eslint']
})

5467
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,34 +9,16 @@
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lint-files": "eslint --ext .js,.ts,.vue",
"vitest": "vitest",
"test": "vitest run",
"coverage": "vitest run --coverage"
"lint-files": "eslint --ext .js,.ts,.vue"
},
"dependencies": {
"@nuxtjs/tailwindcss": "^7.0.0-beta.1",
"@primeuix/themes": "^2.0.1",
"@nuxt/eslint": "^1.9.0",
"eslint": "^9.38.0",
"nuxt": "^4.1.3",
"primeicons": "^7.0.0",
"primevue": "^4.4.1",
"tailwindcss": "^4.1.17",
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@nuxt/eslint": "^1.9.0",
"@primevue/nuxt-module": "^4.4.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vitest/coverage-v8": "^4.0.1",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"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"
"typescript": "^5.9.3"
}
}

View file

@ -1,12 +0,0 @@
sonar.projectKey=lburcusel_glowing-fiesta
sonar.organization=lburcusel
sonar.projectName=Glowing Fiesta
sonar.projectVersion=1.0.0
sonar.sourceEncoding=UTF-8
sonar.sources=app, tests
sonar.inclusions=app/**/*.ts, app/**/*.js, app/**/*.vue, app/**/*.css, app/**/*.scss, tests/**/*.test.ts
sonar.exclusions=**/node_modules/**, **/coverage/**, *.config.ts
sonar.coverage.exclusions=tests/**, *.config.ts
sonar.javascript.lcov.reportPaths=coverage/lcov.info
# sonar.testExecutionReportPaths=coverage/sonar-report.xml

View file

View file

@ -1,18 +0,0 @@
import { describe, expect, it } from "vitest";
import type { VueWrapper } from "@vue/test-utils";
import { mount } from "@vue/test-utils";
import App from "../app/app.vue";
describe("app.vue", () => {
const wrapper: VueWrapper = mount(App, {
global: {
stubs: {
NuxtRouteAnnouncer: true,
NuxtWelcome: true,
},
},
});
it("renders without crashing", () => {
expect(wrapper.exists()).toBe(true);
});
});

View file

@ -1,11 +0,0 @@
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);
});
});

View file

@ -1,54 +0,0 @@
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(...args: any[]) {
super(args.length === 0 ? `${year}-01-01` : args[0]);
}
static now() {
return new RealDate(`${year}-01-01`).getTime();
}
} as any as DateConstructor;
};
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);
});
});

View file

@ -1,11 +0,0 @@
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);
});
});

View file

@ -1,155 +0,0 @@
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 = '<div class="layout-wrapper"></div>';
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");
});
});
});

View file

@ -1,11 +0,0 @@
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);
});
});

View file

@ -1,11 +0,0 @@
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);
});
});

View file

@ -1,11 +0,0 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import IndexPage from "~/pages/index.vue";
describe("pages/index.vue", () => {
const wrapper: VueWrapper = mount(IndexPage, {});
it("loads without crashing", () => {
expect(wrapper.exists()).toBe(true);
});
});

View file

@ -1,24 +0,0 @@
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: {
meta: {
glob: vi.fn(() => ({})),
},
},
writable: true,
});
config.global.plugins = [PrimeVue];
config.global.stubs = {
NuxtLayout: true,
NuxtPage: true,
Divider: true,
NuxtLink: { template: "<a><slot /></a>" },
};
config.global.components = { Button };
config.global.directives = { Ripple };

View file

@ -2,10 +2,23 @@
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{ "path": "./.nuxt/tsconfig.app.json" },
{ "path": "./.nuxt/tsconfig.server.json" },
{ "path": "./.nuxt/tsconfig.shared.json" },
{ "path": "./.nuxt/tsconfig.node.json" },
{ "path": "./tsconfig.test.json" }
]
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
],
"compilerOptions": {
"paths": {
"~/*": ["./src/*"],
"@/*": ["./src/*"]
}
}
}

View file

@ -1,11 +0,0 @@
{
"compilerOptions": {
"paths": {
"~": ["./app"],
"~/*": ["./app/*"],
"@": ["./app"],
"@/*": ["./app/*"]
}
},
"include": ["./tests/**/*"]
}

View file

@ -1,45 +0,0 @@
import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
setupFiles: ["./tests/setup.ts"],
environment: "happy-dom",
include: ["tests/**/*.test.ts"],
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
clean: true,
cleanOnRerun: true,
include: ["**/*.{js,jsx,ts,tsx,vue}"],
exclude: [
"node_modules/**",
"dist/**",
"coverage/**",
"**/*.test.ts",
"tests/mocks/**",
// Exclude Nuxt generated files
".nuxt/**",
".output/**",
// Exclude TypeScript declaration files
"**/*.d.ts",
// Exclude config files
"*.config.*",
"assets/icons/**",
],
},
name: "GFiesta",
},
resolve: {
alias: {
"~": fileURLToPath(new URL("./app", import.meta.url)),
"@": fileURLToPath(new URL("./app", import.meta.url)),
},
},
});