Allow hiding the printed URL on guest signs
This commit is contained in:
@@ -6,6 +6,7 @@ import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import type { SignDesign } from "@/lib/guest-sign";
|
||||
import { signPaper } from "@/lib/guest-sign";
|
||||
@@ -63,6 +64,11 @@ export function SignDesignControls({ design, onChange, onBusy }: { design: SignD
|
||||
finally { URL.revokeObjectURL(url); setBusy(false); onBusy(false); }
|
||||
}
|
||||
return <>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="sign-show-url">Show URL on sign</FieldLabel>
|
||||
<Switch id="sign-show-url" checked={design.showUrl !== false} onCheckedChange={showUrl => onChange({ showUrl })} aria-describedby="sign-show-url-help" />
|
||||
<FieldDescription id="sign-show-url-help">Hide the printed web address for a cleaner card. The QR still works. Save design to keep this setting.</FieldDescription>
|
||||
</Field>
|
||||
{cropFile ? <BannerCropDialog key={`${cropFile.name}-${cropAspect}`} file={cropFile} aspect={cropAspect} title="Frame your background photo" description="Drag the crop frame to reposition it, or resize it to zoom in. The shaded area will be cut off." hint="Matches the photo area in your current sign orientation. Apply to preview it on the sign, then save your design. Changing orientation may require another crop." confirmLabel="Apply crop" onCancel={() => setCropFile(null)} onConfirm={crop => { const file = cropFile; setCropFile(null); void upload(file, "backgroundImage", crop); }} /> : null}
|
||||
{design.paper === "custom" ? <>
|
||||
<Field><FieldLabel htmlFor="sign-unit">Units</FieldLabel><Select value={design.unit ?? "in"} onValueChange={value => {
|
||||
|
||||
@@ -14,6 +14,19 @@ test("standalone QR export scans to the guest link and keeps its quiet zone", as
|
||||
});
|
||||
|
||||
const design: SignDesign = { title: "Sam & Alex’s wedding", headline: "Share your favorite moments", message: "Add your photos to our shared album.", paper: "letter", ink: "indigo" };
|
||||
test("URL can be hidden in every layout without changing QR content", async () => {
|
||||
const url = "https://ma.hadlock.tech/e/demo", qr = signQr(url);
|
||||
for (const layout of [undefined, "compact", "typography", "photo"] as const) {
|
||||
const visible = guestSignSvg({ ...design, layout }, url, qr);
|
||||
const hidden = guestSignSvg({ ...design, layout, showUrl: false }, url, qr);
|
||||
expect(visible).toContain("ma.hadlock.tech");
|
||||
expect(hidden).not.toContain("ma.hadlock.tech");
|
||||
expect(hidden).toContain(qr.path);
|
||||
expect(guestSignSvg({ ...design, layout, showUrl: true }, url, qr)).toBe(visible);
|
||||
const { data, info } = await sharp(Buffer.from(hidden), { density: 120 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||
expect(jsQR(new Uint8ClampedArray(data), info.width, info.height)?.data).toBe(url);
|
||||
}
|
||||
});
|
||||
test("text controls change each text independently without resizing the QR", () => {
|
||||
const url = "https://ma.hadlock.tech/e/demo", qr = signQr(url);
|
||||
const base: SignDesign = { ...design, layout: "compact", paper: "card6x4", title: "Event", headline: "Share", message: "Welcome" };
|
||||
|
||||
@@ -117,7 +117,7 @@ export function guestSignSvg(design: SignDesign, guestUrl: string, qr: SignQr) {
|
||||
${sizedText(design.message, textX, landscape ? 450 : 355, 29, design.messageScale ?? 100, textWidth, landscape ? 150 : 100)}
|
||||
<svg x="${codeX}" y="${codeY}" width="${qrSize}" height="${qrSize}" viewBox="0 0 ${qr.size + 8} ${qr.size + 8}" shape-rendering="crispEdges"><rect width="100%" height="100%" fill="white"/><path d="${qr.path}" transform="translate(4 4)" fill="black"/></svg>
|
||||
${sizedText("Scan to upload", codeX + qrSize / 2, codeY + qrSize + 45, 25, 100, qrSize, 40, 500, true)}
|
||||
${sizedText(guestUrl.replace(/^https?:\/\//, ""), codeX + qrSize / 2, codeY + qrSize + 78, 16, 100, qrSize + 40, 50, 400, true)}
|
||||
${design.showUrl === false ? "" : sizedText(guestUrl.replace(/^https?:\/\//, ""), codeX + qrSize / 2, codeY + qrSize + 78, 16, 100, qrSize + 40, 50, 400, true)}
|
||||
${sizedText(design.title, textX, footerY, 23, design.titleScale ?? 100, landscape ? 530 : 480, 65, 500)}
|
||||
<g transform="translate(${landscape ? 948 : 598} ${footerY - 21}) scale(.7) translate(${-(cx - 100)} ${-(landscape ? 700 : 1005)})">${brandMarkup}</g>
|
||||
</g>
|
||||
@@ -139,7 +139,7 @@ export function guestSignSvg(design: SignDesign, guestUrl: string, qr: SignQr) {
|
||||
${design.messageScale === undefined ? lines(design.message, left, messageY, compact ? 26 : 18, compact ? (landscape ? 38 : 24) : photoLed ? (landscape ? 42 : 35) : (landscape ? 48 : 32)) : sizedText(design.message, left, messageY, compact ? 26 : 18, design.messageScale, landscape && photoLed ? column : Math.min(column, qrX - left - 40), photoLed ? (landscape ? 65 : 85) : landscape ? 230 : 280)}
|
||||
<svg x="${qrX}" y="${qrY}" width="${qrSize}" height="${qrSize}" viewBox="0 0 ${qr.size + 8} ${qr.size + 8}" shape-rendering="crispEdges"><rect width="100%" height="100%" fill="white"/><path d="${qr.path}" transform="translate(4 4)" fill="black"/></svg>
|
||||
${lines("Scan to share", qrX, qrY - 22, compact ? 24 : 18, 30, 600)}
|
||||
${lines(guestUrl.replace(/^https?:\/\//, ""), qrX, qrY + qrSize + 24, compact ? 15 : 11, Math.floor(qrSize / (compact ? 9 : 7)))}
|
||||
${design.showUrl === false ? "" : lines(guestUrl.replace(/^https?:\/\//, ""), qrX, qrY + qrSize + 24, compact ? 15 : 11, Math.floor(qrSize / (compact ? 9 : 7)))}
|
||||
${!photoLed && !compact ? lines("Your photos. Our story.", left, landscape ? 665 : 850, 24, 30, 600) : ""}
|
||||
${lines("No app or account needed.", left, photoLed ? (landscape ? 665 : 950) : (landscape ? 705 : 890), compact ? 22 : 16, photoLed && landscape ? 22 : 32)}
|
||||
${lines("Photos and notes welcome.", left, photoLed ? (landscape ? 710 : 976) : (landscape ? 735 : 920), compact ? 22 : 16, photoLed && landscape ? 22 : 32)}
|
||||
@@ -164,7 +164,7 @@ export function guestSignSvg(design: SignDesign, guestUrl: string, qr: SignQr) {
|
||||
<rect width="100%" height="100%" fill="white"/><path d="${qr.path}" transform="translate(4 4)" fill="black"/>
|
||||
</svg>
|
||||
${text("Scan with your phone camera", landscape ? 644 : 820, 20, 40, 600, qx + 185)}
|
||||
${text(guestUrl.replace(/^https?:\/\//, ""), landscape ? 675 : 854, guestUrl.length > 160 ? 10 : 14, guestUrl.length > 160 ? (landscape ? 68 : 110) : (landscape ? 46 : 78), 400, qx + 185)}
|
||||
${design.showUrl === false ? "" : text(guestUrl.replace(/^https?:\/\//, ""), landscape ? 675 : 854, guestUrl.length > 160 ? 10 : 14, guestUrl.length > 160 ? (landscape ? 68 : 110) : (landscape ? 46 : 78), 400, qx + 185)}
|
||||
<path d="M${landscape ? 90 : 85} ${landscape ? 575 : 901}h${landscape ? 470 : 680}" stroke="${headingInk}" stroke-opacity=".25" stroke-width="1"/>
|
||||
${text("Scan. Choose your photos. Upload.", landscape ? 611 : 935, 21, 50, 500)}
|
||||
${text("No app or account needed. Notes welcome, too.", landscape ? 644 : 965, 16, landscape ? 36 : 55)}
|
||||
|
||||
Reference in New Issue
Block a user