aggiunto e testato quasar apps
8
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: tw-build tw-watch htmx-copy flowbite-copy assets server test db-reset fmt
|
||||
.PHONY: tw-build tw-watch htmx-copy flags-copy assets server test db-reset fmt
|
||||
|
||||
tw-build:
|
||||
npm run tw:build
|
||||
|
|
@ -9,10 +9,10 @@ tw-watch:
|
|||
htmx-copy:
|
||||
mkdir -p web/static/vendor && cp node_modules/htmx.org/dist/htmx.min.js web/static/vendor/htmx.min.js
|
||||
|
||||
flowbite-copy:
|
||||
mkdir -p web/static/vendor && cp node_modules/flowbite/dist/flowbite.min.js web/static/vendor/flowbite.js
|
||||
flags-copy:
|
||||
mkdir -p web/static/vendor/flags && cp assets/flags/*.svg web/static/vendor/flags/
|
||||
|
||||
assets: htmx-copy flowbite-copy tw-build
|
||||
assets: htmx-copy flags-copy tw-build
|
||||
|
||||
server:
|
||||
go run ./cmd/server
|
||||
|
|
|
|||
12
README.md
|
|
@ -18,6 +18,14 @@ Terminale 2:
|
|||
make server
|
||||
```
|
||||
|
||||
Admin SPA (Quasar):
|
||||
- il backend serve `quasar/admin_section/dist/spa` sotto `/admin` (protetto da auth + ruolo admin)
|
||||
- build frontend admin: `cd quasar/admin_section && npm i && npm run build`
|
||||
|
||||
Private SPA (Quasar):
|
||||
- il backend serve `quasar/private_section/dist/spa` sotto `/private` (protetto da auth)
|
||||
- build frontend private: `cd quasar/private_section && npm i && npm run build`
|
||||
|
||||
`make assets` esegue:
|
||||
- copia di `node_modules/flowbite/dist/flowbite.min.js` in `web/static/vendor/flowbite.js`
|
||||
- build Tailwind in `web/static/css/app.css`
|
||||
|
|
@ -65,8 +73,8 @@ DB_PG_DSN=postgres://trustcontact:trustcontact@localhost:5432/trustcontact?sslmo
|
|||
## Template Directories
|
||||
|
||||
- Public: `web/templates/public`
|
||||
- Private: `web/templates/private`
|
||||
- Admin: `web/templates/admin`
|
||||
- Private: `quasar/private_section/dist/spa` (SPA servita da Go sotto `/private`)
|
||||
- Admin: `quasar/admin_section/dist/spa` (SPA servita da Go sotto `/admin`)
|
||||
|
||||
## Email in Develop
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Swiss flag">
|
||||
<rect width="32" height="32" fill="#d52b1e"/>
|
||||
<rect x="13" y="6" width="6" height="20" fill="#ffffff"/>
|
||||
<rect x="6" y="13" width="20" height="6" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 271 B |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="German flag">
|
||||
<rect width="48" height="10.67" x="0" y="0" fill="#000000"/>
|
||||
<rect width="48" height="10.67" x="0" y="10.67" fill="#dd0000"/>
|
||||
<rect width="48" height="10.66" x="0" y="21.34" fill="#ffce00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="English flag">
|
||||
<rect width="48" height="32" fill="#012169"/>
|
||||
<path d="M0 0 48 32M48 0 0 32" stroke="#ffffff" stroke-width="6"/>
|
||||
<path d="M0 0 48 32M48 0 0 32" stroke="#c8102e" stroke-width="3"/>
|
||||
<rect x="20" y="0" width="8" height="32" fill="#ffffff"/>
|
||||
<rect x="0" y="12" width="48" height="8" fill="#ffffff"/>
|
||||
<rect x="22" y="0" width="4" height="32" fill="#c8102e"/>
|
||||
<rect x="0" y="14" width="48" height="4" fill="#c8102e"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 531 B |
|
|
@ -0,0 +1,53 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="United States flag">
|
||||
<rect width="48" height="32" fill="#b22234"/>
|
||||
<g fill="#ffffff">
|
||||
<rect y="2.46" width="48" height="2.46"/>
|
||||
<rect y="7.38" width="48" height="2.46"/>
|
||||
<rect y="12.30" width="48" height="2.46"/>
|
||||
<rect y="17.22" width="48" height="2.46"/>
|
||||
<rect y="22.14" width="48" height="2.46"/>
|
||||
<rect y="27.06" width="48" height="2.46"/>
|
||||
</g>
|
||||
<rect width="20" height="17.23" fill="#3c3b6e"/>
|
||||
<g fill="#ffffff">
|
||||
<circle cx="2.2" cy="2.2" r="0.7"/>
|
||||
<circle cx="5.4" cy="2.2" r="0.7"/>
|
||||
<circle cx="8.6" cy="2.2" r="0.7"/>
|
||||
<circle cx="11.8" cy="2.2" r="0.7"/>
|
||||
<circle cx="15.0" cy="2.2" r="0.7"/>
|
||||
<circle cx="18.2" cy="2.2" r="0.7"/>
|
||||
<circle cx="3.8" cy="4.4" r="0.7"/>
|
||||
<circle cx="7.0" cy="4.4" r="0.7"/>
|
||||
<circle cx="10.2" cy="4.4" r="0.7"/>
|
||||
<circle cx="13.4" cy="4.4" r="0.7"/>
|
||||
<circle cx="16.6" cy="4.4" r="0.7"/>
|
||||
<circle cx="2.2" cy="6.6" r="0.7"/>
|
||||
<circle cx="5.4" cy="6.6" r="0.7"/>
|
||||
<circle cx="8.6" cy="6.6" r="0.7"/>
|
||||
<circle cx="11.8" cy="6.6" r="0.7"/>
|
||||
<circle cx="15.0" cy="6.6" r="0.7"/>
|
||||
<circle cx="18.2" cy="6.6" r="0.7"/>
|
||||
<circle cx="3.8" cy="8.8" r="0.7"/>
|
||||
<circle cx="7.0" cy="8.8" r="0.7"/>
|
||||
<circle cx="10.2" cy="8.8" r="0.7"/>
|
||||
<circle cx="13.4" cy="8.8" r="0.7"/>
|
||||
<circle cx="16.6" cy="8.8" r="0.7"/>
|
||||
<circle cx="2.2" cy="11" r="0.7"/>
|
||||
<circle cx="5.4" cy="11" r="0.7"/>
|
||||
<circle cx="8.6" cy="11" r="0.7"/>
|
||||
<circle cx="11.8" cy="11" r="0.7"/>
|
||||
<circle cx="15.0" cy="11" r="0.7"/>
|
||||
<circle cx="18.2" cy="11" r="0.7"/>
|
||||
<circle cx="3.8" cy="13.2" r="0.7"/>
|
||||
<circle cx="7.0" cy="13.2" r="0.7"/>
|
||||
<circle cx="10.2" cy="13.2" r="0.7"/>
|
||||
<circle cx="13.4" cy="13.2" r="0.7"/>
|
||||
<circle cx="16.6" cy="13.2" r="0.7"/>
|
||||
<circle cx="2.2" cy="15.4" r="0.7"/>
|
||||
<circle cx="5.4" cy="15.4" r="0.7"/>
|
||||
<circle cx="8.6" cy="15.4" r="0.7"/>
|
||||
<circle cx="11.8" cy="15.4" r="0.7"/>
|
||||
<circle cx="15.0" cy="15.4" r="0.7"/>
|
||||
<circle cx="18.2" cy="15.4" r="0.7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="French flag">
|
||||
<rect width="16" height="32" x="0" y="0" fill="#0055a4"/>
|
||||
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
|
||||
<rect width="16" height="32" x="32" y="0" fill="#ef4135"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Italian flag">
|
||||
<rect width="16" height="32" x="0" y="0" fill="#009246"/>
|
||||
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
|
||||
<rect width="16" height="32" x="32" y="0" fill="#ce2b37"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 287 B |
|
|
@ -1,4 +1,5 @@
|
|||
@import "tailwindcss";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@layer utilities {
|
||||
.flag-lang {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,27 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AdminController struct{}
|
||||
type AdminController struct {
|
||||
spaDir string
|
||||
}
|
||||
|
||||
func NewAdminController() *AdminController {
|
||||
return &AdminController{}
|
||||
func NewAdminController(spaDir string) *AdminController {
|
||||
return &AdminController{spaDir: spaDir}
|
||||
}
|
||||
|
||||
func (ac *AdminController) Dashboard(c *fiber.Ctx) error {
|
||||
return renderAdminPage(c, "Admin")
|
||||
return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
|
||||
}
|
||||
|
||||
func renderAdminPage(c *fiber.Ctx, title string) error {
|
||||
viewData := map[string]any{
|
||||
"Title": title,
|
||||
"NavSection": "admin",
|
||||
}
|
||||
for k, v := range localsTemplateData(c) {
|
||||
viewData[k] = v
|
||||
}
|
||||
|
||||
files := []string{
|
||||
"web/templates/layout.html",
|
||||
"web/templates/public/_navbar.html",
|
||||
"web/templates/partials/language_dropdown.html",
|
||||
"web/templates/public/_flash.html",
|
||||
"web/templates/admin/admin.html",
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := tmpl.ExecuteTemplate(&out, "layout.html", viewData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type("html", "utf-8")
|
||||
return c.Send(out.Bytes())
|
||||
func (ac *AdminController) Fallback(c *fiber.Ctx) error {
|
||||
return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
|
||||
}
|
||||
|
||||
func (ac *AdminController) Favicon(c *fiber.Ctx) error {
|
||||
return c.SendFile(filepath.Join(ac.spaDir, "favicon.ico"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,13 +25,6 @@ func (ac *AuthController) ShowHome(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowWelcome(c *fiber.Ctx) error {
|
||||
return renderPrivate(c, "welcome.html", map[string]any{
|
||||
"Title": "Welcome",
|
||||
"NavSection": "private",
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AuthController) ShowSignup(c *fiber.Ctx) error {
|
||||
return renderPublic(c, "signup.html", map[string]any{
|
||||
"Title": "Sign up",
|
||||
|
|
@ -97,7 +90,7 @@ func (ac *AuthController) Login(c *fiber.Ctx) error {
|
|||
if err := httpmw.SetFlashSuccess(c, "Login effettuato"); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/welcome")
|
||||
return c.Redirect("/private")
|
||||
}
|
||||
|
||||
func (ac *AuthController) Logout(c *fiber.Ctx) error {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type PrivateController struct {
|
||||
spaDir string
|
||||
}
|
||||
|
||||
func NewPrivateController(spaDir string) *PrivateController {
|
||||
return &PrivateController{spaDir: spaDir}
|
||||
}
|
||||
|
||||
func (ac *PrivateController) Dashboard(c *fiber.Ctx) error {
|
||||
return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
|
||||
}
|
||||
|
||||
func (ac *PrivateController) Fallback(c *fiber.Ctx) error {
|
||||
return c.SendFile(filepath.Join(ac.spaDir, "index.html"))
|
||||
}
|
||||
|
||||
func (ac *PrivateController) Favicon(c *fiber.Ctx) error {
|
||||
return c.SendFile(filepath.Join(ac.spaDir, "favicon.ico"))
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"trustcontact/internal/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type UsersController struct {
|
||||
usersService *services.UsersService
|
||||
}
|
||||
|
||||
func NewUsersController(usersService *services.UsersService) *UsersController {
|
||||
return &UsersController{usersService: usersService}
|
||||
}
|
||||
|
||||
func (uc *UsersController) Index(c *fiber.Ctx) error {
|
||||
return renderAdminPage(c, "Admin")
|
||||
}
|
||||
|
||||
func (uc *UsersController) Table(c *fiber.Ctx) error {
|
||||
return renderAdminPage(c, "Admin")
|
||||
}
|
||||
|
||||
func (uc *UsersController) Modal(c *fiber.Ctx) error {
|
||||
return renderAdminPage(c, "Admin")
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package http
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"trustcontact/internal/config"
|
||||
"trustcontact/internal/controllers"
|
||||
httpmw "trustcontact/internal/http/middleware"
|
||||
|
|
@ -28,9 +29,10 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
|||
return fmt.Errorf("init auth service: %w", err)
|
||||
}
|
||||
authController := controllers.NewAuthController(authService)
|
||||
usersService := services.NewUsersService(database)
|
||||
usersController := controllers.NewUsersController(usersService)
|
||||
adminController := controllers.NewAdminController()
|
||||
privateSPADir := filepath.FromSlash("quasar/private_section/dist/spa")
|
||||
privateController := controllers.NewPrivateController(privateSPADir)
|
||||
adminSPADir := filepath.FromSlash("quasar/admin_section/dist/spa")
|
||||
adminController := controllers.NewAdminController(adminSPADir)
|
||||
|
||||
app.Get("/healthz", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
|
|
@ -49,20 +51,32 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
|||
app.Get("/reset-password", authController.ShowResetPassword)
|
||||
app.Post("/reset-password", authController.ResetPassword)
|
||||
app.Get("/forbidden", authController.ShowForbidden)
|
||||
app.Get("/welcome", httpmw.RequireAuth(), authController.ShowWelcome)
|
||||
app.Post("/preferences/lang", httpmw.RequireAuth(), authController.UpdateLanguage)
|
||||
app.Post("/preferences/theme", httpmw.RequireAuth(), authController.UpdateTheme)
|
||||
|
||||
// Quasar admin SPA assets are emitted with absolute paths (/assets, /icons, /favicon.ico).
|
||||
// Protect them with the same auth/admin middleware used by /admin.
|
||||
app.Use("/assets", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
app.Use("/icons", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
app.Get("/favicon.ico", httpmw.RequireAuth(), httpmw.RequireAdmin(), privateController.Favicon)
|
||||
app.Static("/assets", filepath.Join(privateSPADir, "assets"))
|
||||
app.Static("/icons", filepath.Join(privateSPADir, "icons"))
|
||||
|
||||
private := app.Group("/private", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
private.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Redirect("/admin/users")
|
||||
})
|
||||
private.Get("/", privateController.Dashboard)
|
||||
private.Get("/*", privateController.Fallback)
|
||||
|
||||
// Quasar admin SPA assets are emitted with absolute paths (/assets, /icons, /favicon.ico).
|
||||
// Protect them with the same auth/admin middleware used by /admin.
|
||||
app.Use("/assets", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
app.Use("/icons", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
app.Get("/favicon.ico", httpmw.RequireAuth(), httpmw.RequireAdmin(), adminController.Favicon)
|
||||
app.Static("/assets", filepath.Join(adminSPADir, "assets"))
|
||||
app.Static("/icons", filepath.Join(adminSPADir, "icons"))
|
||||
|
||||
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
admin.Get("/", adminController.Dashboard)
|
||||
admin.Get("/users", usersController.Index)
|
||||
admin.Get("/users/table", usersController.Table)
|
||||
admin.Get("/users/:id/modal", usersController.Modal)
|
||||
admin.Get("/*", adminController.Fallback)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
"tailwindcss": "^4.1.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"flowbite": "^3.1.2",
|
||||
"htmx.org": "^2.0.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
|
||||
import { defineConfig } from '#q-app/wrappers';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: resolve(__dirname, '../../.env') });
|
||||
|
||||
export default defineConfig((ctx) => {
|
||||
return {
|
||||
|
|
@ -33,6 +37,9 @@ export default defineConfig((ctx) => {
|
|||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
||||
build: {
|
||||
env: {
|
||||
SITE_URL: process.env.SITE_URL || '',
|
||||
},
|
||||
target: {
|
||||
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||
node: 'node20',
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<p>{{ title }}</p>
|
||||
<ul>
|
||||
<li v-for="todo in todos" :key="todo.id" @click="increment">
|
||||
{{ todo.id }} - {{ todo.content }}
|
||||
</li>
|
||||
</ul>
|
||||
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
|
||||
<p>Active: {{ active ? 'yes' : 'no' }}</p>
|
||||
<p>Clicks on todos: {{ clickCount }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Todo, Meta } from './models';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
todos?: Todo[];
|
||||
meta: Meta;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
todos: () => [],
|
||||
});
|
||||
|
||||
const clickCount = ref(0);
|
||||
function increment() {
|
||||
clickCount.value += 1;
|
||||
return clickCount.value;
|
||||
}
|
||||
|
||||
const todoCount = computed(() => props.todos.length);
|
||||
</script>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export interface Todo {
|
||||
id: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
totalCount: number;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: string;
|
||||
SITE_URL: string | undefined;
|
||||
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
|
||||
VUE_ROUTER_BASE: string | undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,13 @@
|
|||
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
|
||||
<q-list>
|
||||
<q-item-label header> List Items </q-item-label>
|
||||
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
tag="a"
|
||||
:href="privateLink"
|
||||
>
|
||||
<q-item-section>Private</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
|
|
@ -28,6 +33,8 @@
|
|||
import { ref } from 'vue';
|
||||
|
||||
const leftDrawerOpen = ref(false);
|
||||
const siteUrl = (process.env.SITE_URL || '').replace(/\/+$/, '');
|
||||
const privateLink = `${siteUrl}/private`;
|
||||
|
||||
function toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
|
|
|
|||
|
|
@ -15,29 +15,29 @@
|
|||
"postinstall": "quasar prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue-i18n": "^11.0.0",
|
||||
"pinia": "^3.0.1",
|
||||
"@quasar/extras": "^1.16.4",
|
||||
"quasar": "^2.16.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^5.0.0"
|
||||
"@quasar/extras": "^1.17.0",
|
||||
"pinia": "^3.0.4",
|
||||
"quasar": "^2.18.6",
|
||||
"vue": "^3.5.29",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"globals": "^16.4.0",
|
||||
"vue-tsc": "^3.0.7",
|
||||
"@vue/eslint-config-typescript": "^14.4.0",
|
||||
"vite-plugin-checker": "^0.11.0",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"@vue/eslint-config-prettier": "^10.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"@types/node": "^20.5.9",
|
||||
"@eslint/js": "^9.39.3",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@quasar/app-vite": "^2.1.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"typescript": "^5.9.2"
|
||||
"@quasar/app-vite": "^2.4.1",
|
||||
"@types/node": "^20.19.35",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"globals": "^16.5.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-plugin-checker": "^0.11.0",
|
||||
"vue-eslint-parser": "^10.4.0",
|
||||
"vue-tsc": "^3.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^28 || ^26 || ^24 || ^22 || ^20",
|
||||
|
|
@ -9,68 +9,68 @@ importers:
|
|||
.:
|
||||
dependencies:
|
||||
'@quasar/extras':
|
||||
specifier: ^1.16.4
|
||||
specifier: ^1.17.0
|
||||
version: 1.17.0
|
||||
pinia:
|
||||
specifier: ^3.0.1
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3))
|
||||
quasar:
|
||||
specifier: ^2.16.0
|
||||
specifier: ^2.18.6
|
||||
version: 2.18.6
|
||||
vue:
|
||||
specifier: ^3.5.22
|
||||
specifier: ^3.5.29
|
||||
version: 3.5.29(typescript@5.9.3)
|
||||
vue-i18n:
|
||||
specifier: ^11.0.0
|
||||
specifier: ^11.2.8
|
||||
version: 11.2.8(vue@3.5.29(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^5.0.0
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.14.0
|
||||
specifier: ^9.39.3
|
||||
version: 9.39.3
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(rollup@4.59.0)(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))
|
||||
'@quasar/app-vite':
|
||||
specifier: ^2.1.0
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1(@types/node@20.19.35)(eslint@9.39.3)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(quasar@2.18.6)(rollup@4.59.0)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)(vue-router@5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yaml@2.8.2)
|
||||
'@types/node':
|
||||
specifier: ^20.5.9
|
||||
specifier: ^20.19.35
|
||||
version: 20.19.35
|
||||
'@vue/eslint-config-prettier':
|
||||
specifier: ^10.1.0
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0(eslint@9.39.3)(prettier@3.8.1)
|
||||
'@vue/eslint-config-typescript':
|
||||
specifier: ^14.4.0
|
||||
specifier: ^14.7.0
|
||||
version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(vue-eslint-parser@10.4.0(eslint@9.39.3)))(eslint@9.39.3)(typescript@5.9.3)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.2
|
||||
specifier: ^10.4.27
|
||||
version: 10.4.27(postcss@8.5.6)
|
||||
eslint:
|
||||
specifier: ^9.14.0
|
||||
specifier: ^9.39.3
|
||||
version: 9.39.3
|
||||
eslint-plugin-vue:
|
||||
specifier: ^10.4.0
|
||||
specifier: ^10.8.0
|
||||
version: 10.8.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(vue-eslint-parser@10.4.0(eslint@9.39.3))
|
||||
globals:
|
||||
specifier: ^16.4.0
|
||||
specifier: ^16.5.0
|
||||
version: 16.5.0
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vite-plugin-checker:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(eslint@9.39.3)(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@20.19.35)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))
|
||||
vue-eslint-parser:
|
||||
specifier: ^10.2.0
|
||||
specifier: ^10.4.0
|
||||
version: 10.4.0(eslint@9.39.3)
|
||||
vue-tsc:
|
||||
specifier: ^3.0.7
|
||||
specifier: ^3.2.5
|
||||
version: 3.2.5(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 859 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
|
@ -3,6 +3,10 @@
|
|||
|
||||
import { defineConfig } from '#q-app/wrappers';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: resolve(__dirname, '../../.env') });
|
||||
|
||||
export default defineConfig((ctx) => {
|
||||
return {
|
||||
|
|
@ -33,6 +37,9 @@ export default defineConfig((ctx) => {
|
|||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
||||
build: {
|
||||
env: {
|
||||
SITE_URL: process.env.SITE_URL || '',
|
||||
},
|
||||
target: {
|
||||
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||
node: 'node20',
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
|
@ -1,6 +1,7 @@
|
|||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: string;
|
||||
SITE_URL: string | undefined;
|
||||
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
|
||||
VUE_ROUTER_BASE: string | undefined;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
|
||||
|
||||
<q-toolbar-title> Quasar App </q-toolbar-title>
|
||||
|
||||
<div>Quasar v{{ $q.version }}</div>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
|
||||
<q-list>
|
||||
<q-item-label header> items list </q-item-label>
|
||||
<q-item
|
||||
clickable
|
||||
tag="a"
|
||||
:href="privateLink"
|
||||
>
|
||||
<q-item-section>Private</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
tag="a"
|
||||
:href="adminLink"
|
||||
>
|
||||
<q-item-section>Admin</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const leftDrawerOpen = ref(false);
|
||||
const siteUrl = (process.env.SITE_URL || '').replace(/\/+$/, '');
|
||||
const privateLink = `${siteUrl}/private`;
|
||||
const adminLink = `${siteUrl}/admin`;
|
||||
|
||||
function toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<q-page class="row items-center justify-evenly">
|
||||
private page
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "./.quasar/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<q-item clickable tag="a" target="_blank" :href="link">
|
||||
<q-item-section v-if="icon" avatar>
|
||||
<q-icon :name="icon" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ title }}</q-item-label>
|
||||
<q-item-label caption>{{ caption }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface EssentialLinkProps {
|
||||
title: string;
|
||||
caption?: string;
|
||||
link?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<EssentialLinkProps>(), {
|
||||
caption: '',
|
||||
link: '#',
|
||||
icon: '',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<p>{{ title }}</p>
|
||||
<ul>
|
||||
<li v-for="todo in todos" :key="todo.id" @click="increment">
|
||||
{{ todo.id }} - {{ todo.content }}
|
||||
</li>
|
||||
</ul>
|
||||
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
|
||||
<p>Active: {{ active ? 'yes' : 'no' }}</p>
|
||||
<p>Clicks on todos: {{ clickCount }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Todo, Meta } from './models';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
todos?: Todo[];
|
||||
meta: Meta;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
todos: () => [],
|
||||
});
|
||||
|
||||
const clickCount = ref(0);
|
||||
function increment() {
|
||||
clickCount.value += 1;
|
||||
return clickCount.value;
|
||||
}
|
||||
|
||||
const todoCount = computed(() => props.todos.length);
|
||||
</script>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export interface Todo {
|
||||
id: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
totalCount: number;
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
|
||||
|
||||
<q-toolbar-title> Quasar App </q-toolbar-title>
|
||||
|
||||
<div>Quasar v{{ $q.version }}</div>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
|
||||
<q-list>
|
||||
<q-item-label header> Essential Links </q-item-label>
|
||||
|
||||
<EssentialLink v-for="link in linksList" :key="link.title" v-bind="link" />
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import EssentialLink, { type EssentialLinkProps } from 'components/EssentialLink.vue';
|
||||
|
||||
const linksList: EssentialLinkProps[] = [
|
||||
{
|
||||
title: 'Docs',
|
||||
caption: 'quasar.dev',
|
||||
icon: 'school',
|
||||
link: 'https://quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Github',
|
||||
caption: 'github.com/quasarframework',
|
||||
icon: 'code',
|
||||
link: 'https://github.com/quasarframework',
|
||||
},
|
||||
{
|
||||
title: 'Discord Chat Channel',
|
||||
caption: 'chat.quasar.dev',
|
||||
icon: 'chat',
|
||||
link: 'https://chat.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Forum',
|
||||
caption: 'forum.quasar.dev',
|
||||
icon: 'record_voice_over',
|
||||
link: 'https://forum.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
caption: '@quasarframework',
|
||||
icon: 'rss_feed',
|
||||
link: 'https://twitter.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Facebook',
|
||||
caption: '@QuasarFramework',
|
||||
icon: 'public',
|
||||
link: 'https://facebook.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Quasar Awesome',
|
||||
caption: 'Community Quasar projects',
|
||||
icon: 'favorite',
|
||||
link: 'https://awesome.quasar.dev',
|
||||
},
|
||||
];
|
||||
|
||||
const leftDrawerOpen = ref(false);
|
||||
|
||||
function toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<template>
|
||||
<q-page class="row items-center justify-evenly">
|
||||
<example-component
|
||||
title="Example component"
|
||||
active
|
||||
:todos="todos"
|
||||
:meta="meta"
|
||||
></example-component>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { Todo, Meta } from 'components/models';
|
||||
import ExampleComponent from 'components/ExampleComponent.vue';
|
||||
|
||||
const todos = ref<Todo[]>([
|
||||
{
|
||||
id: 1,
|
||||
content: 'ct1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'ct2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'ct3',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
content: 'ct4',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
content: 'ct5',
|
||||
},
|
||||
]);
|
||||
|
||||
const meta = ref<Meta>({
|
||||
totalCount: 1200,
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "./.quasar/tsconfig.json"
|
||||
}
|
||||
|
|
@ -2,9 +2,8 @@ module.exports = {
|
|||
darkMode: "class",
|
||||
content: [
|
||||
"./web/templates/**/*.{html,gohtml}",
|
||||
"./web/static/**/*.js",
|
||||
"./node_modules/flowbite/**/*.js"
|
||||
"./web/static/**/*.js"
|
||||
],
|
||||
theme: { extend: {} },
|
||||
plugins: [require("flowbite/plugin")]
|
||||
plugins: []
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Swiss flag">
|
||||
<rect width="32" height="32" fill="#d52b1e"/>
|
||||
<rect x="13" y="6" width="6" height="20" fill="#ffffff"/>
|
||||
<rect x="6" y="13" width="20" height="6" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 271 B |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="German flag">
|
||||
<rect width="48" height="10.67" x="0" y="0" fill="#000000"/>
|
||||
<rect width="48" height="10.67" x="0" y="10.67" fill="#dd0000"/>
|
||||
<rect width="48" height="10.66" x="0" y="21.34" fill="#ffce00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="English flag">
|
||||
<rect width="48" height="32" fill="#012169"/>
|
||||
<path d="M0 0 48 32M48 0 0 32" stroke="#ffffff" stroke-width="6"/>
|
||||
<path d="M0 0 48 32M48 0 0 32" stroke="#c8102e" stroke-width="3"/>
|
||||
<rect x="20" y="0" width="8" height="32" fill="#ffffff"/>
|
||||
<rect x="0" y="12" width="48" height="8" fill="#ffffff"/>
|
||||
<rect x="22" y="0" width="4" height="32" fill="#c8102e"/>
|
||||
<rect x="0" y="14" width="48" height="4" fill="#c8102e"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 531 B |
|
|
@ -0,0 +1,53 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="United States flag">
|
||||
<rect width="48" height="32" fill="#b22234"/>
|
||||
<g fill="#ffffff">
|
||||
<rect y="2.46" width="48" height="2.46"/>
|
||||
<rect y="7.38" width="48" height="2.46"/>
|
||||
<rect y="12.30" width="48" height="2.46"/>
|
||||
<rect y="17.22" width="48" height="2.46"/>
|
||||
<rect y="22.14" width="48" height="2.46"/>
|
||||
<rect y="27.06" width="48" height="2.46"/>
|
||||
</g>
|
||||
<rect width="20" height="17.23" fill="#3c3b6e"/>
|
||||
<g fill="#ffffff">
|
||||
<circle cx="2.2" cy="2.2" r="0.7"/>
|
||||
<circle cx="5.4" cy="2.2" r="0.7"/>
|
||||
<circle cx="8.6" cy="2.2" r="0.7"/>
|
||||
<circle cx="11.8" cy="2.2" r="0.7"/>
|
||||
<circle cx="15.0" cy="2.2" r="0.7"/>
|
||||
<circle cx="18.2" cy="2.2" r="0.7"/>
|
||||
<circle cx="3.8" cy="4.4" r="0.7"/>
|
||||
<circle cx="7.0" cy="4.4" r="0.7"/>
|
||||
<circle cx="10.2" cy="4.4" r="0.7"/>
|
||||
<circle cx="13.4" cy="4.4" r="0.7"/>
|
||||
<circle cx="16.6" cy="4.4" r="0.7"/>
|
||||
<circle cx="2.2" cy="6.6" r="0.7"/>
|
||||
<circle cx="5.4" cy="6.6" r="0.7"/>
|
||||
<circle cx="8.6" cy="6.6" r="0.7"/>
|
||||
<circle cx="11.8" cy="6.6" r="0.7"/>
|
||||
<circle cx="15.0" cy="6.6" r="0.7"/>
|
||||
<circle cx="18.2" cy="6.6" r="0.7"/>
|
||||
<circle cx="3.8" cy="8.8" r="0.7"/>
|
||||
<circle cx="7.0" cy="8.8" r="0.7"/>
|
||||
<circle cx="10.2" cy="8.8" r="0.7"/>
|
||||
<circle cx="13.4" cy="8.8" r="0.7"/>
|
||||
<circle cx="16.6" cy="8.8" r="0.7"/>
|
||||
<circle cx="2.2" cy="11" r="0.7"/>
|
||||
<circle cx="5.4" cy="11" r="0.7"/>
|
||||
<circle cx="8.6" cy="11" r="0.7"/>
|
||||
<circle cx="11.8" cy="11" r="0.7"/>
|
||||
<circle cx="15.0" cy="11" r="0.7"/>
|
||||
<circle cx="18.2" cy="11" r="0.7"/>
|
||||
<circle cx="3.8" cy="13.2" r="0.7"/>
|
||||
<circle cx="7.0" cy="13.2" r="0.7"/>
|
||||
<circle cx="10.2" cy="13.2" r="0.7"/>
|
||||
<circle cx="13.4" cy="13.2" r="0.7"/>
|
||||
<circle cx="16.6" cy="13.2" r="0.7"/>
|
||||
<circle cx="2.2" cy="15.4" r="0.7"/>
|
||||
<circle cx="5.4" cy="15.4" r="0.7"/>
|
||||
<circle cx="8.6" cy="15.4" r="0.7"/>
|
||||
<circle cx="11.8" cy="15.4" r="0.7"/>
|
||||
<circle cx="15.0" cy="15.4" r="0.7"/>
|
||||
<circle cx="18.2" cy="15.4" r="0.7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="French flag">
|
||||
<rect width="16" height="32" x="0" y="0" fill="#0055a4"/>
|
||||
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
|
||||
<rect width="16" height="32" x="32" y="0" fill="#ef4135"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Italian flag">
|
||||
<rect width="16" height="32" x="0" y="0" fill="#009246"/>
|
||||
<rect width="16" height="32" x="16" y="0" fill="#ffffff"/>
|
||||
<rect width="16" height="32" x="32" y="0" fill="#ce2b37"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 287 B |
|
|
@ -0,0 +1,62 @@
|
|||
(function () {
|
||||
var STORAGE_KEY = 'theme';
|
||||
var isAuthenticated = !!window.__TC_IS_AUTHENTICATED;
|
||||
var serverTheme = (window.__TC_SERVER_THEME || '').toLowerCase();
|
||||
var hasStoredTheme = false;
|
||||
|
||||
function getPreferredTheme() {
|
||||
var stored = localStorage.getItem(STORAGE_KEY);
|
||||
hasStoredTheme = stored === 'dark' || stored === 'light';
|
||||
if (hasStoredTheme) return stored;
|
||||
if (serverTheme === 'dark' || serverTheme === 'light') return serverTheme;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
var isDark = theme === 'dark';
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
var button = document.getElementById('themeToggle');
|
||||
if (button) {
|
||||
button.setAttribute('aria-pressed', isDark ? 'true' : 'false');
|
||||
button.textContent = isDark ? 'Light mode' : 'Dark mode';
|
||||
}
|
||||
}
|
||||
|
||||
function persistTheme(theme) {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
hasStoredTheme = true;
|
||||
}
|
||||
|
||||
function sendThemeToServer(theme) {
|
||||
if (!isAuthenticated) return;
|
||||
fetch('/preferences/theme', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: 'theme=' + encodeURIComponent(theme),
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
window.toggleTheme = function toggleTheme() {
|
||||
var currentIsDark = document.documentElement.classList.contains('dark');
|
||||
var nextTheme = currentIsDark ? 'light' : 'dark';
|
||||
applyTheme(nextTheme);
|
||||
persistTheme(nextTheme);
|
||||
sendThemeToServer(nextTheme);
|
||||
};
|
||||
|
||||
window.initThemeToggle = function initThemeToggle() {
|
||||
applyTheme(document.documentElement.classList.contains('dark') ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
var initialTheme = getPreferredTheme();
|
||||
applyTheme(initialTheme);
|
||||
|
||||
var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (typeof mediaQuery.addEventListener === 'function') {
|
||||
mediaQuery.addEventListener('change', function (event) {
|
||||
if (hasStoredTheme) return;
|
||||
applyTheme(event.matches ? 'dark' : 'light');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<script src="/static/vendor/theme.js?v={{.BuildHash}}"></script>
|
||||
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
|
||||
<script src="/static/vendor/htmx.min.js"></script>
|
||||
<script src="/static/vendor/flowbite.js"></script>
|
||||
|
||||
</head>
|
||||
<body class="flex min-h-screen flex-col bg-white text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100">
|
||||
{{template "navbar" .}}
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
'forgot.title': 'Password dimenticata', 'forgot.subtitle': 'Inserisci la tua email per ricevere il link di reset.', 'forgot.submit': 'Invia link reset', 'forgot.back_login': 'Torna al login',
|
||||
'reset.title': 'Reset password', 'reset.subtitle': 'Imposta una nuova password.', 'reset.new_password': 'Nuova password', 'reset.submit': 'Aggiorna password', 'reset.invalid_token': 'Token mancante o non valido.',
|
||||
'verify.title': 'Verifica email', 'verify.p1': 'Controlla la casella di posta e apri il link di verifica ricevuto.', 'verify.p2': 'Se il link è scaduto, ripeti la registrazione o contatta supporto.', 'verify.go_login': 'Vai al login',
|
||||
'welcome.title': 'Dashboard', 'welcome.back_prefix': 'Bentornato', 'welcome.generic': 'Benvenuto.', 'welcome.quick_links': 'Link rapidi',
|
||||
'private.title': 'Dashboard', 'private.back_prefix': 'Bentornato', 'private.generic': 'Benvenuto.', 'private.quick_links': 'Link rapidi',
|
||||
'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Area amministrazione.', 'admin.users_count': 'Utenti', 'admin.current_role': 'Ruolo corrente', 'admin.navigation': 'Navigazione', 'admin.manage_users': 'Gestione utenti',
|
||||
'users.title': 'Users', 'users.subtitle': 'Ricerca, ordinamento e paging server-side via HTMX.', 'users.new_user': 'Nuovo Utente', 'users.search': 'Search', 'users.search_placeholder': 'Cerca nome o email', 'users.page_size': 'Page size', 'users.search_button': 'Cerca', 'users.user_detail': 'Dettaglio utente', 'users.actions': 'Azioni', 'users.open': 'Apri', 'users.none': 'Nessun utente trovato.', 'users.total': 'Totale', 'users.users_label': 'utenti', 'users.page': 'Pagina', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Chiudi',
|
||||
'users.new_user_modal_title': 'Nuovo utente', 'users.new_user_modal_placeholder': 'Placeholder UI Flowbite. La creazione utente può essere collegata a una route backend quando disponibile.',
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
'forgot.title': 'Forgot Password', 'forgot.subtitle': 'Enter your email to receive a reset link.', 'forgot.submit': 'Send reset link', 'forgot.back_login': 'Back to login',
|
||||
'reset.title': 'Reset Password', 'reset.subtitle': 'Set a new password.', 'reset.new_password': 'New password', 'reset.submit': 'Update password', 'reset.invalid_token': 'Missing or invalid token.',
|
||||
'verify.title': 'Verify email', 'verify.p1': 'Check your inbox and open the verification link.', 'verify.p2': 'If the link expired, sign up again or contact support.', 'verify.go_login': 'Go to login',
|
||||
'welcome.title': 'Dashboard', 'welcome.back_prefix': 'Welcome back', 'welcome.generic': 'Welcome.', 'welcome.quick_links': 'Quick links',
|
||||
'private.title': 'Dashboard', 'private.back_prefix': 'Signed in as', 'private.generic': 'Signed in.', 'private.quick_links': 'Quick links',
|
||||
'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Administration area.', 'admin.users_count': 'Users', 'admin.current_role': 'Current role', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Manage users',
|
||||
'users.title': 'Users', 'users.subtitle': 'Search, sorting and server-side paging via HTMX.', 'users.new_user': 'New user', 'users.search': 'Search', 'users.search_placeholder': 'Search by name or email', 'users.page_size': 'Page size', 'users.search_button': 'Search', 'users.user_detail': 'User details', 'users.actions': 'Actions', 'users.open': 'Open', 'users.none': 'No users found.', 'users.total': 'Total', 'users.users_label': 'users', 'users.page': 'Page', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Close',
|
||||
'users.new_user_modal_title': 'New user', 'users.new_user_modal_placeholder': 'Flowbite placeholder UI. Connect creation to backend route when available.',
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
'forgot.title': 'Passwort vergessen', 'forgot.subtitle': 'Geben Sie Ihre E-Mail ein, um einen Reset-Link zu erhalten.', 'forgot.submit': 'Reset-Link senden', 'forgot.back_login': 'Zurück zum Login',
|
||||
'reset.title': 'Passwort zurücksetzen', 'reset.subtitle': 'Legen Sie ein neues Passwort fest.', 'reset.new_password': 'Neues Passwort', 'reset.submit': 'Passwort aktualisieren', 'reset.invalid_token': 'Token fehlt oder ist ungültig.',
|
||||
'verify.title': 'E-Mail verifizieren', 'verify.p1': 'Öffnen Sie die Verifizierungs-E-Mail in Ihrem Posteingang.', 'verify.p2': 'Wenn der Link abgelaufen ist, registrieren Sie sich erneut oder kontaktieren Sie den Support.', 'verify.go_login': 'Zum Login',
|
||||
'welcome.title': 'Dashboard', 'welcome.back_prefix': 'Willkommen zurück', 'welcome.generic': 'Willkommen.', 'welcome.quick_links': 'Schnelllinks',
|
||||
'private.title': 'Dashboard', 'private.back_prefix': 'Willkommen zurück', 'private.generic': 'Willkommen.', 'private.quick_links': 'Schnelllinks',
|
||||
'admin.dashboard.title': 'Admin-Dashboard', 'admin.dashboard.area': 'Administrationsbereich.', 'admin.users_count': 'Benutzer', 'admin.current_role': 'Aktuelle Rolle', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Benutzer verwalten',
|
||||
'users.title': 'Benutzer', 'users.subtitle': 'Suche, Sortierung und serverseitiges Paging via HTMX.', 'users.new_user': 'Neuer Benutzer', 'users.search': 'Suche', 'users.search_placeholder': 'Nach Name oder E-Mail suchen', 'users.page_size': 'Seitengröße', 'users.search_button': 'Suchen', 'users.user_detail': 'Benutzerdetails', 'users.actions': 'Aktionen', 'users.open': 'Öffnen', 'users.none': 'Keine Benutzer gefunden.', 'users.total': 'Gesamt', 'users.users_label': 'Benutzer', 'users.page': 'Seite', 'users.prev': 'Zurück', 'users.next': 'Weiter', 'users.close': 'Schließen',
|
||||
'users.new_user_modal_title': 'Neuer Benutzer', 'users.new_user_modal_placeholder': 'Flowbite-Placeholder-UI. Bei Bedarf mit Backend-Route verbinden.',
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
'forgot.title': 'Mot de passe oublié', 'forgot.subtitle': 'Entrez votre email pour recevoir un lien de réinitialisation.', 'forgot.submit': 'Envoyer le lien', 'forgot.back_login': 'Retour à la connexion',
|
||||
'reset.title': 'Réinitialiser le mot de passe', 'reset.subtitle': 'Définissez un nouveau mot de passe.', 'reset.new_password': 'Nouveau mot de passe', 'reset.submit': 'Mettre à jour', 'reset.invalid_token': 'Jeton manquant ou invalide.',
|
||||
'verify.title': 'Vérifier l’email', 'verify.p1': 'Vérifiez votre boîte mail et ouvrez le lien de vérification.', 'verify.p2': 'Si le lien a expiré, réinscrivez-vous ou contactez le support.', 'verify.go_login': 'Aller à la connexion',
|
||||
'welcome.title': 'Tableau de bord', 'welcome.back_prefix': 'Bon retour', 'welcome.generic': 'Bienvenue.', 'welcome.quick_links': 'Liens rapides',
|
||||
'private.title': 'Tableau de bord', 'private.back_prefix': 'Bon retour', 'private.generic': 'Bienvenue.', 'private.quick_links': 'Liens rapides',
|
||||
'admin.dashboard.title': 'Tableau de bord admin', 'admin.dashboard.area': 'Zone d’administration.', 'admin.users_count': 'Utilisateurs', 'admin.current_role': 'Rôle actuel', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Gérer les utilisateurs',
|
||||
'users.title': 'Utilisateurs', 'users.subtitle': 'Recherche, tri et pagination côté serveur via HTMX.', 'users.new_user': 'Nouvel utilisateur', 'users.search': 'Recherche', 'users.search_placeholder': 'Rechercher par nom ou email', 'users.page_size': 'Taille de page', 'users.search_button': 'Rechercher', 'users.user_detail': 'Détails utilisateur', 'users.actions': 'Actions', 'users.open': 'Ouvrir', 'users.none': 'Aucun utilisateur trouvé.', 'users.total': 'Total', 'users.users_label': 'utilisateurs', 'users.page': 'Page', 'users.prev': 'Préc.', 'users.next': 'Suiv.', 'users.close': 'Fermer',
|
||||
'users.new_user_modal_title': 'Nouvel utilisateur', 'users.new_user_modal_placeholder': 'UI Flowbite placeholder. Connecter à une route backend si nécessaire.',
|
||||
|
|
@ -207,22 +207,60 @@
|
|||
});
|
||||
}
|
||||
|
||||
function reinitFlowbiteComponents(target) {
|
||||
if (typeof window.initDropdowns === 'function') window.initDropdowns();
|
||||
if (typeof window.initModals === 'function') {
|
||||
if (!target || target.id === 'usersTableContainer') {
|
||||
window.initModals();
|
||||
}
|
||||
function initNavbarComponents(root) {
|
||||
(root || document).querySelectorAll('[data-collapse-toggle]').forEach(function (button) {
|
||||
if (button.dataset.tcBound === '1') return;
|
||||
button.dataset.tcBound = '1';
|
||||
var targetId = button.getAttribute('data-collapse-toggle');
|
||||
var target = document.getElementById(targetId);
|
||||
if (!target) return;
|
||||
button.addEventListener('click', function () {
|
||||
var isHidden = target.classList.contains('hidden');
|
||||
target.classList.toggle('hidden', !isHidden);
|
||||
button.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
|
||||
});
|
||||
});
|
||||
|
||||
var userButton = document.getElementById('user-menu-button');
|
||||
var userDropdown = document.getElementById('user-dropdown');
|
||||
if (userButton && userDropdown && userButton.dataset.tcBound !== '1') {
|
||||
userButton.dataset.tcBound = '1';
|
||||
userButton.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
var isHidden = userDropdown.classList.contains('hidden');
|
||||
userDropdown.classList.toggle('hidden', !isHidden);
|
||||
userButton.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.__tcNavbarDocBound) {
|
||||
window.__tcNavbarDocBound = true;
|
||||
document.addEventListener('click', function (event) {
|
||||
var btn = document.getElementById('user-menu-button');
|
||||
var menu = document.getElementById('user-dropdown');
|
||||
if (!btn || !menu || menu.classList.contains('hidden')) return;
|
||||
if (btn.contains(event.target) || menu.contains(event.target)) return;
|
||||
menu.classList.add('hidden');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key !== 'Escape') return;
|
||||
var btn = document.getElementById('user-menu-button');
|
||||
var menu = document.getElementById('user-dropdown');
|
||||
if (!btn || !menu) return;
|
||||
menu.classList.add('hidden');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
reinitFlowbiteComponents();
|
||||
initNavbarComponents(document);
|
||||
applyTranslations(document);
|
||||
if (typeof window.initThemeToggle === 'function') {
|
||||
window.initThemeToggle();
|
||||
}
|
||||
document.body.addEventListener('htmx:afterSwap', function (evt) {
|
||||
reinitFlowbiteComponents(evt.target || null);
|
||||
initNavbarComponents(evt.target || document);
|
||||
applyTranslations(evt.target || document);
|
||||
if (typeof window.initThemeToggle === 'function') {
|
||||
window.initThemeToggle();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
<ul class="mt-4 flex flex-col gap-2 rounded-lg border border-gray-100 bg-gray-50 p-4 text-sm font-medium md:mt-0 md:flex-row md:items-center md:gap-1 md:border-0 md:bg-transparent md:p-0 dark:border-gray-700 dark:bg-gray-800 md:dark:bg-transparent">
|
||||
{{if eq .NavSection "home"}}
|
||||
{{if .CurrentUser}}
|
||||
<li><a href="/welcome" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">Welcome</a></li>
|
||||
{{else}}
|
||||
<li><a href="/login" class="block rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.login">Login</a></li>
|
||||
{{end}}
|
||||
|
|
@ -53,6 +52,10 @@
|
|||
<span class="block truncate text-sm text-gray-500 dark:text-gray-400">{{.CurrentUser.Email}}</span>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<a href="/private" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">Private</a>
|
||||
{{if eq .CurrentUser.Role "admin"}}
|
||||
<a href="/admin" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.admin">Admin</a>
|
||||
{{end}}
|
||||
<form action="/logout" method="post" class="px-2">
|
||||
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900/40" data-i18n="nav.logout">Logout</button>
|
||||
</form>
|
||||
|
|
|
|||