diff --git a/README.md b/README.md
index 18536ca..a10c45a 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,19 @@ Guests open a shareable event link, optionally add a name, email, and note,
and upload photos. Event people approve what appears in the gallery.
Originals stay full quality in S3-compatible object storage.
+## Image processing
+
+Uploads go directly to object storage; the background worker creates WebP
+derivatives with Sharp. Displays fit within 2048 × 2048 (quality 82), thumbnails
+within 400 × 400 (quality 75), and cropped 8:3 banners within 2400 × 900
+(quality 82). Encoding uses effort 4. Small images are not enlarged. Display
+copies are auto-oriented and omit source metadata; original uploads and exports
+retain their original bytes.
+
+Existing JPEG derivatives remain supported through their saved database keys.
+Only newly processed images use `.webp` keys and `image/webp`; this change does
+not bulk-regenerate existing images or delete their JPEGs.
+
## Repository
```text
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index 7de91ce..686a304 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -3,6 +3,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
poweredByHeader: false,
+ images: { qualities: [75, 85] },
async redirects() {
return [{
source: "/:path*",
diff --git a/apps/web/package.json b/apps/web/package.json
index e2b52e5..ef198e0 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -15,6 +15,9 @@
"@album/database": "workspace:*",
"@album/email": "workspace:*",
"@album/storage": "workspace:*",
+ "@fontsource-variable/funnel-display": "^5.3.0",
+ "@fontsource-variable/geologica": "^5.3.0",
+ "@fontsource-variable/inter": "^5.3.0",
"@radix-ui/react-popover": "^1.1.23",
"@tanstack/react-query": "^5.90.2",
"@trpc/client": "^11.4.3",
@@ -27,13 +30,16 @@
"date-fns": "^4.4.0",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.45.2",
+ "heic-to": "^1.5.2",
"lucide-react": "^0.468.0",
"next": "16.2.12",
"next-themes": "^0.4.6",
+ "qrcode": "^1.5.4",
"radix-ui": "^1.6.7",
"react": "19.2.8",
"react-day-picker": "^9",
"react-dom": "19.2.8",
+ "react-image-crop": "^11.1.2",
"server-only": "^0.0.1",
"shadcn": "^4.21.0",
"sonner": "^2.0.7",
@@ -46,9 +52,12 @@
"@tailwindcss/postcss": "^4.1.10",
"@types/bun": "^1.3.14",
"@types/node": "^22.15.32",
+ "@types/qrcode": "^1.5.6",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
+ "jsqr": "^1.4.0",
"postcss": "^8.5.22",
+ "sharp": "0.35.3",
"tailwindcss": "^4.1.10",
"typescript": "^5.8.3"
}
diff --git a/apps/web/public/branding/hadlock-tech.svg b/apps/web/public/branding/hadlock-tech.svg
new file mode 100644
index 0000000..873acfa
--- /dev/null
+++ b/apps/web/public/branding/hadlock-tech.svg
@@ -0,0 +1,78 @@
+
+
+
diff --git a/apps/web/public/fonts/Funnel-Display-LICENSE.txt b/apps/web/public/fonts/Funnel-Display-LICENSE.txt
new file mode 100644
index 0000000..6c994ae
--- /dev/null
+++ b/apps/web/public/fonts/Funnel-Display-LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2023 The Funnel Project Authors (https://github.com/Dicotype/Funnel)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/apps/web/public/fonts/Geologica-LICENSE.txt b/apps/web/public/fonts/Geologica-LICENSE.txt
new file mode 100644
index 0000000..8357dcc
--- /dev/null
+++ b/apps/web/public/fonts/Geologica-LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Geologica Project Authors (https://github.com/googlefonts/geologica)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/apps/web/public/fonts/Inter-LICENSE.txt b/apps/web/public/fonts/Inter-LICENSE.txt
new file mode 100644
index 0000000..40589da
--- /dev/null
+++ b/apps/web/public/fonts/Inter-LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/apps/web/public/fonts/funnel-display.woff2 b/apps/web/public/fonts/funnel-display.woff2
new file mode 100644
index 0000000..bb79810
Binary files /dev/null and b/apps/web/public/fonts/funnel-display.woff2 differ
diff --git a/apps/web/public/fonts/geologica.woff2 b/apps/web/public/fonts/geologica.woff2
new file mode 100644
index 0000000..f5c1294
Binary files /dev/null and b/apps/web/public/fonts/geologica.woff2 differ
diff --git a/apps/web/public/fonts/inter.woff2 b/apps/web/public/fonts/inter.woff2
new file mode 100644
index 0000000..d15208d
Binary files /dev/null and b/apps/web/public/fonts/inter.woff2 differ
diff --git a/apps/web/src/app/dashboard/events/[id]/event-settings-form.tsx b/apps/web/src/app/dashboard/events/[id]/event-settings-form.tsx
index 418cc9f..af0209e 100644
--- a/apps/web/src/app/dashboard/events/[id]/event-settings-form.tsx
+++ b/apps/web/src/app/dashboard/events/[id]/event-settings-form.tsx
@@ -246,22 +246,24 @@ export function EventSettingsForm({
Choose from approved gallery photos ({bannerPhotos.length})
-
- Dedicated uploads stay out of the gallery. Gallery banners follow gallery visibility rules. Images are center-cropped for this preview; page layouts may crop differently.
+ Banners use an 8:3 frame everywhere. Uploads use your chosen crop and stay out of the gallery. Gallery photos are center-cropped and follow gallery visibility rules.Publish notes
diff --git a/apps/web/src/app/dashboard/events/[id]/page.tsx b/apps/web/src/app/dashboard/events/[id]/page.tsx
index 1aeafa1..9c2ff5d 100644
--- a/apps/web/src/app/dashboard/events/[id]/page.tsx
+++ b/apps/web/src/app/dashboard/events/[id]/page.tsx
@@ -1,4 +1,7 @@
import { notFound } from "next/navigation";
+import Link from "next/link";
+import { QrCodeIcon } from "lucide-react";
+import { Button } from "@/components/ui/button";
import { createServerCaller } from "@/trpc/server";
import { Badge } from "@/components/ui/badge";
import { EventWorkspace, type EventWorkspaceTab } from "@/components/event-workspace";
@@ -132,10 +135,11 @@ export default async function EventDashboardPage({
Gallery held
)}
-
+ Portrait, square, or landscape. 3–48 inches per edge. Small signs should be scanned up close.
+ > : null}
+ Design preset
+ Heading font
+ {design.layout === "compact" ? "The Linen text-and-QR design without a photo. Starts at 6 × 4 inches; choose any paper size above." : design.layout === "photo" ? "A large event photo with a compact QR section. Add a photo below to enable sign export." : "Bold typography, a full-color background, and a prominent QR. Your uploaded photo is kept for photo presets."}
+
+ Background onChange({ background: e.target.value })} />
+ Accent onChange({ accent: e.target.value })} />Text adapts for contrast against the background.
+
+ {([{ key: "backgroundImage", label: "Background photo" }, { key: "logoImage", label: "Your logo" }] as const).map(field =>
+ {field.label} { const file = e.target.files?.[0]; e.target.value = ""; if (file) void upload(file, field.key); }} />
+ {design[field.key] ? : null}
+ {field.key === "logoImage" ? "Transparent PNG works best. Replaces the Manyangles logo." : "Used by photo presets. Cropped to fill the photo area; switch presets without losing your upload."} Embedded in your export; never added to the event gallery.
+ )}
+ Manyangles logo
+ >;
+}
diff --git a/apps/web/src/app/dashboard/events/[id]/sign/page.tsx b/apps/web/src/app/dashboard/events/[id]/sign/page.tsx
new file mode 100644
index 0000000..7746b25
--- /dev/null
+++ b/apps/web/src/app/dashboard/events/[id]/sign/page.tsx
@@ -0,0 +1,15 @@
+import { notFound } from "next/navigation";
+import { createServerCaller } from "@/trpc/server";
+import { effectiveEvent } from "@/lib/event-lifecycle";
+import { signQr } from "@/server/sign-qr";
+import { GuestSignStudio } from "./studio";
+
+export default async function GuestSignPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const caller = await createServerCaller();
+ let event;
+ try { event = await caller.manager.event({ eventId: id }); } catch { notFound(); }
+ const current = effectiveEvent(event);
+ return ;
+}
diff --git a/apps/web/src/app/dashboard/events/[id]/sign/studio.tsx b/apps/web/src/app/dashboard/events/[id]/sign/studio.tsx
new file mode 100644
index 0000000..f901dae
--- /dev/null
+++ b/apps/web/src/app/dashboard/events/[id]/sign/studio.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import Link from "next/link";
+import { useEffect, useRef, useState } from "react";
+import { ArrowLeftIcon, DownloadIcon, PrinterIcon, QrCodeIcon } from "lucide-react";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { guestQrSvg, guestSignSvg, signPrintHtml, signPaper, SIGN_PAPERS, type SignDesign, type SignQr, type SignPaper } from "@/lib/guest-sign";
+import { SignDesignControls } from "./design-controls";
+
+export function GuestSignStudio({ eventId, title, guestUrl, qr, warning }: {
+ eventId: string; title: string; guestUrl: string; qr: SignQr; warning: string | null;
+}) {
+ const [design, setDesign] = useState({ title, headline: "Share your favorite moments", message: "Help us see the day through your eyes. Add your photos to our shared album.", paper: "letter", ink: "indigo", font: "funnel", layout: "typography", background: "#4055b5", accent: "#ffffff", decoration: "minimal", showBrand: true, customWidth: 8.5, customHeight: 11, unit: "in" });
+ const [fontError, setFontError] = useState(false);
+ const [assetBusy, setAssetBusy] = useState(false);
+ useEffect(() => {
+ let active = true;
+ void Promise.all(["funnel-display", "inter", "geologica"].map(async name => {
+ const response = await fetch(`/fonts/${name}.woff2`);
+ if (!response.ok) throw new Error("Font unavailable");
+ const bytes = new Uint8Array(await response.arrayBuffer());
+ let binary = "";
+ for (const byte of bytes) binary += String.fromCharCode(byte);
+ return `data:font/woff2;base64,${btoa(binary)}`;
+ })).then(([fontData, interFontData, geologicaFontData]) => {
+ if (active) setDesign(d => ({ ...d, fontData, interFontData, geologicaFontData }));
+ }).catch(() => { if (active) setFontError(true); });
+ return () => { active = false; };
+ }, []);
+ const printFrame = useRef(null);
+ const [printing, setPrinting] = useState(false);
+ useEffect(() => () => { printFrame.current?.remove(); }, []);
+ let sizeError: string | null = null;
+ let paper = SIGN_PAPERS.letter as ReturnType;
+ try { paper = signPaper(design); } catch (error) { sizeError = error instanceof Error ? error.message : "Invalid paper size"; }
+ const svg = guestSignSvg(sizeError ? { ...design, paper: "letter" } : design, guestUrl, qr);
+ const fontReady = Boolean(design.fontData && design.interFontData && design.geologicaFontData);
+ const valid = Boolean(design.title.trim() && design.headline.trim() && design.message.trim() && !sizeError && fontReady && !assetBusy && (design.layout !== "photo" || design.backgroundImage));
+ const localUrl = ["localhost", "127.0.0.1", "[::1]"].includes(new URL(guestUrl).hostname);
+ function downloadSvg(content: string, filename: string) {
+ const url = URL.createObjectURL(new Blob([content], { type: "image/svg+xml;charset=utf-8" }));
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = filename;
+ link.click();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ }
+ function print() {
+ printFrame.current?.remove();
+ const frame = document.createElement("iframe");
+ frame.title = "Printable guest sign";
+ frame.setAttribute("aria-hidden", "true");
+ frame.tabIndex = -1;
+ frame.style.cssText = "position:fixed;left:-10000px;top:0;width:850px;height:1202px;border:0";
+ printFrame.current = frame;
+ setPrinting(true);
+ frame.onload = async () => {
+ try {
+ if (!frame.contentWindow) throw new Error("Print window unavailable");
+ await frame.contentDocument?.fonts.ready;
+ await Promise.all(Array.from(frame.contentDocument?.images ?? []).map(image => image.decode()));
+ frame.contentWindow.addEventListener("afterprint", () => frame.remove(), { once: true });
+ frame.contentWindow.focus();
+ frame.contentWindow.print();
+ } catch { toast.error("Printing is unavailable here. Download the SVG and open it in your browser to print."); }
+ finally { setPrinting(false); }
+ };
+ frame.srcdoc = signPrintHtml(svg, design);
+ document.body.append(frame);
+ }
+ return
+
+
+ Back to event
+
Guest sign studio
+
A little sign. Everyone’s photos. Customize, preview, and print.
+
+
+
+
+
+
+
+
+
+ Make it yoursEdits update the preview only, not your event. Download your sign before leaving.
+
+
+ Paper size
+
+
+ setDesign(d => ({ ...d, ...patch }))} onBusy={setAssetBusy} />
+ {sizeError ?
{sizeError} Preview uses Letter until corrected.
: null}
+ {!fontReady ?
{fontError ? "Our fonts could not load. Reload before exporting." : "Loading embedded brand fonts…"}
Uses the saved guest link, not a temporary download URL. Changing the event slug later means reprinting the sign.
+
{localUrl ? "Local preview: this QR points to localhost and will not work on guests’ phones. Generate the final sign on your deployed site." : "Before printing a batch, scan one copy with your phone."}{warning ? ` ${warning}` : ""}
+
Print at 100% on the selected paper size, with browser headers and footers off. The sign includes a safe page margin and a clear border around the QR code.
+
+
+
+
+
Live print preview{paper.width} × {paper.height}
+
+
+
+
;
+}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index c5b180a..44c188c 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,11 +1,11 @@
import Link from "next/link";
import Image from "next/image";
-import weddingExit from "@/assets/homepage/457677937.webp";
+import gardenToast from "@/assets/homepage/2157148814.webp";
+import guestSelfie from "@/assets/homepage/2157148813.webp";
+import weddingGreetings from "@/assets/homepage/599131260.webp";
import guestTunnel from "@/assets/homepage/599129661.webp";
import firstDance from "@/assets/homepage/498647922.webp";
-import toast from "@/assets/homepage/240365594.webp";
import guest from "@/assets/homepage/189193781.webp";
-import reception from "@/assets/homepage/208516668.webp";
import {
ArrowRightIcon,
CheckCircle2Icon,
@@ -18,6 +18,7 @@ import { createServerCaller } from "@/trpc/server";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { BrandMark } from "@/components/brand-mark";
+import { SiteFooter } from "@/components/site-footer";
import { cn, formatEventDate } from "@/lib/utils";
import {
Card,
@@ -38,7 +39,7 @@ const features = [
number: "02",
icon: ShieldCheckIcon,
title: "You decide what’s public",
- body: "Every photo lands in a private review queue before it reaches the shared gallery.",
+ body: "Approve photos before sharing, publish automatically, or keep the gallery private. You choose.",
},
{
number: "03",
@@ -51,10 +52,10 @@ const features = [
const previewPhotos = [
{ src: guest, layout: "row-span-2", position: "50% 40%" },
{ src: firstDance, layout: "col-span-2", position: "50% 45%" },
- { src: toast, layout: "", position: "50% 50%" },
+ { src: guestSelfie, layout: "", position: "50% 40%" },
{ src: guestTunnel, layout: "row-span-2", position: "50% 50%" },
- { src: reception, layout: "", position: "65% 50%" },
- { src: weddingExit, layout: "", position: "50% 50%" },
+ { src: gardenToast, layout: "", position: "50% 35%" },
+ { src: weddingGreetings, layout: "", position: "50% 50%" },
] as const;
function AlbumPreview() {
@@ -84,10 +85,10 @@ function AlbumPreview() {
src={photo.src}
alt=""
fill
- sizes={index === 1 ? "(max-width: 640px) 60vw, 360px" : "(max-width: 640px) 30vw, 180px"}
+ sizes={index === 0 || index === 3 ? "(max-width: 640px) 80vw, 480px" : index === 1 ? "(max-width: 640px) 60vw, 360px" : "(max-width: 640px) 40vw, 240px"}
+ quality={85}
className="object-cover"
style={{ objectPosition: photo.position }}
- placeholder="blur"
loading="eager"
/>
@@ -117,6 +118,7 @@ export default async function HomePage() {
const demoEvent = listed[0];
return (
+ <>