feat/welcome-letter integration #2

Merged
me merged 5 commits from feature/welcome-letter into main 2026-05-06 16:46:53 +00:00
2 changed files with 178 additions and 0 deletions
Showing only changes of commit 0104703852 - Show all commits
@@ -0,0 +1,77 @@
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { getWelcomeContent } from "../../config/welcomeLetter";
import { formatDate } from "../../utils/dateFormat";
import { type CanvasTools, ComposeCanvas } from "../editor/ComposeCanvas";
import { EnvelopeReveal } from "../reader/EnvelopeReveal";
interface WelcomeLetterOverlayProps {
onComplete: () => void;
userName: string;
}
export function WelcomeLetterOverlay({
onComplete,
userName,
}: WelcomeLetterOverlayProps) {
const [revealState, setRevealState] = useState<"SEALED" | "REVEALED">(
"SEALED",
);
const canvasRef = useRef<CanvasTools>(null);
useEffect(() => {
if (revealState === "REVEALED" && canvasRef.current) {
const welcomeContent = getWelcomeContent(userName);
canvasRef.current.loadData(welcomeContent);
}
}, [revealState, userName]);
return (
<div className="fixed inset-0 z-30 backdrop-blur-3xl flex flex-col items-center justify-center p-4 md:p-8 overflow-x-hidden">
<div className="fixed inset-0 bg-vig pointer-events-none z-0" />
<div className="w-full max-w-4xl z-10 flex flex-col items-center">
<AnimatePresence mode="wait">
{revealState === "SEALED" && (
<motion.div
key="envelope"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 0.8, opacity: 1 }}
exit={{
scale: 1,
opacity: 0,
transition: { duration: 0.5, ease: "easeOut" },
}}
transition={{ duration: 4, delay: 1 }}
>
<EnvelopeReveal
recipient={userName}
date={formatDate(new Date())}
onRevealComplete={() => setRevealState("REVEALED")}
ignite={false}
/>
</motion.div>
)}
</AnimatePresence>
<div
className={`w-full space-y-8 py-12 ${revealState === "REVEALED" ? "block" : "hidden"}`}
>
<div className="bg-paper shadow-warm rounded-sm overflow-hidden mx-auto max-w-180">
<div className="p-1 md:p-2 bg-base-content/5 opacity-10 pointer-events-none absolute inset-0 z-10" />
<ComposeCanvas ref={canvasRef} readOnly />
</div>
<div className="flex justify-center mt-12">
<button
type="button"
onClick={onComplete}
className="btn btn-accent opacity-80 px-12 shadow-lg"
>
I'll see you
</button>
</div>
</div>
</div>
</div>
);
}
+101
View File
@@ -0,0 +1,101 @@
import type { CanvasJSON } from "../components/editor/ComposeCanvas";
export function getWelcomeLetterContent(userName: string): CanvasJSON {
return {
objects: [
{
fontSize: 18,
fontWeight: 500,
fontFamily: "Kavivanar",
fontStyle: "normal",
lineHeight: 1.5,
text: `\nDear ${userName}, \n\nYou made it this far, which means something already brought you here. \nA name, maybe. A feeling you haven't been able to shake. Something you typed and deleted too many times to count.\n\nMost people carry it quietly. They tell themselves it doesn't matter anymore, or that too much time has passed, or that the other person wouldn't understand anyway. And maybe they're right. \n\nBut the thing is — the unsaid thing doesn't really care about any of that. \nIt just stays.\n\nSo here you are.\n\nYou don't have to know what you want to say yet. \nYou don't have to have it figured out — who it's for, or why it still matters, or what you're hoping will happen after. \n\nA lot of letters written here start without any of that. They find their way.\n\nTake your time. \nNo one's watching. \n\nWhen you're ready, write a letter.\n\nSometimes the wrong train takes you to the right station.\n- S.F.`,
charSpacing: 0,
textAlign: "left",
styles: [],
pathStartOffset: 0,
pathSide: "left",
pathAlign: "baseline",
underline: false,
overline: false,
linethrough: false,
textBackgroundColor: "",
direction: "ltr",
textDecorationThickness: 66.667,
minWidth: 20,
splitByGrapheme: false,
type: "Textbox",
version: "7.2.0",
originX: "left",
originY: "top",
left: 36,
top: 36,
width: 608,
height: 813.6,
fill: "#111e67",
stroke: null,
strokeWidth: 1,
strokeDashArray: null,
strokeLineCap: "butt",
strokeDashOffset: 0,
strokeLineJoin: "miter",
strokeUniform: false,
strokeMiterLimit: 4,
scaleX: 1,
scaleY: 1,
angle: 0,
flipX: false,
flipY: false,
opacity: 1,
shadow: null,
visible: true,
backgroundColor: "",
fillRule: "nonzero",
paintFirst: "fill",
globalCompositeOperation: "source-over",
skewX: 0,
skewY: 0,
},
{
cropX: 0,
cropY: 0,
type: "Image",
version: "7.2.0",
originX: "left",
originY: "top",
left: 298.4065,
top: 660.2853,
width: 512,
height: 400,
fill: "rgb(0,0,0)",
stroke: null,
strokeWidth: 0,
strokeDashArray: null,
strokeLineCap: "butt",
strokeDashOffset: 0,
strokeLineJoin: "miter",
strokeUniform: false,
strokeMiterLimit: 4,
scaleX: 0.4753,
scaleY: 0.4753,
angle: 355.5436,
flipX: false,
flipY: false,
opacity: 1,
shadow: null,
visible: true,
backgroundColor: "",
fillRule: "nonzero",
paintFirst: "fill",
globalCompositeOperation: "source-over",
skewX: 0,
skewY: 0,
src: "/screenshots/train.png",
crossOrigin: null,
filters: [],
},
],
canvasWidth: 680,
canvasHeight: 900,
};
}