Преглед на файлове

Font selection

Sviluppo_Carrello_Immagini
Lorenzo Pollutri преди 1 месец
родител
ревизия
1caeedbec6
променени са 7 файла, в които са добавени 164 реда и са изтрити 14 реда
  1. +22
    -5
      app/admin/page.tsx
  2. +58
    -0
      app/api/fonts/route.ts
  3. +4
    -4
      app/api/portals/route.ts
  4. +71
    -4
      app/layout.tsx
  5. +5
    -0
      lib/config.ts
  6. +3
    -1
      lib/db.ts
  7. +1
    -0
      types/index.ts

+ 22
- 5
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<T extends string>({
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<HTMLDivElement>(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<string[]>([]);
// 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<HTMLInputElement>, field: string, isPortal = false) => {
@@ -519,6 +521,21 @@ export default function AdminDashboard() {
</div>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Font del portale</label>
<StyledSelect<string>
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, '') })),
]}
/>
<p className="text-xs text-gray-500 mt-1">
Per aggiungere altri font, copia file <code className="bg-gray-100 px-1 rounded">.woff2</code>, <code className="bg-gray-100 px-1 rounded">.woff</code>, <code className="bg-gray-100 px-1 rounded">.ttf</code> o <code className="bg-gray-100 px-1 rounded">.otf</code> in <code className="bg-gray-100 px-1 rounded">data/fonts/</code> e ricarica questa pagina.
</p>
</div>
</div>
<div className="space-y-6">
@@ -614,7 +631,7 @@ export default function AdminDashboard() {
</div>
<div>
<label className="block text-sm font-semibold text-gray-800 mb-1">Card Type</label>
<CardTypeSelect
<StyledSelect<CardType>
value={(isEditing.cardType || 'INFO_PAGE') as CardType}
onChange={(v) => setIsEditing({ ...isEditing, cardType: v })}
options={[


+ 58
- 0
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<string, string> = {
'.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([]);
}
}

+ 4
- 4
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 });


+ 71
- 4
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<string | null> {
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 (
<html
lang="it"
className={`${GeistSans.variable} ${GeistMono.variable} h-full antialiased`}
>
<head>
{fontStyleCss && <style dangerouslySetInnerHTML={{ __html: fontStyleCss }} />}
</head>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);


+ 5
- 0
lib/config.ts Целия файл

@@ -1,3 +1,8 @@
// Feature flags compilati nel bundle. Modifica e ricostruisci (npm run build) per applicare.

export const EXTERNAL_LINK_ENABLED = true;

// Font di default se il portale non ne ha impostato uno.
// Lascia stringa vuota per usare il font di sistema (Arial).
// Per usare un font, scrivi il nome esatto del file presente in data/fonts/ (es. "Geist-Variable.woff2").
export const DEFAULT_FONT = '';

+ 3
- 1
lib/db.ts Целия файл

@@ -6,11 +6,13 @@ const DATA_DIR = path.join(process.cwd(), 'data');
const PORTALS_FILE = path.join(DATA_DIR, 'portals.txt');
const CARDS_FILE = path.join(DATA_DIR, 'cards.txt');
// Helper to ensure files exist
// Helper to ensure files/folders exist
async function ensureDb() {
try { await fs.access(DATA_DIR); } catch { await fs.mkdir(DATA_DIR); }
try { await fs.access(PORTALS_FILE); } catch { await fs.writeFile(PORTALS_FILE, '[]'); }
try { await fs.access(CARDS_FILE); } catch { await fs.writeFile(CARDS_FILE, '[]'); }
const FONTS_DIR = path.join(DATA_DIR, 'fonts');
try { await fs.access(FONTS_DIR); } catch { await fs.mkdir(FONTS_DIR, { recursive: true }); }
}
export async function getPortals(): Promise<Portal[]> {


+ 1
- 0
types/index.ts Целия файл

@@ -33,4 +33,5 @@ export interface Portal {
fadeHeroImage?: boolean;
maxGridColumns?: number;
externalLinkEnabled?: boolean; // se false, l'admin nasconde il tipo "External Link" nel dropdown
fontFamily?: string; // nome del file in data/fonts/ (es. "Geist-Variable.woff2"). "" o undefined = Sistema (Arial)
}

Зареждане…
Отказ
Запис