diff --git a/app/admin/page.tsx b/app/admin/page.tsx index edcc176..f4da364 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -4,14 +4,14 @@ import { useState, useEffect, useRef } from 'react'; import { Card, Portal, MediaItem, CardType } from '@/types'; import { EXTERNAL_LINK_ENABLED as EXTERNAL_LINK_DEFAULT } from '@/lib/config'; -function CardTypeSelect({ +function StyledSelect({ value, onChange, options, }: { - value: CardType; - onChange: (v: CardType) => void; - options: { value: CardType; label: string }[]; + value: T; + onChange: (v: T) => void; + options: { value: T; label: string }[]; }) { const [open, setOpen] = useState(false); const ref = useRef(null); @@ -181,6 +181,7 @@ export default function AdminDashboard() { const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const [confirmDialog, setConfirmDialog] = useState<{ message: string, onConfirm: () => void } | null>(null); const [pdfProgress, setPdfProgress] = useState<{ name: string; page: number; total: number } | null>(null); + const [availableFonts, setAvailableFonts] = useState([]); // External Link feature flag: priorità al setting del portale, fallback alla costante in lib/config. const externalLinksOn = portal.externalLinkEnabled ?? EXTERNAL_LINK_DEFAULT; @@ -194,6 +195,7 @@ export default function AdminDashboard() { useEffect(() => { fetch('/api/cards').then(res => res.json()).then(setCards); fetch('/api/portals').then(res => res.json()).then(data => data && setPortal(data)); + fetch('/api/fonts').then(res => res.json()).then(setAvailableFonts).catch(() => setAvailableFonts([])); }, []); const handleUpload = async (e: React.ChangeEvent, field: string, isPortal = false) => { @@ -519,6 +521,21 @@ export default function AdminDashboard() { + +
+ + + value={portal.fontFamily ?? ''} + onChange={(v) => setPortal({ ...portal, fontFamily: v })} + options={[ + { value: '', label: 'Sistema (Arial)' }, + ...availableFonts.map(f => ({ value: f, label: f.replace(/\.(woff2?|ttf|otf)$/i, '') })), + ]} + /> +

+ Per aggiungere altri font, copia file .woff2, .woff, .ttf o .otf in data/fonts/ e ricarica questa pagina. +

+
@@ -614,7 +631,7 @@ export default function AdminDashboard() {
- value={(isEditing.cardType || 'INFO_PAGE') as CardType} onChange={(v) => setIsEditing({ ...isEditing, cardType: v })} options={[ diff --git a/app/api/fonts/route.ts b/app/api/fonts/route.ts new file mode 100644 index 0000000..1d69a55 --- /dev/null +++ b/app/api/fonts/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +export const dynamic = 'force-dynamic'; + +const MIME: Record = { + '.woff2': 'font/woff2', + '.woff': 'font/woff', + '.ttf': 'font/ttf', + '.otf': 'font/otf', +}; + +const FONTS_DIR = path.join(process.cwd(), 'data', 'fonts'); + +function ensureFontsDir() { + try { fs.mkdirSync(FONTS_DIR, { recursive: true }); } catch {} +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const name = searchParams.get('name'); + + if (name) { + // Serve un singolo file font + const safeName = path.basename(name); // protezione path traversal + const ext = path.extname(safeName).toLowerCase(); + const mimeType = MIME[ext]; + if (!mimeType) return new NextResponse('Unsupported format', { status: 400 }); + + const filePath = path.join(FONTS_DIR, safeName); + try { + const buffer = fs.readFileSync(filePath); + return new NextResponse(buffer, { + headers: { + 'Content-Type': mimeType, + 'Cache-Control': 'public, max-age=31536000, immutable', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch { + return new NextResponse('Font not found', { status: 404 }); + } + } + + // List mode: ritorna i font "regular" (non-italic) presenti + ensureFontsDir(); + try { + const files = fs.readdirSync(FONTS_DIR); + const list = files + .filter(f => /\.(woff2?|ttf|otf)$/i.test(f)) + .filter(f => !/italic/i.test(f)) + .sort(); + return NextResponse.json(list); + } catch { + return NextResponse.json([]); + } +} diff --git a/app/api/portals/route.ts b/app/api/portals/route.ts index ed8d28f..11ee59a 100644 --- a/app/api/portals/route.ts +++ b/app/api/portals/route.ts @@ -22,10 +22,10 @@ export async function POST(request: Request) { } await savePortals(portals); - - // ADD THIS LINE - revalidatePath('/'); - + + // Rivalida sia la home che il layout (il font @font-face è applicato nel layout root) + revalidatePath('/', 'layout'); + return NextResponse.json(portals[0], { status: 200 }); } catch (error) { return NextResponse.json({ error: 'Failed to save portal settings' }, { status: 500 }); diff --git a/app/layout.tsx b/app/layout.tsx index 03c5239..03c03fa 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,10 @@ import type { Metadata } from "next"; import { GeistSans } from "geist/font/sans"; import { GeistMono } from "geist/font/mono"; +import fs from "fs/promises"; +import path from "path"; +import { getPortals } from "@/lib/db"; +import { DEFAULT_FONT } from "@/lib/config"; import "./globals.css"; export const metadata: Metadata = { @@ -8,16 +12,79 @@ export const metadata: Metadata = { description: "Welcome", }; -export default function RootLayout({ +function fontFormat(ext: string): string { + switch (ext.toLowerCase()) { + case ".woff2": return "woff2"; + case ".woff": return "woff"; + case ".ttf": return "truetype"; + case ".otf": return "opentype"; + default: return "woff2"; + } +} + +async function findItalicSibling(regularFilename: string): Promise { + const m = regularFilename.match(/^(.*)(\.(?:woff2?|ttf|otf))$/i); + if (!m) return null; + const [, base, ext] = m; + const candidates = [ + `${base}-Italic${ext}`, `${base}-italic${ext}`, + `${base}_Italic${ext}`, `${base}_italic${ext}`, + `${base} Italic${ext}`, `${base} italic${ext}`, + `${base}Italic${ext}`, `${base}italic${ext}`, + ]; + try { + const files = await fs.readdir(path.join(process.cwd(), "data", "fonts")); + for (const c of candidates) { + if (files.includes(c)) return c; + } + } catch {} + return null; +} + +export default async function RootLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: Readonly<{ children: React.ReactNode }>) { + // Leggi il portale per scegliere il font + let chosenFont = DEFAULT_FONT; + try { + const portals = await getPortals(); + const portal = portals[0]; + if (portal && portal.fontFamily !== undefined) chosenFont = portal.fontFamily; + } catch {} + + let fontStyleCss = ""; + if (chosenFont) { + const regularExt = path.extname(chosenFont); + const italicFile = await findItalicSibling(chosenFont); + const italicExt = italicFile ? path.extname(italicFile) : ""; + const regularUrl = `/api/fonts?name=${encodeURIComponent(chosenFont)}`; + const italicUrl = italicFile ? `/api/fonts?name=${encodeURIComponent(italicFile)}` : ""; + + fontStyleCss = ` +@font-face { + font-family: 'PortalFont'; + src: url('${regularUrl}') format('${fontFormat(regularExt)}'); + font-style: normal; + font-display: swap; +}${italicFile ? ` +@font-face { + font-family: 'PortalFont'; + src: url('${italicUrl}') format('${fontFormat(italicExt)}'); + font-style: italic; + font-display: swap; +}` : ""} +body { font-family: 'PortalFont', Arial, Helvetica, sans-serif; } +`; + } + return ( + + {fontStyleCss &&