diff --git a/app/app/routes/app.settings.tsx b/app/app/routes/app.settings.tsx index b0bdd51..76f5971 100644 --- a/app/app/routes/app.settings.tsx +++ b/app/app/routes/app.settings.tsx @@ -12,6 +12,7 @@ import { Card, Tabs, TextField, + Select, Button, ButtonGroup, Banner, @@ -27,6 +28,18 @@ import { authenticate } from "../shopify.server"; import db from "../db.server"; import { decryptSecret, encryptSecret } from "../lib/crypto.server"; import { sendTestEmail } from "../lib/mailer.server"; +import { renderStep2 } from "../lib/recesso.view"; +import { statementTemplate } from "../lib/recesso.copy"; +import { + FONT_OPTIONS, + FONT_PRESETS, + RADIUS_MAX, + RADIUS_MIN, + WIDTH_MAX, + WIDTH_MIN, + isHexColor, + type ThemeTokens, +} from "../lib/theme"; import { DEFAULT_INTRO, DEFAULT_NOTE, @@ -69,6 +82,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { // Esiste un SMTP di default a livello app (env/fly secret)? In prod oggi NO: // senza SMTP per-shop non parte nessuna ricevuta. appDefaultSmtp: !!process.env.SMTP_HOST, + themeAccent: s?.themeAccent ?? "", + themeButtonBg: s?.themeButtonBg ?? "", + themeButtonText: s?.themeButtonText ?? "", + themeRadius: s?.themeRadius != null ? String(s.themeRadius) : "", + themeFont: s?.themeFont ?? "system", + themeWidth: s?.themeWidth != null ? String(s.themeWidth) : "", + themeCustomCss: s?.themeCustomCss ?? "", }; }; @@ -144,6 +164,15 @@ export const action = async ({ request }: ActionFunctionArgs) => { smtpUser: String(f.get("smtpUser") ?? "").trim() || null, smtpSecure: f.get("smtpSecure") === "true", smtpFrom: String(f.get("smtpFrom") ?? "").trim() || null, + // Tema: si salva solo cio' che e' valido. Un valore sbagliato non viene + // scritto, cosi' il form ricade sul default invece di rompersi. + themeAccent: hexOrNull(f.get("themeAccent")), + themeButtonBg: hexOrNull(f.get("themeButtonBg")), + themeButtonText: hexOrNull(f.get("themeButtonText")), + themeRadius: intInRange(f.get("themeRadius"), RADIUS_MIN, RADIUS_MAX), + themeWidth: intInRange(f.get("themeWidth"), WIDTH_MIN, WIDTH_MAX), + themeFont: fontOrNull(f.get("themeFont")), + themeCustomCss: String(f.get("themeCustomCss") ?? "").trim() || null, }; // Password SMTP: cifrata solo se fornita; vuota = invariata. @@ -160,6 +189,65 @@ export const action = async ({ request }: ActionFunctionArgs) => { return { ok: true, tested: false, error: null }; }; +function hexOrNull(v: FormDataEntryValue | null): string | null { + const s = String(v ?? "").trim(); + return isHexColor(s) ? s : null; +} +function intInRange( + v: FormDataEntryValue | null, + min: number, + max: number, +): number | null { + const n = Number(String(v ?? "").trim()); + if (!Number.isFinite(n) || n === 0) return null; + return Math.min(max, Math.max(min, Math.round(n))); +} +function fontOrNull(v: FormDataEntryValue | null): string | null { + const s = String(v ?? "").trim(); + return s && FONT_PRESETS[s] ? s : null; +} + +function ColorField(props: { + label: string; + value: string; + onChange: (v: string) => void; + placeholder?: string; + helpText?: string; +}) { + const valid = isHexColor(props.value); + const invalid = !valid && props.value.trim().length > 0; + return ( + +
+ +
+ props.onChange(e.currentTarget.value)} + style={{ + width: 40, + height: 36, + padding: 0, + border: "1px solid #c9cccf", + borderRadius: 8, + background: "none", + cursor: "pointer", + }} + /> +
+ ); +} + function opFrame(html: string, height = 130) { const doc = `${html}
`; return ( @@ -210,6 +298,13 @@ export default function SettingsPage() { const [smtpSecure, setSmtpSecure] = useState(d.smtpSecure); const [smtpFrom, setSmtpFrom] = useState(d.smtpFrom); const [testTo, setTestTo] = useState(d.notifyEmail); + const [themeAccent, setThemeAccent] = useState(d.themeAccent); + const [themeButtonBg, setThemeButtonBg] = useState(d.themeButtonBg); + const [themeButtonText, setThemeButtonText] = useState(d.themeButtonText); + const [themeRadius, setThemeRadius] = useState(d.themeRadius); + const [themeFont, setThemeFont] = useState(d.themeFont); + const [themeWidth, setThemeWidth] = useState(d.themeWidth); + const [themeCustomCss, setThemeCustomCss] = useState(d.themeCustomCss); const [showSaved, setShowSaved] = useState(false); const saving = nav.state === "submitting"; @@ -255,6 +350,37 @@ export default function SettingsPage() { [opTextShipped, opTextUnfulfilled, returnAddress, returnAtCustomerExpense], ); + // Anteprima FEDELE: stesso renderer e stesso CSS dello storefront (recesso.view). + const themePreview = useMemo(() => { + const tokens: ThemeTokens = { + accent: themeAccent, + buttonBg: themeButtonBg, + buttonText: themeButtonText, + radius: Number(themeRadius) || null, + font: themeFont, + width: Number(themeWidth) || null, + customCss: themeCustomCss, + }; + return renderStep2( + { + orderId: "gid://shopify/Order/0", + orderName: "#1001", + email: "mario.rossi@example.com", + customerName: "Mario Rossi", + statementText: statementTemplate("#1001"), + }, + tokens, + ); + }, [ + themeAccent, + themeButtonBg, + themeButtonText, + themeRadius, + themeFont, + themeWidth, + themeCustomCss, + ]); + const handleTest = () => { setShowSaved(false); const fd = new FormData(); @@ -294,9 +420,27 @@ export default function SettingsPage() { fd.set("smtpPass", smtpPass); fd.set("smtpSecure", String(smtpSecure)); fd.set("smtpFrom", smtpFrom); + fd.set("themeAccent", themeAccent); + fd.set("themeButtonBg", themeButtonBg); + fd.set("themeButtonText", themeButtonText); + fd.set("themeRadius", themeRadius); + fd.set("themeFont", themeFont); + fd.set("themeWidth", themeWidth); + fd.set("themeCustomCss", themeCustomCss); submit(fd, { method: "post" }); }; + const resetTheme = () => { + setThemeAccent(""); + setThemeButtonBg(""); + setThemeButtonText(""); + setThemeRadius(""); + setThemeFont("system"); + setThemeWidth(""); + setThemeCustomCss(""); + setShowSaved(false); + }; + const resetEmail = () => { setSubject(DEFAULT_SUBJECT); setIntro(DEFAULT_INTRO); @@ -318,6 +462,7 @@ export default function SettingsPage() { { id: "regole", content: "Regole recesso" }, { id: "reso", content: "Reso e stato ordine" }, { id: "smtp", content: "Email (SMTP)" }, + { id: "aspetto", content: "Aspetto" }, ]; return ( @@ -706,6 +851,117 @@ export default function SettingsPage() { ) : null} + + {tab === 5 ? ( + + + + + + Aspetto del form + + + Campi vuoti = default. Un valore non valido non viene + salvato: il form ricade sul default invece di rompersi. + + + + + + + +