Compare commits

...

10 commits

Author SHA1 Message Date
b5b082bc5d
Updated packages and fixed identified CS and typo in sonar-project.properties
Some checks failed
Sonar / SonarQube (push) Has been cancelled
2025-12-03 11:09:23 +01:00
defa605e45
Package update. 2025-11-15 11:16:19 +01:00
Liviu Burcusel
d1967f718e
Added default layout to project
* Added default layout for the site

* App is in light mode by default.

* App is in light mode by default.
2025-11-12 12:50:56 +01:00
dependabot[bot]
9a4dd4b721
Bump tar from 7.5.1 to 7.5.2 (#11)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.1 to 7.5.2.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.2)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-01 14:08:26 +01:00
Liviu Burcusel
82acb354ca
8 primevue install (#9)
* Added PrimeVue with TailwindCSS support and modified Homepage to test the changes.

* Configured vitest to use PrimeVue and wrote a quick test for home page
2025-10-26 19:54:14 +01:00
b27e3655d1
Added security policy and PR template 2025-10-26 11:00:46 +01:00
Liviu Burcusel
3b58a25ccf
Added vitest (#5)
* Moved eslint and @nuxt/eslint to devDependencies

* Added vitest to project and 1 test

* Tweaked Sonar workflow in order to run tests and coverage

* Removed offending config line
2025-10-24 17:20:05 +02:00
c98879430b
Added permissions to workflow (PR #4) 2025-10-23 16:10:08 +02:00
7df500cb99 Added Prettier to project 2025-10-23 15:52:16 +02:00
61184f57a5
Added Sonar workflow 2025-10-22 15:51:40 +02:00
46 changed files with 5518 additions and 1447 deletions

11
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,11 @@
## 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

36
.github/workflows/sonar.yml vendored Normal file
View file

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

8
.prettierrc.json Normal file
View file

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

22
.vscode/settings.json vendored Normal file
View file

@ -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"
]
}

49
SECURITY.md Normal file
View file

@ -0,0 +1,49 @@
# 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,6 +1,8 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>

View file

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

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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";

View file

@ -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);
}

View file

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

21
app/layouts/Default.vue Normal file
View file

@ -0,0 +1,21 @@
<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

@ -0,0 +1,9 @@
<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

@ -0,0 +1,35 @@
<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

@ -0,0 +1,56 @@
<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

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

View file

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

5
app/pages/index.vue Normal file
View file

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

View file

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

View file

@ -1,6 +1,79 @@
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 },
modules: ['@nuxt/eslint']
})
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",
},
},
},
},
});

5473
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,16 +9,34 @@
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lint-files": "eslint --ext .js,.ts,.vue"
"lint-files": "eslint --ext .js,.ts,.vue",
"vitest": "vitest",
"test": "vitest run",
"coverage": "vitest run --coverage"
},
"dependencies": {
"@nuxt/eslint": "^1.9.0",
"eslint": "^9.38.0",
"@nuxtjs/tailwindcss": "^7.0.0-beta.1",
"@primeuix/themes": "^2.0.1",
"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": {
"typescript": "^5.9.3"
"@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"
}
}

12
sonar-project.properties Normal file
View file

@ -0,0 +1,12 @@
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

0
tests/.keep Normal file
View file

18
tests/app.test.ts Normal file
View file

@ -0,0 +1,18 @@
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

@ -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);
});
});

View file

@ -0,0 +1,54 @@
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

@ -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);
});
});

View file

@ -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 = '<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

@ -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);
});
});

View file

@ -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);
});
});

11
tests/pages/index.test.ts Normal file
View file

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

24
tests/setup.ts Normal file
View file

@ -0,0 +1,24 @@
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,23 +2,10 @@
// 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"
}
],
"compilerOptions": {
"paths": {
"~/*": ["./src/*"],
"@/*": ["./src/*"]
}
}
{ "path": "./.nuxt/tsconfig.app.json" },
{ "path": "./.nuxt/tsconfig.server.json" },
{ "path": "./.nuxt/tsconfig.shared.json" },
{ "path": "./.nuxt/tsconfig.node.json" },
{ "path": "./tsconfig.test.json" }
]
}

11
tsconfig.test.json Normal file
View file

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

45
vitest.config.ts Normal file
View file

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