Compare commits
No commits in common. "main" and "v0.1" have entirely different histories.
|
|
@ -11,7 +11,7 @@ Le bouton de bascule thème clair/sombre ne répond pas au toucher sur Firefox i
|
||||||
- Tester avec un bouton complètement isolé (hors du header, hors des composants dashboard)
|
- Tester avec un bouton complètement isolé (hors du header, hors des composants dashboard)
|
||||||
- Vérifier si le problème persiste dans une page vide sans CSS gradient
|
- Vérifier si le problème persiste dans une page vide sans CSS gradient
|
||||||
|
|
||||||
## Filtrage par en-têtes HTTP (ex: Authelia) — ✅ fait (v0.2)
|
## Filtrage par en-têtes HTTP (ex: Authelia)
|
||||||
|
|
||||||
Masquer/afficher des sections ou des liens en fonction des en-têtes HTTP envoyés par un reverse proxy ou un provider d'authentification (Authelia, Authentik, etc.).
|
Masquer/afficher des sections ou des liens en fonction des en-têtes HTTP envoyés par un reverse proxy ou un provider d'authentification (Authelia, Authentik, etc.).
|
||||||
|
|
||||||
|
|
|
||||||
8
TODO.md
8
TODO.md
|
|
@ -1,8 +0,0 @@
|
||||||
# TODO
|
|
||||||
|
|
||||||
## Sécurité
|
|
||||||
|
|
||||||
- Retirer le token Forgejo de l'URL du remote git dans `.git/config`
|
|
||||||
(actuellement en clair : `https://maxsoch:<token>@git.socheleau.fr/...`)
|
|
||||||
→ utiliser un credential helper ou SSH à la place
|
|
||||||
- Vérifier que le token n'apparaît pas ailleurs dans le repo
|
|
||||||
|
|
@ -55,18 +55,16 @@ Configuration optionnelle pour l'authentification via un reverse proxy (Authelia
|
||||||
| Champ | Requis | Type | Défaut | Description |
|
| Champ | Requis | Type | Défaut | Description |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `logout_url` | Oui | chaîne (URL) | — | URL de déconnexion (ex: `"https://auth.example.com/logout"`) |
|
| `logout_url` | Oui | chaîne (URL) | — | URL de déconnexion (ex: `"https://auth.example.com/logout"`) |
|
||||||
| `login_url` | Non | chaîne (URL) | — | URL de connexion. Un bouton login remplace le bouton logout quand l'utilisateur n'est pas connecté (ex: `"https://auth.example.com"`) |
|
|
||||||
| `header_format` | Non | chaîne | `"Remote-User"` | Nom de l'en-tête HTTP indiquant que l'utilisateur est connecté |
|
| `header_format` | Non | chaîne | `"Remote-User"` | Nom de l'en-tête HTTP indiquant que l'utilisateur est connecté |
|
||||||
| `username` | Non | chaîne | — | Nom de l'en-tête HTTP contenant le nom d'utilisateur (ex: `"Remote-Name"`). Affiche "Hi {valeur}" dans le header. |
|
| `username` | Non | chaîne | — | Nom de l'en-tête HTTP contenant le nom d'utilisateur (ex: `"Remote-Name"`). Affiche "Hi {valeur}" dans le header. |
|
||||||
| `header_user` | Non | chaîne | — | Nom de l'en-tête HTTP pour filtrer par utilisateur (ex: `"Remote-User"`) |
|
| `header_user` | Non | chaîne | — | Nom de l'en-tête HTTP pour filtrer par utilisateur (ex: `"Remote-User"`) |
|
||||||
| `header_group` | Non | chaîne | — | Nom de l'en-tête HTTP pour filtrer par groupe (ex: `"Remote-Groups"`) |
|
| `header_group` | Non | chaîne | — | Nom de l'en-tête HTTP pour filtrer par groupe (ex: `"Remote-Groups"`) |
|
||||||
|
|
||||||
Quand l'en-tête défini par `header_format` est présent **avec une valeur valide** (non vide, sans placeholder non résolu), un bouton de déconnexion apparaît à droite du bouton thème. Sinon, si `login_url` est défini, un bouton de connexion est affiché à la place.
|
Quand l'en-tête défini par `header_format` est présent dans la requête, un bouton de déconnexion apparaît à droite du bouton thème.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
authentication:
|
authentication:
|
||||||
logout_url: "https://auth.example.com/logout"
|
logout_url: "https://auth.example.com/logout"
|
||||||
login_url: "https://auth.example.com"
|
|
||||||
header_format: "Remote-User"
|
header_format: "Remote-User"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,84 @@
|
||||||
site:
|
site:
|
||||||
name: "Soch Family"
|
name: "Soch Family"
|
||||||
title: " "
|
title: " "
|
||||||
|
#title: "Les applications de la famille"
|
||||||
logoUrl: "https://immich.socheleau.fr/api/assets/e010423f-ca95-45a4-a9ee-9bb1a5a48688/thumbnail?slug=us_6&size=preview&c=IhgKJYiHg4ZfhXh6h5aJp5eAdQlJ&edited=true"
|
logoUrl: "https://immich.socheleau.fr/api/assets/e010423f-ca95-45a4-a9ee-9bb1a5a48688/thumbnail?slug=us_6&size=preview&c=IhgKJYiHg4ZfhXh6h5aJp5eAdQlJ&edited=true"
|
||||||
|
#description: "Accès rapide à nos services"
|
||||||
showFooter: false
|
showFooter: false
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
mode: "dark"
|
mode: "dark"
|
||||||
accent: "#8b5cf6"
|
accent: "#8b5cf6"
|
||||||
background: "gradient"
|
background: "gradient"
|
||||||
icon_size: "large"
|
|
||||||
|
|
||||||
authentication:
|
authentication:
|
||||||
logout_url: "https://auth.socheleau.fr/logout"
|
logout_url: "https://auth.socheleau.fr/logout"
|
||||||
login_url: "https://auth.socheleau.fr"
|
|
||||||
header_format: "Remote-User"
|
header_format: "Remote-User"
|
||||||
username: "Remote-Name"
|
username: "Remote-Name"
|
||||||
header_user: "Remote-User"
|
header_user: "Remote-User"
|
||||||
header_group: "Remote-Groups"
|
header_group: "Remote-Groups"
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
- id: "soch-services"
|
- id: "global"
|
||||||
title: "Soch's services"
|
title: "Global"
|
||||||
|
#description: "Services accessibles à toute la famille"
|
||||||
cardLayout: "horizontal"
|
cardLayout: "horizontal"
|
||||||
|
visible_by_group: "arr"
|
||||||
links:
|
links:
|
||||||
- id: "authelia"
|
- id: "nextcloud"
|
||||||
name: "Authelia"
|
name: "Nextcloud"
|
||||||
description: "Authentification unique"
|
description: "Fichiers et documents"
|
||||||
url: "https://auth.socheleau.fr"
|
url: "https://cloud.example.com"
|
||||||
icon: "Shield"
|
icon: "Cloud"
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg"
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg"
|
||||||
color: "#1a7f37"
|
color: "#2daae1"
|
||||||
openInNewTab: false
|
openInNewTab: false
|
||||||
|
visible_by_user: "jean"
|
||||||
|
|
||||||
- id: "plex"
|
- id: "jellyfin"
|
||||||
name: "Plex"
|
name: "Jellyfin"
|
||||||
description: "Films et séries"
|
description: "Films et séries"
|
||||||
url: "https://plex.socheleau.fr"
|
url: "https://jellyfin.example.com"
|
||||||
icon: "Film"
|
icon: "Film"
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg"
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg"
|
||||||
color: "#e5a00d"
|
color: "#aa5cc3"
|
||||||
openInNewTab: false
|
openInNewTab: false
|
||||||
|
|
||||||
- id: "bitwarden"
|
- id: "navidrome"
|
||||||
name: "Bitwarden"
|
name: "Navidrome"
|
||||||
description: "Mots de passe"
|
description: "Musique"
|
||||||
url: "https://bitwarden.socheleau.fr"
|
url: "https://music.example.com"
|
||||||
icon: "Shield"
|
icon: "Music"
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/bitwarden.svg"
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/navidrome.svg"
|
||||||
color: "#175ddc"
|
color: "#22c55e"
|
||||||
openInNewTab: false
|
openInNewTab: false
|
||||||
|
|
||||||
- id: "immich"
|
- id: "minecraft"
|
||||||
name: "Immich"
|
name: "Minecraft"
|
||||||
description: "Photos et vidéos"
|
description: "Serveur de jeu"
|
||||||
url: "https://immich.socheleau.fr"
|
url: "https://minecraft.example.com"
|
||||||
icon: "Image"
|
icon: "Gamepad2"
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg"
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/minecraft.svg"
|
||||||
color: "#4250af"
|
color: "#f97316"
|
||||||
openInNewTab: false
|
openInNewTab: false
|
||||||
|
|
||||||
- id: "freshrss"
|
- id: "custom"
|
||||||
name: "FreshRSS"
|
title: "Liens personnalisés"
|
||||||
description: "Flux RSS"
|
|
||||||
url: "https://rss.socheleau.fr"
|
|
||||||
icon: "Rss"
|
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/freshrss.svg"
|
|
||||||
color: "#0979b0"
|
|
||||||
openInNewTab: false
|
|
||||||
|
|
||||||
- id: "tandoor"
|
|
||||||
name: "Tandoor"
|
|
||||||
description: "Recettes de cuisine"
|
|
||||||
url: "https://tandoor.socheleau.fr"
|
|
||||||
icon: "ChefHat"
|
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tandoor-recipes.svg"
|
|
||||||
color: "#dc7b2a"
|
|
||||||
openInNewTab: false
|
|
||||||
|
|
||||||
- id: "readeck"
|
|
||||||
name: "Readeck"
|
|
||||||
description: "Lecture différée"
|
|
||||||
url: "https://readeck.socheleau.fr"
|
|
||||||
icon: "BookOpen"
|
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/readeck.svg"
|
|
||||||
color: "#14b8a6"
|
|
||||||
openInNewTab: false
|
|
||||||
|
|
||||||
- id: "homes"
|
|
||||||
title: "Homes"
|
|
||||||
cardLayout: "horizontal"
|
cardLayout: "horizontal"
|
||||||
links:
|
links:
|
||||||
- id: "arasu"
|
- id: "homeassistant"
|
||||||
name: "Arasu"
|
name: "Home Assistant"
|
||||||
description: "Domotique"
|
description: "Domotique"
|
||||||
url: "https://arasu.socheleau.fr"
|
url: "https://ha.example.com"
|
||||||
icon: "House"
|
icon: "House"
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg"
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg"
|
||||||
color: "#22c55e"
|
color: "#22c55e"
|
||||||
openInNewTab: false
|
openInNewTab: false
|
||||||
|
|
||||||
- id: "arr"
|
- id: "router"
|
||||||
title: "*arr"
|
name: "Routeur"
|
||||||
cardLayout: "horizontal"
|
description: "Administration réseau"
|
||||||
visible_by_group: "arr"
|
url: "http://192.168.1.1"
|
||||||
links:
|
icon: "Router"
|
||||||
- id: "radarr"
|
color: "#f59e0b"
|
||||||
name: "Radarr"
|
|
||||||
description: "Gestion de films"
|
|
||||||
url: "https://radarr.arr.socheleau.fr"
|
|
||||||
icon: "Clapperboard"
|
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg"
|
|
||||||
color: "#e73a2e"
|
|
||||||
openInNewTab: false
|
|
||||||
|
|
||||||
- id: "sonarr"
|
|
||||||
name: "Sonarr"
|
|
||||||
description: "Gestion de séries"
|
|
||||||
url: "https://sonarr.arr.socheleau.fr"
|
|
||||||
icon: "Tv"
|
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg"
|
|
||||||
color: "#35c5f4"
|
|
||||||
openInNewTab: false
|
|
||||||
|
|
||||||
- id: "prowlarr"
|
|
||||||
name: "Prowlarr"
|
|
||||||
description: "Indexeur de torrents"
|
|
||||||
url: "https://prowlarr.arr.socheleau.fr"
|
|
||||||
icon: "Search"
|
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/prowlarr.svg"
|
|
||||||
color: "#e65c21"
|
|
||||||
openInNewTab: false
|
openInNewTab: false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
sochboard:
|
sochboard:
|
||||||
image: registry2.socheleau.fr/maxence/sochboard:latest
|
build: .
|
||||||
container_name: sochboard
|
container_name: sochboard
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sochboard",
|
"name": "sochboard",
|
||||||
"version": "0.3.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node node_modules/next/dist/bin/next dev",
|
"dev": "node node_modules/next/dist/bin/next dev",
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,12 @@ html.theme-light {
|
||||||
--focus-ring-offset: #f8fafc;
|
--focus-ring-offset: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.dashboard-root {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-gradient);
|
background: var(--bg-gradient);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-root {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { Inter } from "next/font/google"
|
import { Inter } from "next/font/google"
|
||||||
import { getConfig } from "@/lib/config"
|
import { getConfig } from "@/lib/config"
|
||||||
import { SochboardLogoDataUri } from "@/components/ui/SochboardLogo"
|
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
|
|
@ -16,17 +15,12 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||||
return {
|
return {
|
||||||
title: config.site.name,
|
title: config.site.name,
|
||||||
description: config.site.title,
|
description: config.site.title,
|
||||||
icons: {
|
icons: config.site.favicon ? { icon: config.site.favicon } : undefined,
|
||||||
icon: config.site.favicon ?? SochboardLogoDataUri(config.theme.accent),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
title: "Sochboard",
|
title: "Sochboard",
|
||||||
description: "Dashboard",
|
description: "Dashboard",
|
||||||
icons: {
|
|
||||||
icon: SochboardLogoDataUri(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,6 @@ import { ErrorDisplay } from "@/components/ui/ErrorDisplay"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
function isValidHeaderValue(value: string | null | undefined): value is string {
|
|
||||||
if (!value) return false
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (trimmed.length === 0) return false
|
|
||||||
if (trimmed.includes("{") || trimmed.includes("}")) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConfigResult() {
|
function getConfigResult() {
|
||||||
try {
|
try {
|
||||||
return { ok: true as const, config: getConfig() }
|
return { ok: true as const, config: getConfig() }
|
||||||
|
|
@ -38,17 +30,12 @@ export default async function Home() {
|
||||||
const authHeader = config.authentication?.header_format ?? "Remote-User"
|
const authHeader = config.authentication?.header_format ?? "Remote-User"
|
||||||
const usernameHeader = config.authentication?.username
|
const usernameHeader = config.authentication?.username
|
||||||
const headersList = await headers()
|
const headersList = await headers()
|
||||||
const authValue = config.authentication ? headersList.get(authHeader) : null
|
const isAuthenticated = config.authentication != null && headersList.has(authHeader)
|
||||||
const isAuthenticated = isValidHeaderValue(authValue)
|
const username = isAuthenticated && usernameHeader ? headersList.get(usernameHeader) ?? null : null
|
||||||
const username = isAuthenticated && usernameHeader && isValidHeaderValue(headersList.get(usernameHeader))
|
|
||||||
? headersList.get(usernameHeader)!.trim()
|
|
||||||
: null
|
|
||||||
|
|
||||||
const userHeader = config.authentication?.header_user
|
const userHeader = config.authentication?.header_user
|
||||||
const groupHeader = config.authentication?.header_group
|
const groupHeader = config.authentication?.header_group
|
||||||
const currentUser = isAuthenticated && userHeader && isValidHeaderValue(headersList.get(userHeader))
|
const currentUser = isAuthenticated && userHeader ? headersList.get(userHeader) ?? null : null
|
||||||
? headersList.get(userHeader)!.trim()
|
|
||||||
: null
|
|
||||||
const userGroups = isAuthenticated && groupHeader
|
const userGroups = isAuthenticated && groupHeader
|
||||||
? (headersList.get(groupHeader) ?? "").split(",").map((g) => g.trim()).filter(Boolean)
|
? (headersList.get(groupHeader) ?? "").split(",").map((g) => g.trim()).filter(Boolean)
|
||||||
: []
|
: []
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { DashboardHeader } from "./DashboardHeader"
|
import { DashboardHeader } from "./DashboardHeader"
|
||||||
import { DashboardSection } from "./DashboardSection"
|
import { DashboardSection } from "./DashboardSection"
|
||||||
import { VERSION } from "@/lib/version"
|
|
||||||
import type { DashboardConfig } from "@/types/config"
|
import type { DashboardConfig } from "@/types/config"
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
|
|
@ -20,12 +19,10 @@ export function Dashboard({ config, isAuthenticated = false, username = null, cu
|
||||||
authentication={config.authentication}
|
authentication={config.authentication}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
username={username}
|
username={username}
|
||||||
accent={config.theme.accent}
|
|
||||||
version={VERSION}
|
|
||||||
/>
|
/>
|
||||||
<main className="flex flex-col gap-12">
|
<main className="flex flex-col gap-12">
|
||||||
{config.sections.map((section, index) => (
|
{config.sections.map((section, index) => (
|
||||||
<DashboardSection key={section.id} section={section} sectionIndex={index} currentUser={currentUser} userGroups={userGroups} iconSize={config.theme.icon_size} />
|
<DashboardSection key={section.id} section={section} sectionIndex={index} currentUser={currentUser} userGroups={userGroups} />
|
||||||
))}
|
))}
|
||||||
</main>
|
</main>
|
||||||
{config.site.showFooter !== false && (
|
{config.site.showFooter !== false && (
|
||||||
|
|
@ -35,4 +32,4 @@ export function Dashboard({ config, isAuthenticated = false, username = null, cu
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import type { SiteConfig, AuthenticationConfig } from "@/types/config"
|
import type { SiteConfig, AuthenticationConfig } from "@/types/config"
|
||||||
import { SochboardLogo } from "@/components/ui/SochboardLogo"
|
|
||||||
|
|
||||||
interface DashboardHeaderProps {
|
interface DashboardHeaderProps {
|
||||||
site: SiteConfig
|
site: SiteConfig
|
||||||
|
|
@ -10,11 +9,9 @@ interface DashboardHeaderProps {
|
||||||
authentication?: AuthenticationConfig
|
authentication?: AuthenticationConfig
|
||||||
isAuthenticated?: boolean
|
isAuthenticated?: boolean
|
||||||
username?: string | null
|
username?: string | null
|
||||||
accent?: string
|
|
||||||
version?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardHeader({ site, initialTheme, authentication, isAuthenticated = false, username = null, accent, version }: DashboardHeaderProps) {
|
export function DashboardHeader({ site, initialTheme, authentication, isAuthenticated = false, username = null }: DashboardHeaderProps) {
|
||||||
const [isLight, setIsLight] = useState(initialTheme === "light")
|
const [isLight, setIsLight] = useState(initialTheme === "light")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -36,7 +33,12 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
className="size-12 rounded-xl object-cover"
|
className="size-12 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SochboardLogo size={48} accent={accent} />
|
<div
|
||||||
|
className="flex size-12 items-center justify-center rounded-xl bg-gradient-to-br from-[#8b5cf6] to-[#6d28d9] text-lg font-bold text-white shadow-lg shadow-[#8b5cf6]/20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
S
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-[var(--text-primary)]">
|
<h1 className="text-xl font-bold text-[var(--text-primary)]">
|
||||||
|
|
@ -45,9 +47,6 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
<p className="mt-0.5 text-sm text-[var(--text-secondary)]">
|
<p className="mt-0.5 text-sm text-[var(--text-secondary)]">
|
||||||
{site.title}
|
{site.title}
|
||||||
</p>
|
</p>
|
||||||
{version && (
|
|
||||||
<p className="text-xs text-[var(--text-secondary)]">v{version}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -107,7 +106,7 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{isAuthenticated && authentication ? (
|
{isAuthenticated && authentication && (
|
||||||
<a
|
<a
|
||||||
href={authentication.logout_url}
|
href={authentication.logout_url}
|
||||||
className="flex size-10 items-center justify-center rounded-lg border transition-colors hover:opacity-80 active:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)] sm:size-9"
|
className="flex size-10 items-center justify-center rounded-lg border transition-colors hover:opacity-80 active:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)] sm:size-9"
|
||||||
|
|
@ -135,37 +134,6 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
<line x1="21" x2="9" y1="12" y2="12" />
|
<line x1="21" x2="9" y1="12" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
|
||||||
!isAuthenticated &&
|
|
||||||
authentication?.login_url && (
|
|
||||||
<a
|
|
||||||
href={authentication.login_url}
|
|
||||||
className="flex size-10 items-center justify-center rounded-lg border transition-colors hover:opacity-80 active:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)] sm:size-9"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "rgba(128,128,128,0.15)",
|
|
||||||
borderColor: "rgba(128,128,128,0.25)",
|
|
||||||
color: "var(--text-tertiary)",
|
|
||||||
}}
|
|
||||||
aria-label="Connexion"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
|
|
||||||
<polyline points="10 17 15 12 10 7" />
|
|
||||||
<line x1="15" x2="3" y1="12" y2="12" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { ServiceCard } from "./ServiceCard"
|
import { ServiceCard } from "./ServiceCard"
|
||||||
import { isVisible } from "@/lib/visibility"
|
import { isVisible } from "@/lib/visibility"
|
||||||
import type { SectionConfig, IconSize } from "@/types/config"
|
import type { SectionConfig } from "@/types/config"
|
||||||
|
|
||||||
interface DashboardSectionProps {
|
interface DashboardSectionProps {
|
||||||
section: SectionConfig
|
section: SectionConfig
|
||||||
sectionIndex?: number
|
sectionIndex?: number
|
||||||
currentUser?: string | null
|
currentUser?: string | null
|
||||||
userGroups?: string[]
|
userGroups?: string[]
|
||||||
iconSize?: IconSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardSection({ section, sectionIndex = 0, currentUser = null, userGroups = [], iconSize = "medium" }: DashboardSectionProps) {
|
export function DashboardSection({ section, sectionIndex = 0, currentUser = null, userGroups = [] }: DashboardSectionProps) {
|
||||||
if (!isVisible(section, currentUser, userGroups)) return null
|
if (!isVisible(section, currentUser, userGroups)) return null
|
||||||
|
|
||||||
const visibleLinks = section.links.filter((link) => isVisible(link, currentUser, userGroups))
|
const visibleLinks = section.links.filter((link) => isVisible(link, currentUser, userGroups))
|
||||||
|
|
@ -33,14 +32,13 @@ export function DashboardSection({ section, sectionIndex = 0, currentUser = null
|
||||||
<p className="mt-1 text-sm text-[var(--text-secondary)]">{section.description}</p>
|
<p className="mt-1 text-sm text-[var(--text-secondary)]">{section.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 sm:gap-4">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 sm:gap-4">
|
||||||
{visibleLinks.map((link, index) => (
|
{visibleLinks.map((link, index) => (
|
||||||
<ServiceCard
|
<ServiceCard
|
||||||
key={link.id}
|
key={link.id}
|
||||||
link={link}
|
link={link}
|
||||||
index={index}
|
index={index}
|
||||||
layout={section.cardLayout}
|
layout={section.cardLayout}
|
||||||
iconSize={iconSize}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
import { IconRenderer } from "./IconRenderer"
|
import { IconRenderer } from "./IconRenderer"
|
||||||
import type { LinkConfig, IconSize } from "@/types/config"
|
import type { LinkConfig } from "@/types/config"
|
||||||
|
|
||||||
interface ServiceCardProps {
|
interface ServiceCardProps {
|
||||||
link: LinkConfig
|
link: LinkConfig
|
||||||
index?: number
|
index?: number
|
||||||
layout?: "horizontal" | "vertical"
|
layout?: "horizontal" | "vertical"
|
||||||
iconSize?: IconSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeMap: Record<IconSize, { hCont: number; hImg: string; hIcon: number; vCont: number; vImg: string; vIcon: number }> = {
|
export function ServiceCard({ link, index = 0, layout = "horizontal" }: ServiceCardProps) {
|
||||||
small: { hCont: 10, hImg: "size-5", hIcon: 18, vCont: 12, vImg: "size-6", vIcon: 22 },
|
|
||||||
medium: { hCont: 12, hImg: "size-7", hIcon: 24, vCont: 14, vImg: "size-8", vIcon: 30 },
|
|
||||||
large: { hCont: 12, hImg: "size-8", hIcon: 28, vCont: 14, vImg: "size-9", vIcon: 34 },
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ServiceCard({ link, index = 0, layout = "horizontal", iconSize = "medium" }: ServiceCardProps) {
|
|
||||||
const linkProps = link.openInNewTab
|
const linkProps = link.openInNewTab
|
||||||
? { target: "_blank", rel: "noopener noreferrer" }
|
? { target: "_blank", rel: "noopener noreferrer" }
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const isHorizontal = layout === "horizontal"
|
const isHorizontal = layout === "horizontal"
|
||||||
const s = sizeMap[iconSize]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
|
@ -32,18 +24,16 @@ export function ServiceCard({ link, index = 0, layout = "horizontal", iconSize =
|
||||||
{isHorizontal ? (
|
{isHorizontal ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="flex shrink-0 items-center justify-center overflow-hidden rounded-lg"
|
className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg"
|
||||||
style={{
|
style={{
|
||||||
width: s.hCont * 4,
|
|
||||||
height: s.hCont * 4,
|
|
||||||
backgroundColor: `${link.color || "#8b5cf6"}20`,
|
backgroundColor: `${link.color || "#8b5cf6"}20`,
|
||||||
color: link.color || "#8b5cf6",
|
color: link.color || "#8b5cf6",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{link.iconUrl ? (
|
{link.iconUrl ? (
|
||||||
<img src={link.iconUrl} alt="" className={`${s.hImg} object-contain`} />
|
<img src={link.iconUrl} alt="" className="size-5 object-contain" />
|
||||||
) : (
|
) : (
|
||||||
<IconRenderer name={link.icon} size={s.hIcon} />
|
<IconRenderer name={link.icon} size={18} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
|
|
@ -83,18 +73,16 @@ export function ServiceCard({ link, index = 0, layout = "horizontal", iconSize =
|
||||||
<>
|
<>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center overflow-hidden rounded-xl"
|
className="flex size-12 items-center justify-center overflow-hidden rounded-xl"
|
||||||
style={{
|
style={{
|
||||||
width: s.vCont * 4,
|
|
||||||
height: s.vCont * 4,
|
|
||||||
backgroundColor: `${link.color || "#8b5cf6"}20`,
|
backgroundColor: `${link.color || "#8b5cf6"}20`,
|
||||||
color: link.color || "#8b5cf6",
|
color: link.color || "#8b5cf6",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{link.iconUrl ? (
|
{link.iconUrl ? (
|
||||||
<img src={link.iconUrl} alt="" className={`${s.vImg} object-contain`} />
|
<img src={link.iconUrl} alt="" className="size-6 object-contain" />
|
||||||
) : (
|
) : (
|
||||||
<IconRenderer name={link.icon} size={s.vIcon} />
|
<IconRenderer name={link.icon} size={22} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{link.openInNewTab && (
|
{link.openInNewTab && (
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
interface SochboardLogoProps {
|
|
||||||
size?: number
|
|
||||||
accent?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SochboardLogo({ size = 48, accent = "#f59e0b", className }: SochboardLogoProps) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={className}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<rect width="48" height="48" rx="12" fill={accent} />
|
|
||||||
<rect x="4" y="4" width="40" height="40" rx="10" fill={`${accent}20`} />
|
|
||||||
<path
|
|
||||||
d="M24 12C21.5 12 19 13.5 19 16.5C19 18 20 19 21 19.5C19.5 20 18 21.5 18 24C18 26.5 20 28 22 28C20.5 28.5 19 30 19 32.5C19 35.5 21.5 37 24 37C26.5 37 29 35.5 29 32.5C29 31 28 30 27 29.5C28.5 29 30 27.5 30 25C30 22.5 28 21 26 21C27.5 20.5 29 19 29 16.5C29 13.5 26.5 12 24 12Z"
|
|
||||||
fill="white"
|
|
||||||
fillOpacity="0.95"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12 12H36"
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeOpacity="0.2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12 36H36"
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeOpacity="0.2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12 24H36"
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeOpacity="0.2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M24 12V36"
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeOpacity="0.2"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SochboardLogoDataUri(accent: string = "#f59e0b"): string {
|
|
||||||
const hex = accent.replace("#", "%23")
|
|
||||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
||||||
<rect width="48" height="48" rx="12" fill="${hex}"/>
|
|
||||||
<rect x="4" y="4" width="40" height="40" rx="10" fill="${hex}20"/>
|
|
||||||
<path d="M24 12C21.5 12 19 13.5 19 16.5C19 18 20 19 21 19.5C19.5 20 18 21.5 18 24C18 26.5 20 28 22 28C20.5 28.5 19 30 19 32.5C19 35.5 21.5 37 24 37C26.5 37 29 35.5 29 32.5C29 31 28 30 27 29.5C28.5 29 30 27.5 30 25C30 22.5 28 21 26 21C27.5 20.5 29 19 29 16.5C29 13.5 26.5 12 24 12Z" fill="white" fill-opacity="0.95"/>
|
|
||||||
<path d="M12 12H36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
|
|
||||||
<path d="M12 36H36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
|
|
||||||
<path d="M12 24H36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
|
|
||||||
<path d="M24 12V36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
|
|
||||||
</svg>`
|
|
||||||
return `data:image/svg+xml,${encodeURIComponent(svg)}`
|
|
||||||
}
|
|
||||||
|
|
@ -32,7 +32,6 @@ export const themeSchema = z.object({
|
||||||
.regex(hexColorRegex, "Invalid accent color")
|
.regex(hexColorRegex, "Invalid accent color")
|
||||||
.default("#8b5cf6"),
|
.default("#8b5cf6"),
|
||||||
background: z.enum(["gradient", "solid"]).default("gradient"),
|
background: z.enum(["gradient", "solid"]).default("gradient"),
|
||||||
icon_size: z.enum(["small", "medium", "large"]).optional().default("medium"),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const siteSchema = z.object({
|
export const siteSchema = z.object({
|
||||||
|
|
@ -46,7 +45,6 @@ export const siteSchema = z.object({
|
||||||
|
|
||||||
export const authenticationSchema = z.object({
|
export const authenticationSchema = z.object({
|
||||||
logout_url: z.string().url("Invalid logout URL").min(1, "Logout URL is required"),
|
logout_url: z.string().url("Invalid logout URL").min(1, "Logout URL is required"),
|
||||||
login_url: z.string().url("Invalid login URL").optional(),
|
|
||||||
header_format: z.string().optional().default("Remote-User"),
|
header_format: z.string().optional().default("Remote-User"),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
header_user: z.string().optional(),
|
header_user: z.string().optional(),
|
||||||
|
|
@ -59,7 +57,6 @@ export const dashboardConfigSchema = z.object({
|
||||||
mode: "dark",
|
mode: "dark",
|
||||||
accent: "#8b5cf6",
|
accent: "#8b5cf6",
|
||||||
background: "gradient",
|
background: "gradient",
|
||||||
icon_size: "medium",
|
|
||||||
}),
|
}),
|
||||||
authentication: authenticationSchema.optional(),
|
authentication: authenticationSchema.optional(),
|
||||||
sections: z.array(sectionSchema).default([]),
|
sections: z.array(sectionSchema).default([]),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import pkg from "../../package.json"
|
|
||||||
|
|
||||||
export const VERSION = pkg.version
|
|
||||||
|
|
@ -7,13 +7,10 @@ export interface SiteConfig {
|
||||||
showFooter?: boolean
|
showFooter?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IconSize = "small" | "medium" | "large"
|
|
||||||
|
|
||||||
export interface ThemeConfig {
|
export interface ThemeConfig {
|
||||||
mode: "dark" | "light"
|
mode: "dark" | "light"
|
||||||
accent: string
|
accent: string
|
||||||
background: "gradient" | "solid"
|
background: "gradient" | "solid"
|
||||||
icon_size?: IconSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LinkConfig {
|
export interface LinkConfig {
|
||||||
|
|
@ -41,7 +38,6 @@ export interface SectionConfig {
|
||||||
|
|
||||||
export interface AuthenticationConfig {
|
export interface AuthenticationConfig {
|
||||||
logout_url: string
|
logout_url: string
|
||||||
login_url?: string
|
|
||||||
header_format?: string
|
header_format?: string
|
||||||
username?: string
|
username?: string
|
||||||
header_user?: string
|
header_user?: string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue