From 8af9eab6ea93ebc38b80503b7267da3efcb78cb0 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 20:49:13 +0530 Subject: [PATCH 1/5] refactor: move font imports to compose canvas for syncing across reader and editor pages --- .../src/components/editor/ComposeCanvas.tsx | 650 +++++++++--------- frontend/src/pages/Editor.tsx | 10 +- 2 files changed, 335 insertions(+), 325 deletions(-) diff --git a/frontend/src/components/editor/ComposeCanvas.tsx b/frontend/src/components/editor/ComposeCanvas.tsx index 5a4a0b3..8075963 100644 --- a/frontend/src/components/editor/ComposeCanvas.tsx +++ b/frontend/src/components/editor/ComposeCanvas.tsx @@ -2,6 +2,12 @@ import * as fabric from "fabric"; import type * as React from "react"; import { useCallback, useEffect, useImperativeHandle, useRef } from "react"; +import "@fontsource/kavivanar/index.css"; +import "@fontsource/space-mono/index.css"; +import "@fontsource/cutive-mono/index.css"; +import "@fontsource/architects-daughter/index.css"; +import "@fontsource/redacted-script/index.css"; + const PAD = 36; const BASE_WIDTH = 680; const DEFAULT_LOGICAL_HEIGHT = 900; @@ -9,385 +15,393 @@ const DEFAULT_FONT_FAMILY = "Playfair Display Variable"; const DEFAULT_FONT_COLOR = "#000"; export interface FabricObjectJSON { - type: string; - name?: string; - top: number; - left: number; - width: number; - height: number; + type: string; + name?: string; + top: number; + left: number; + width: number; + height: number; - [key: string]: unknown; + [key: string]: unknown; } export interface FabricImageJSON extends FabricObjectJSON { - type: "Image"; - src: string; - _customRawFile?: File; + type: "Image"; + src: string; + _customRawFile?: File; } export interface CanvasJSON { - objects: (FabricObjectJSON | FabricImageJSON)[]; - canvasWidth?: number; - canvasHeight?: number; + objects: (FabricObjectJSON | FabricImageJSON)[]; + canvasWidth?: number; + canvasHeight?: number; } export interface CanvasStyle { - fontFamily: string; - fontColor: string; + fontFamily: string; + fontColor: string; } export type CanvasTools = { - addImage: (url: string, file: File) => void; - getData: () => CanvasJSON; - getImages: () => { src: string; file: File }[]; - loadData: (data: CanvasJSON) => Promise; - getStyle: () => CanvasStyle; + addImage: (url: string, file: File) => void; + getData: () => CanvasJSON; + getImages: () => { src: string; file: File }[]; + loadData: (data: CanvasJSON) => Promise; + getStyle: () => CanvasStyle; }; export interface FabricImageWithFile extends fabric.FabricImage { - _customRawFile: File; + _customRawFile: File; } // NOTE: We use the same canvasData to render on both mobile and desktop viewports. // Instead of calculating the entire objects pad again, we apply a zoom multiplier (scale down or up) // over the last saved canvas size. const applyResponsiveViewport = ( - canvas: fabric.Canvas, - wrapper: HTMLDivElement, - logicalWidth: number, - logicalHeight: number, + canvas: fabric.Canvas, + wrapper: HTMLDivElement, + logicalWidth: number, + logicalHeight: number, ) => { - const physicalWidth = wrapper.clientWidth || logicalWidth; - const zoomMultiplier = physicalWidth / logicalWidth; - const physicalHeight = Math.max(1, logicalHeight * zoomMultiplier); + const physicalWidth = wrapper.clientWidth || logicalWidth; + const zoomMultiplier = physicalWidth / logicalWidth; + const physicalHeight = Math.max(1, logicalHeight * zoomMultiplier); - canvas.setDimensions({ - width: physicalWidth, - height: physicalHeight, - }); + canvas.setDimensions({ + width: physicalWidth, + height: physicalHeight, + }); - wrapper.style.height = `${physicalHeight}px`; - canvas.setViewportTransform([zoomMultiplier, 0, 0, zoomMultiplier, 0, 0]); - canvas.requestRenderAll(); + wrapper.style.height = `${physicalHeight}px`; + canvas.setViewportTransform([zoomMultiplier, 0, 0, zoomMultiplier, 0, 0]); + canvas.requestRenderAll(); }; // to find the maximum height of the content to dynamically resize the canvas // would've been wayyy easier only if canvas supported fit-content like CSS property :) const measureLogicalContentHeight = ( - canvas: fabric.Canvas, - minimumHeight = DEFAULT_LOGICAL_HEIGHT, + canvas: fabric.Canvas, + minimumHeight = DEFAULT_LOGICAL_HEIGHT, ) => { - const maxBottom = canvas.getObjects().reduce((maxHeight, currObj) => { - const top = currObj.top; - const height = currObj.getScaledHeight(); - return Math.max(maxHeight, top + height); - }, 0); + const maxBottom = canvas.getObjects().reduce((maxHeight, currObj) => { + const top = currObj.top; + const height = currObj.getScaledHeight(); + return Math.max(maxHeight, top + height); + }, 0); - return Math.max(minimumHeight, maxBottom + PAD); + return Math.max(minimumHeight, maxBottom + PAD); }; const DEFAULT_INIT_TEXT = "Take a deep breath..."; interface ComposeCanvasProps { - readOnly?: boolean; - initialData?: CanvasJSON | null; - style?: CanvasStyle; - ref?: React.Ref; + readOnly?: boolean; + initialData?: CanvasJSON | null; + style?: CanvasStyle; + ref?: React.Ref; } export function ComposeCanvas({ - readOnly = false, - initialData = null, - style, - ref, + readOnly = false, + initialData = null, + style, + ref, }: ComposeCanvasProps) { - // wrapper is the parent div box - const wrapperRef = useRef(null); - const canvasRef = useRef(null); - const fabricRef = useRef(null); + // wrapper is the parent div box + const wrapperRef = useRef(null); + const canvasRef = useRef(null); + const fabricRef = useRef(null); - const textboxRef = useRef(null); - const deferredDataRef = useRef(null); - const logicalSizeRef = useRef({ - width: BASE_WIDTH, - height: DEFAULT_LOGICAL_HEIGHT, - }); - - // re-calculates height based on content and applies the zoom transform - const syncViewport = useCallback(() => { - if (!(fabricRef.current && wrapperRef.current)) return; - - const minHeight = initialData?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT; - logicalSizeRef.current.height = measureLogicalContentHeight( - fabricRef.current, - minHeight, - ); - - applyResponsiveViewport( - fabricRef.current, - wrapperRef.current, - logicalSizeRef.current.width, - logicalSizeRef.current.height, - ); - - fabricRef.current.requestRenderAll(); - }, [initialData]); - - // auto focus the cursor into the main textbox no matter the latest element added - const focusTextbox = useCallback( - (textbox: fabric.Textbox) => { - if (readOnly || !fabricRef.current) return; - - fabricRef.current.setActiveObject(textbox); - textbox.enterEditing(); - - // move the cursor to the end of the text - const textLength = textbox.text?.length ?? 0; - textbox.selectionStart = textLength; - textbox.selectionEnd = textLength; - - fabricRef.current.requestRenderAll(); - }, - [readOnly], - ); - - const loadContent = useCallback( - async (data: CanvasJSON | null) => { - const canvas = fabricRef.current; - const wrapper = wrapperRef.current; - if (!(canvas && wrapper)) return; - - // clean the canvas everytime and set fresh - canvas.clear(); - let textbox: fabric.Textbox | null = null; - - // restore logical size from prev saved data if available (in case of existing letter) - logicalSizeRef.current = { - width: data?.canvasWidth ?? BASE_WIDTH, - height: data?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT, - }; - - if (data?.objects?.length) { - await canvas.loadFromJSON(data); - textbox = canvas.getObjects("Textbox")[0] as fabric.Textbox; - } else { - // Create a fresh letter if no data exists - textbox = new fabric.Textbox(DEFAULT_INIT_TEXT, { - name: "main-textbox", - originX: "left", - originY: "top", - left: PAD, - top: PAD, - width: BASE_WIDTH - PAD * 2, - fontSize: 18, - fontWeight: 500, - fontFamily: DEFAULT_FONT_FAMILY, - fill: DEFAULT_FONT_COLOR, - lineHeight: 1.5, - // NOTE: splitByGrapheme is required for word wrap and re-low - // but fabric asks to disable this for clear font?? So we disable it for read view - splitByGrapheme: !readOnly, - lockMovementX: true, - lockMovementY: true, - lockScalingX: true, - lockScalingY: true, - lockRotation: true, - hasControls: false, - hasBorders: false, - objectCaching: false, - noScaleCache: false, - }); - canvas.add(textbox); - } - - if (!textbox) return; - - // readonly contraints applicable for post seal view - textbox.selectable = !readOnly; - textbox.evented = !readOnly; - textbox.editable = !readOnly; - textbox.hasBorders = false; - - textboxRef.current = textbox; - - // observe and auto-resize the canvas height whenever typed - textbox.on("changed", syncViewport); - - // trapping the focus into the textbox wherever clicked on canvas (except images) - canvas.on("mouse:down", (e) => { - if (!e.target || e.target === textbox) { - focusTextbox(textbox); - } - }); - - syncViewport(); - - // Hack: Fabric needs a small initial delay to mount before it will accept focus. - // otherwise it goes to the front - if (!readOnly) { - setTimeout(() => focusTextbox(textbox), 200); - } - }, - [readOnly, syncViewport, focusTextbox], - ); - - useEffect(() => { - if (style && textboxRef.current) { - const textBox = textboxRef.current; - textBox.fontFamily = style.fontFamily || textBox.fontFamily; - textBox.fill = style.fontColor || textBox.fill; - syncViewport(); - } - }, [style, syncViewport]); - - useEffect(() => { - let isMounted = true; - let resizeObserver: ResizeObserver | null = null; - let lastWidth = 0; - - const initCanvas = async () => { - // HACK: actual font may change the text-width - small ux improvement - await document.fonts.ready; - - if (!(wrapperRef.current && canvasRef.current && isMounted)) return; - - let width = wrapperRef.current.clientWidth; - if (width === 0) { - await new Promise((resolve) => requestAnimationFrame(resolve)); - width = wrapperRef.current?.clientWidth || BASE_WIDTH; - } - - // init the fabric instance - const canvas = new fabric.Canvas(canvasRef.current, { - width, + const textboxRef = useRef(null); + const deferredDataRef = useRef(null); + const logicalSizeRef = useRef({ + width: BASE_WIDTH, height: DEFAULT_LOGICAL_HEIGHT, - selection: !readOnly, - preserveObjectStacking: true, - allowTouchScrolling: true, - enableRetinaScaling: true, - objectCaching: false, - }); + }); - // remove default fabric background to let our CSS show through - // TODO: provision custom bg (color in scope, but how does img fit?) - const wrapperEl = canvas.getElement().parentElement; - if (wrapperEl) wrapperEl.style.background = "transparent"; + // re-calculates height based on content and applies the zoom transform + const syncViewport = useCallback(() => { + if (!(fabricRef.current && wrapperRef.current)) return; - fabricRef.current = canvas; + const minHeight = initialData?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT; + logicalSizeRef.current.height = measureLogicalContentHeight( + fabricRef.current, + minHeight, + ); - await loadContent(initialData); + applyResponsiveViewport( + fabricRef.current, + wrapperRef.current, + logicalSizeRef.current.width, + logicalSizeRef.current.height, + ); - // sometimes loadData() may be called before the canvas finished the init render - // so we retry that stashed render right after the init - if (deferredDataRef.current) { - await loadContent(deferredDataRef.current); - deferredDataRef.current = null; - } + fabricRef.current.requestRenderAll(); + }, [initialData]); - // auto window resizing based width - lastWidth = wrapperRef.current.clientWidth; - resizeObserver = new ResizeObserver(() => { - const nextWidth = wrapperRef.current?.clientWidth; - if (!nextWidth || nextWidth === lastWidth) return; - lastWidth = nextWidth; - syncViewport(); - }); - resizeObserver.observe(wrapperRef.current!); - }; + // auto focus the cursor into the main textbox no matter the latest element added + const focusTextbox = useCallback( + (textbox: fabric.Textbox) => { + if (readOnly || !fabricRef.current) return; - initCanvas().then(); + fabricRef.current.setActiveObject(textbox); + textbox.enterEditing(); - return () => { - isMounted = false; - resizeObserver?.disconnect(); - fabricRef.current?.dispose(); - fabricRef.current = null; - textboxRef.current = null; - }; - }, [initialData, loadContent, readOnly, syncViewport]); + // move the cursor to the end of the text + const textLength = textbox.text?.length ?? 0; + textbox.selectionStart = textLength; + textbox.selectionEnd = textLength; - // WHY?: fabric doesn't work like react with state and props based optimized re-renders. - // everytime we there's a change in the data, we should force the render, - // so we let the parent Editor component take control of this. - useImperativeHandle(ref, () => ({ - addImage: (url: string, file: File) => { - if (!fabricRef.current) return; + fabricRef.current.requestRenderAll(); + }, + [readOnly], + ); - fabric.FabricImage.fromURL(url).then((img) => { - img.scaleToWidth(Math.min(300, img.width)); - img.set({ - originX: "left", - originY: "top", - left: PAD, - top: PAD, - noScaleCache: false, - objectCaching: false, - // WHY?: after image object clean-up, its src becomes local blob:// - // but browser won't let us parse this blob:// into file afterwards. so we hold a local copy - _customRawFile: file, - } as Partial); + const loadContent = useCallback( + async (data: CanvasJSON | null) => { + const canvas = fabricRef.current; + const wrapper = wrapperRef.current; + if (!(canvas && wrapper)) return; - fabricRef.current?.add(img); - fabricRef.current?.setActiveObject(img); + // clean the canvas everytime and set fresh + canvas.clear(); + let textbox: fabric.Textbox | null = null; - syncViewport(); - // clean up memory - URL.revokeObjectURL(url); - }); - }, + // restore logical size from prev saved data if available (in case of existing letter) + logicalSizeRef.current = { + width: data?.canvasWidth ?? BASE_WIDTH, + height: data?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT, + }; - getData: () => { - if (!fabricRef.current) return { objects: [] }; - syncViewport(); + if (data?.objects?.length) { + await canvas.loadFromJSON(data); + textbox = canvas.getObjects("Textbox")[0] as fabric.Textbox; + } else { + // Create a fresh letter if no data exists + textbox = new fabric.Textbox(DEFAULT_INIT_TEXT, { + name: "main-textbox", + originX: "left", + originY: "top", + left: PAD, + top: PAD, + width: BASE_WIDTH - PAD * 2, + fontSize: 18, + fontWeight: 500, + fontFamily: DEFAULT_FONT_FAMILY, + fill: DEFAULT_FONT_COLOR, + lineHeight: 1.5, + splitByGrapheme: false, + lockMovementX: true, + lockMovementY: true, + lockScalingX: true, + lockScalingY: true, + lockRotation: true, + hasControls: false, + hasBorders: false, + objectCaching: false, + noScaleCache: false, + }); + canvas.add(textbox); + } - const json = fabricRef.current.toJSON() as CanvasJSON; - json.canvasWidth = logicalSizeRef.current.width; - json.canvasHeight = logicalSizeRef.current.height; - return json; - }, + if (!textbox) return; - getImages: () => { - if (!fabricRef.current) return []; - const images = fabricRef.current.getObjects( - "Image", - ) as FabricImageWithFile[]; - return images.map((img) => ({ - src: img.getSrc(), - file: img._customRawFile, - })); - }, + // readonly contraints applicable for post seal view + textbox.selectable = !readOnly; + textbox.evented = !readOnly; + textbox.editable = !readOnly; + textbox.hasBorders = false; - loadData: async (data: CanvasJSON) => { - // if canvas isn't ready yet, stash the data and let the useEffect pick it up - if (!fabricRef.current) { - deferredDataRef.current = data; - return; - } - await loadContent(data); - }, + textboxRef.current = textbox; - getStyle: () => { - const textBox = textboxRef.current; + // observe and auto-resize the canvas height whenever typed + textbox.on("changed", syncViewport); - return { - fontFamily: textBox?.fontFamily || DEFAULT_FONT_FAMILY, - fontColor: (textBox?.fill as string) || DEFAULT_FONT_COLOR, - }; - }, - })); + // trapping the focus into the textbox wherever clicked on canvas (except images) + canvas.on("mouse:down", (e) => { + if (!e.target || e.target === textbox) { + focusTextbox(textbox); + } + }); - return ( -
- -
- ); + for (const img of canvas.getObjects("Image")) { + img.set({ + hasControls: !readOnly, + hasBorders: !readOnly, + }); + } + + // NOTE: fabric refreshes fonts once the textbox is rendered after initial focus + await document.fonts.ready; + textbox.set("dirty", true); + syncViewport(); + + // Hack: Fabric needs a small initial delay to mount before it will accept focus. + // otherwise it goes to the front + if (!readOnly) { + setTimeout(() => focusTextbox(textbox), 200); + } + }, + [readOnly, syncViewport, focusTextbox], + ); + + useEffect(() => { + if (style && textboxRef.current) { + const textBox = textboxRef.current; + textBox.fontFamily = style.fontFamily || textBox.fontFamily; + textBox.fill = style.fontColor || textBox.fill; + syncViewport(); + } + }, [style, syncViewport]); + + useEffect(() => { + let isMounted = true; + let resizeObserver: ResizeObserver | null = null; + let lastWidth = 0; + + const initCanvas = async () => { + // HACK: actual font may change the text-width - small ux improvement + await document.fonts.ready; + + if (!(wrapperRef.current && canvasRef.current && isMounted)) return; + + let width = wrapperRef.current.clientWidth; + if (width === 0) { + await new Promise((resolve) => requestAnimationFrame(resolve)); + width = wrapperRef.current?.clientWidth || BASE_WIDTH; + } + + // init the fabric instance + const canvas = new fabric.Canvas(canvasRef.current, { + width, + height: DEFAULT_LOGICAL_HEIGHT, + selection: !readOnly, + preserveObjectStacking: true, + allowTouchScrolling: true, + enableRetinaScaling: true, + objectCaching: false, + }); + + // remove default fabric background to let our CSS show through + // TODO: provision custom bg (color in scope, but how does img fit?) + const wrapperEl = canvas.getElement().parentElement; + if (wrapperEl) wrapperEl.style.background = "transparent"; + + fabricRef.current = canvas; + + await loadContent(initialData); + + // sometimes loadData() may be called before the canvas finished the init render + // so we retry that stashed render right after the init + if (deferredDataRef.current) { + await loadContent(deferredDataRef.current); + deferredDataRef.current = null; + } + + // auto window resizing based width + lastWidth = wrapperRef.current.clientWidth; + resizeObserver = new ResizeObserver(() => { + const nextWidth = wrapperRef.current?.clientWidth; + if (!nextWidth || nextWidth === lastWidth) return; + lastWidth = nextWidth; + syncViewport(); + }); + resizeObserver.observe(wrapperRef.current!); + }; + + initCanvas().then(); + + return () => { + isMounted = false; + resizeObserver?.disconnect(); + fabricRef.current?.dispose(); + fabricRef.current = null; + textboxRef.current = null; + }; + }, [initialData, loadContent, readOnly, syncViewport]); + + // WHY?: fabric doesn't work like react with state and props based optimized re-renders. + // everytime we there's a change in the data, we should force the render, + // so we let the parent Editor component take control of this. + useImperativeHandle(ref, () => ({ + addImage: (url: string, file: File) => { + if (!fabricRef.current) return; + + fabric.FabricImage.fromURL(url).then((img) => { + img.scaleToWidth(Math.min(300, img.width)); + img.set({ + originX: "left", + originY: "top", + left: PAD, + top: PAD, + noScaleCache: false, + objectCaching: false, + // WHY?: after image object clean-up, its src becomes local blob:// + // but browser won't let us parse this blob:// into file afterwards. so we hold a local copy + _customRawFile: file, + } as Partial); + + fabricRef.current?.add(img); + fabricRef.current?.setActiveObject(img); + + syncViewport(); + // clean up memory + URL.revokeObjectURL(url); + }); + }, + + getData: () => { + if (!fabricRef.current) return { objects: [] }; + syncViewport(); + + const json = fabricRef.current.toJSON() as CanvasJSON; + json.canvasWidth = logicalSizeRef.current.width; + json.canvasHeight = logicalSizeRef.current.height; + return json; + }, + + getImages: () => { + if (!fabricRef.current) return []; + const images = fabricRef.current.getObjects( + "Image", + ) as FabricImageWithFile[]; + return images.map((img) => ({ + src: img.getSrc(), + file: img._customRawFile, + })); + }, + + loadData: async (data: CanvasJSON) => { + // if canvas isn't ready yet, stash the data and let the useEffect pick it up + if (!fabricRef.current) { + deferredDataRef.current = data; + return; + } + await loadContent(data); + }, + + getStyle: () => { + const textBox = textboxRef.current; + + return { + fontFamily: textBox?.fontFamily || DEFAULT_FONT_FAMILY, + fontColor: (textBox?.fill as string) || DEFAULT_FONT_COLOR, + }; + }, + })); + + return ( +
+ +
+ ); } ComposeCanvas.displayName = "ComposeCanvas"; diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 57a29b8..ce96e05 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -34,12 +34,6 @@ import { CryptoUtils } from "../utils/crypto"; import { formatRelativeDate } from "../utils/dateFormat"; import { decryptCanvasImages, encryptCanvasImages } from "../utils/letterLogic"; -import "@fontsource/kavivanar/index.css"; -import "@fontsource/space-mono/index.css"; -import "@fontsource/cutive-mono/index.css"; -import "@fontsource/architects-daughter/index.css"; -import "@fontsource/redacted-script/index.css"; - type SaveOverlay = "IDLE" | "SAVING" | "SAVED" | "ERROR"; const OVERLAY_FADE_MS = 250; @@ -268,7 +262,9 @@ export default function Editor() { await cryptoUtils.initialize(); try { - const canvasData = canvasRef.current?.getData() || { objects: [] }; + const canvasData = (await canvasRef.current?.getData()) || { + objects: [], + }; const canvasImages = canvasRef.current?.getImages() || []; const { encryptedImageFiles, encryptedCanvasData } = -- 2.52.0 From 0104703852dbea50ecdb905f9c8519a2907cded1 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 21:17:14 +0530 Subject: [PATCH 2/5] feat: add onboarding welcome letter overlay --- .../drawer/WelcomeLetterOverlay.tsx | 77 +++++++++++++ frontend/src/config/welcomeLetter.ts | 101 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 frontend/src/components/drawer/WelcomeLetterOverlay.tsx create mode 100644 frontend/src/config/welcomeLetter.ts diff --git a/frontend/src/components/drawer/WelcomeLetterOverlay.tsx b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx new file mode 100644 index 0000000..a45d2b2 --- /dev/null +++ b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx @@ -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(null); + + useEffect(() => { + if (revealState === "REVEALED" && canvasRef.current) { + const welcomeContent = getWelcomeContent(userName); + canvasRef.current.loadData(welcomeContent); + } + }, [revealState, userName]); + + return ( +
+
+ +
+ + {revealState === "SEALED" && ( + + setRevealState("REVEALED")} + ignite={false} + /> + + )} + +
+
+
+ +
+ +
+ +
+
+
+
+ ); +} diff --git a/frontend/src/config/welcomeLetter.ts b/frontend/src/config/welcomeLetter.ts new file mode 100644 index 0000000..f35ed58 --- /dev/null +++ b/frontend/src/config/welcomeLetter.ts @@ -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, + }; +} -- 2.52.0 From 9800174df1df94bedee412483c4b481a7da9eb3b Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 21:20:39 +0530 Subject: [PATCH 3/5] feat: show welcome letter overlay for first-time users in Drawer page --- frontend/public/screenshots/train.png | Bin 0 -> 132169 bytes .../drawer/WelcomeLetterOverlay.tsx | 4 +- frontend/src/pages/Drawer.test.tsx | 162 +++++++++++------- frontend/src/pages/Drawer.tsx | 31 +++- 4 files changed, 127 insertions(+), 70 deletions(-) create mode 100644 frontend/public/screenshots/train.png diff --git a/frontend/public/screenshots/train.png b/frontend/public/screenshots/train.png new file mode 100644 index 0000000000000000000000000000000000000000..9e605829f06f12c07d6a42d2b8b02251ee017d1e GIT binary patch literal 132169 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/drawer/WelcomeLetterOverlay.tsx b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx index a45d2b2..5b59420 100644 --- a/frontend/src/components/drawer/WelcomeLetterOverlay.tsx +++ b/frontend/src/components/drawer/WelcomeLetterOverlay.tsx @@ -1,6 +1,6 @@ import { AnimatePresence, motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; -import { getWelcomeContent } from "../../config/welcomeLetter"; +import { getWelcomeLetterContent } from "../../config/welcomeLetter"; import { formatDate } from "../../utils/dateFormat"; import { type CanvasTools, ComposeCanvas } from "../editor/ComposeCanvas"; import { EnvelopeReveal } from "../reader/EnvelopeReveal"; @@ -21,7 +21,7 @@ export function WelcomeLetterOverlay({ useEffect(() => { if (revealState === "REVEALED" && canvasRef.current) { - const welcomeContent = getWelcomeContent(userName); + const welcomeContent = getWelcomeLetterContent(userName); canvasRef.current.loadData(welcomeContent); } }, [revealState, userName]); diff --git a/frontend/src/pages/Drawer.test.tsx b/frontend/src/pages/Drawer.test.tsx index 05feade..206ec94 100644 --- a/frontend/src/pages/Drawer.test.tsx +++ b/frontend/src/pages/Drawer.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { mockUser } from "../../test/fixtures/user.fixture"; @@ -7,77 +7,117 @@ import { useAuthStore } from "../store/useAuthStore"; import Drawer from "./Drawer"; vi.mock("../hooks/useLetters"); +vi.mock("../components/drawer/WelcomeLetterOverlay", () => ({ + WelcomeLetterOverlay: ({ onComplete }: any) => ( +

+ ), +})); describe("Drawer Page", () => { - beforeEach(() => { - // Setup authenticated state for the test - useAuthStore.setState({ - user: mockUser, - accessToken: "fake-token", - isInitializing: false, - }); - - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: false, - isAuthRequired: false, - }); + beforeEach(() => { + // Setup authenticated state for the test + useAuthStore.setState({ + user: mockUser, + accessToken: "fake-token", + isInitializing: false, }); - it("renders the cabinet sections and empty state message", () => { - render( - - - , - ); + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: false, + }); + }); - expect(screen.getByText(/Drafts/i)).toBeInTheDocument(); - expect(screen.getAllByText(/Kept/i).length).toBeGreaterThanOrEqual(1); - expect(screen.getByText(/Vault/i)).toBeInTheDocument(); - expect(screen.getByText(/This drawer remains silent/i)).toBeInTheDocument(); + it("renders the cabinet sections and empty state message", () => { + render( + + + , + ); + + expect(screen.getByText(/Drafts/i)).toBeInTheDocument(); + expect(screen.getAllByText(/Kept/i).length).toBeGreaterThanOrEqual(1); + expect(screen.getByText(/Vault/i)).toBeInTheDocument(); + expect(screen.getByText(/This drawer remains silent/i)).toBeInTheDocument(); + }); + + it("renders the loading state", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: true, + isAuthRequired: false, }); - it("renders the loading state", () => { - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: true, - isAuthRequired: false, - }); + render( + + + , + ); - render( - - - , - ); + expect(screen.getByText(/Opening your cabinet/i)).toBeInTheDocument(); + }); - expect(screen.getByText(/Opening your cabinet/i)).toBeInTheDocument(); + it("renders the authentication required modal when api requires auth", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: true, }); - it("renders the authentication required modal when api requires auth", () => { - vi.mocked(useLetters).mockReturnValue({ - drafts: [], - kept: [], - sent: [], - vault: [], - loading: false, - isAuthRequired: true, - }); + render( + + + , + ); - render( - - - , - ); + expect(screen.getByText(/You've been away a while./i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); + }); - expect( - screen.getByText(/You've been away a while./i), - ).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); - }); + it("renders the welcome letter when firstTime state is present", () => { + render( + + + , + ); + + expect(screen.getByTestId("welcome-letter-overlay")).toBeInTheDocument(); + }); + + it("renders the drawer content when the letter is closed", () => { + render( + + + , + ); + + const completeButton = screen.getByTestId("overlay-exit-button"); + fireEvent.click(completeButton); + + expect( + screen.queryByTestId("welcome-letter-overlay"), + ).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx index 5fe14f4..b21f941 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -1,9 +1,10 @@ import { FeatherIcon } from "@phosphor-icons/react"; import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { DrawerSection } from "../components/drawer/DrawerSection.tsx"; import { LetterItem } from "../components/drawer/LetterItem.tsx"; import { PasskeyModal } from "../components/drawer/PasskeyModal.tsx"; +import { WelcomeLetterOverlay } from "../components/drawer/WelcomeLetterOverlay.tsx"; import Logo from "../components/Logo"; import Saajan from "../components/ui/Saajan.tsx"; import { PATHS } from "../config/routes"; @@ -19,6 +20,10 @@ export default function Drawer() { const [openSection, setOpenSection] = useState(null); const navigate = useNavigate(); + const location = useLocation(); + const [showWelcomeLetter, setShowWelcomeLetter] = useState( + !!location.state?.firstTime, + ); const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters(); if (!user) return null; @@ -30,6 +35,16 @@ export default function Drawer() {
+ {showWelcomeLetter && ( + { + setShowWelcomeLetter(false); + navigate(location.pathname, { replace: true, state: {} }); + }} + /> + )} + {isAuthRequired && }
@@ -166,12 +181,14 @@ export default function Drawer() {
For your unsaid.
-
- -
+ {!showWelcomeLetter && ( +
+ +
+ )}
); } -- 2.52.0 From e532507a0f69bc6a4f1d6c5cc62cda5c26321914 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 21:21:28 +0530 Subject: [PATCH 4/5] feat: route location state to drawer from login --- frontend/src/App.tsx | 2 +- .../src/components/editor/ComposeCanvas.tsx | 630 +++++++++--------- .../src/components/login/WelcomeModal.tsx | 9 +- frontend/src/pages/Login.tsx | 4 +- 4 files changed, 322 insertions(+), 323 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c54c4e9..4db6ed3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,7 +31,7 @@ export default function App() { return ( -
+
}> } /> diff --git a/frontend/src/components/editor/ComposeCanvas.tsx b/frontend/src/components/editor/ComposeCanvas.tsx index 8075963..6c97b1e 100644 --- a/frontend/src/components/editor/ComposeCanvas.tsx +++ b/frontend/src/components/editor/ComposeCanvas.tsx @@ -15,393 +15,393 @@ const DEFAULT_FONT_FAMILY = "Playfair Display Variable"; const DEFAULT_FONT_COLOR = "#000"; export interface FabricObjectJSON { - type: string; - name?: string; - top: number; - left: number; - width: number; - height: number; + type: string; + name?: string; + top: number; + left: number; + width: number; + height: number; - [key: string]: unknown; + [key: string]: unknown; } export interface FabricImageJSON extends FabricObjectJSON { - type: "Image"; - src: string; - _customRawFile?: File; + type: "Image"; + src: string; + _customRawFile?: File; } export interface CanvasJSON { - objects: (FabricObjectJSON | FabricImageJSON)[]; - canvasWidth?: number; - canvasHeight?: number; + objects: (FabricObjectJSON | FabricImageJSON)[]; + canvasWidth?: number; + canvasHeight?: number; } export interface CanvasStyle { - fontFamily: string; - fontColor: string; + fontFamily: string; + fontColor: string; } export type CanvasTools = { - addImage: (url: string, file: File) => void; - getData: () => CanvasJSON; - getImages: () => { src: string; file: File }[]; - loadData: (data: CanvasJSON) => Promise; - getStyle: () => CanvasStyle; + addImage: (url: string, file: File) => void; + getData: () => CanvasJSON; + getImages: () => { src: string; file: File }[]; + loadData: (data: CanvasJSON) => Promise; + getStyle: () => CanvasStyle; }; export interface FabricImageWithFile extends fabric.FabricImage { - _customRawFile: File; + _customRawFile: File; } // NOTE: We use the same canvasData to render on both mobile and desktop viewports. // Instead of calculating the entire objects pad again, we apply a zoom multiplier (scale down or up) // over the last saved canvas size. const applyResponsiveViewport = ( - canvas: fabric.Canvas, - wrapper: HTMLDivElement, - logicalWidth: number, - logicalHeight: number, + canvas: fabric.Canvas, + wrapper: HTMLDivElement, + logicalWidth: number, + logicalHeight: number, ) => { - const physicalWidth = wrapper.clientWidth || logicalWidth; - const zoomMultiplier = physicalWidth / logicalWidth; - const physicalHeight = Math.max(1, logicalHeight * zoomMultiplier); + const physicalWidth = wrapper.clientWidth || logicalWidth; + const zoomMultiplier = physicalWidth / logicalWidth; + const physicalHeight = Math.max(1, logicalHeight * zoomMultiplier); - canvas.setDimensions({ - width: physicalWidth, - height: physicalHeight, - }); + canvas.setDimensions({ + width: physicalWidth, + height: physicalHeight, + }); - wrapper.style.height = `${physicalHeight}px`; - canvas.setViewportTransform([zoomMultiplier, 0, 0, zoomMultiplier, 0, 0]); - canvas.requestRenderAll(); + wrapper.style.height = `${physicalHeight}px`; + canvas.setViewportTransform([zoomMultiplier, 0, 0, zoomMultiplier, 0, 0]); + canvas.requestRenderAll(); }; // to find the maximum height of the content to dynamically resize the canvas // would've been wayyy easier only if canvas supported fit-content like CSS property :) const measureLogicalContentHeight = ( - canvas: fabric.Canvas, - minimumHeight = DEFAULT_LOGICAL_HEIGHT, + canvas: fabric.Canvas, + minimumHeight = DEFAULT_LOGICAL_HEIGHT, ) => { - const maxBottom = canvas.getObjects().reduce((maxHeight, currObj) => { - const top = currObj.top; - const height = currObj.getScaledHeight(); - return Math.max(maxHeight, top + height); - }, 0); + const maxBottom = canvas.getObjects().reduce((maxHeight, currObj) => { + const top = currObj.top; + const height = currObj.getScaledHeight(); + return Math.max(maxHeight, top + height); + }, 0); - return Math.max(minimumHeight, maxBottom + PAD); + return Math.max(minimumHeight, maxBottom + PAD); }; const DEFAULT_INIT_TEXT = "Take a deep breath..."; interface ComposeCanvasProps { - readOnly?: boolean; - initialData?: CanvasJSON | null; - style?: CanvasStyle; - ref?: React.Ref; + readOnly?: boolean; + initialData?: CanvasJSON | null; + style?: CanvasStyle; + ref?: React.Ref; } export function ComposeCanvas({ - readOnly = false, - initialData = null, - style, - ref, + readOnly = false, + initialData = null, + style, + ref, }: ComposeCanvasProps) { - // wrapper is the parent div box - const wrapperRef = useRef(null); - const canvasRef = useRef(null); - const fabricRef = useRef(null); + // wrapper is the parent div box + const wrapperRef = useRef(null); + const canvasRef = useRef(null); + const fabricRef = useRef(null); - const textboxRef = useRef(null); - const deferredDataRef = useRef(null); - const logicalSizeRef = useRef({ - width: BASE_WIDTH, - height: DEFAULT_LOGICAL_HEIGHT, - }); + const textboxRef = useRef(null); + const deferredDataRef = useRef(null); + const logicalSizeRef = useRef({ + width: BASE_WIDTH, + height: DEFAULT_LOGICAL_HEIGHT, + }); - // re-calculates height based on content and applies the zoom transform - const syncViewport = useCallback(() => { - if (!(fabricRef.current && wrapperRef.current)) return; + // re-calculates height based on content and applies the zoom transform + const syncViewport = useCallback(() => { + if (!(fabricRef.current && wrapperRef.current)) return; - const minHeight = initialData?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT; - logicalSizeRef.current.height = measureLogicalContentHeight( - fabricRef.current, - minHeight, - ); - - applyResponsiveViewport( - fabricRef.current, - wrapperRef.current, - logicalSizeRef.current.width, - logicalSizeRef.current.height, - ); - - fabricRef.current.requestRenderAll(); - }, [initialData]); - - // auto focus the cursor into the main textbox no matter the latest element added - const focusTextbox = useCallback( - (textbox: fabric.Textbox) => { - if (readOnly || !fabricRef.current) return; - - fabricRef.current.setActiveObject(textbox); - textbox.enterEditing(); - - // move the cursor to the end of the text - const textLength = textbox.text?.length ?? 0; - textbox.selectionStart = textLength; - textbox.selectionEnd = textLength; - - fabricRef.current.requestRenderAll(); - }, - [readOnly], + const minHeight = initialData?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT; + logicalSizeRef.current.height = measureLogicalContentHeight( + fabricRef.current, + minHeight, ); - const loadContent = useCallback( - async (data: CanvasJSON | null) => { - const canvas = fabricRef.current; - const wrapper = wrapperRef.current; - if (!(canvas && wrapper)) return; - - // clean the canvas everytime and set fresh - canvas.clear(); - let textbox: fabric.Textbox | null = null; - - // restore logical size from prev saved data if available (in case of existing letter) - logicalSizeRef.current = { - width: data?.canvasWidth ?? BASE_WIDTH, - height: data?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT, - }; - - if (data?.objects?.length) { - await canvas.loadFromJSON(data); - textbox = canvas.getObjects("Textbox")[0] as fabric.Textbox; - } else { - // Create a fresh letter if no data exists - textbox = new fabric.Textbox(DEFAULT_INIT_TEXT, { - name: "main-textbox", - originX: "left", - originY: "top", - left: PAD, - top: PAD, - width: BASE_WIDTH - PAD * 2, - fontSize: 18, - fontWeight: 500, - fontFamily: DEFAULT_FONT_FAMILY, - fill: DEFAULT_FONT_COLOR, - lineHeight: 1.5, - splitByGrapheme: false, - lockMovementX: true, - lockMovementY: true, - lockScalingX: true, - lockScalingY: true, - lockRotation: true, - hasControls: false, - hasBorders: false, - objectCaching: false, - noScaleCache: false, - }); - canvas.add(textbox); - } - - if (!textbox) return; - - // readonly contraints applicable for post seal view - textbox.selectable = !readOnly; - textbox.evented = !readOnly; - textbox.editable = !readOnly; - textbox.hasBorders = false; - - textboxRef.current = textbox; - - // observe and auto-resize the canvas height whenever typed - textbox.on("changed", syncViewport); - - // trapping the focus into the textbox wherever clicked on canvas (except images) - canvas.on("mouse:down", (e) => { - if (!e.target || e.target === textbox) { - focusTextbox(textbox); - } - }); - - for (const img of canvas.getObjects("Image")) { - img.set({ - hasControls: !readOnly, - hasBorders: !readOnly, - }); - } - - // NOTE: fabric refreshes fonts once the textbox is rendered after initial focus - await document.fonts.ready; - textbox.set("dirty", true); - syncViewport(); - - // Hack: Fabric needs a small initial delay to mount before it will accept focus. - // otherwise it goes to the front - if (!readOnly) { - setTimeout(() => focusTextbox(textbox), 200); - } - }, - [readOnly, syncViewport, focusTextbox], + applyResponsiveViewport( + fabricRef.current, + wrapperRef.current, + logicalSizeRef.current.width, + logicalSizeRef.current.height, ); - useEffect(() => { - if (style && textboxRef.current) { - const textBox = textboxRef.current; - textBox.fontFamily = style.fontFamily || textBox.fontFamily; - textBox.fill = style.fontColor || textBox.fill; - syncViewport(); + fabricRef.current.requestRenderAll(); + }, [initialData]); + + // auto focus the cursor into the main textbox no matter the latest element added + const focusTextbox = useCallback( + (textbox: fabric.Textbox) => { + if (readOnly || !fabricRef.current) return; + + fabricRef.current.setActiveObject(textbox); + textbox.enterEditing(); + + // move the cursor to the end of the text + const textLength = textbox.text?.length ?? 0; + textbox.selectionStart = textLength; + textbox.selectionEnd = textLength; + + fabricRef.current.requestRenderAll(); + }, + [readOnly], + ); + + const loadContent = useCallback( + async (data: CanvasJSON | null) => { + const canvas = fabricRef.current; + const wrapper = wrapperRef.current; + if (!(canvas && wrapper)) return; + + // clean the canvas everytime and set fresh + canvas.clear(); + let textbox: fabric.Textbox | null = null; + + // restore logical size from prev saved data if available (in case of existing letter) + logicalSizeRef.current = { + width: data?.canvasWidth ?? BASE_WIDTH, + height: data?.canvasHeight ?? DEFAULT_LOGICAL_HEIGHT, + }; + + if (data?.objects?.length) { + await canvas.loadFromJSON(data); + textbox = canvas.getObjects("Textbox")[0] as fabric.Textbox; + } else { + // Create a fresh letter if no data exists + textbox = new fabric.Textbox(DEFAULT_INIT_TEXT, { + name: "main-textbox", + originX: "left", + originY: "top", + left: PAD, + top: PAD, + width: BASE_WIDTH - PAD * 2, + fontSize: 18, + fontWeight: 500, + fontFamily: DEFAULT_FONT_FAMILY, + fill: DEFAULT_FONT_COLOR, + lineHeight: 1.5, + splitByGrapheme: false, + lockMovementX: true, + lockMovementY: true, + lockScalingX: true, + lockScalingY: true, + lockRotation: true, + hasControls: false, + hasBorders: false, + objectCaching: false, + noScaleCache: false, + }); + canvas.add(textbox); + } + + if (!textbox) return; + + // readonly contraints applicable for post seal view + textbox.selectable = !readOnly; + textbox.evented = !readOnly; + textbox.editable = !readOnly; + textbox.hasBorders = false; + + textboxRef.current = textbox; + + // observe and auto-resize the canvas height whenever typed + textbox.on("changed", syncViewport); + + // trapping the focus into the textbox wherever clicked on canvas (except images) + canvas.on("mouse:down", (e) => { + if (!e.target || e.target === textbox) { + focusTextbox(textbox); } - }, [style, syncViewport]); + }); - useEffect(() => { - let isMounted = true; - let resizeObserver: ResizeObserver | null = null; - let lastWidth = 0; + for (const img of canvas.getObjects("Image")) { + img.set({ + hasControls: !readOnly, + hasBorders: !readOnly, + }); + } - const initCanvas = async () => { - // HACK: actual font may change the text-width - small ux improvement - await document.fonts.ready; + // NOTE: fabric refreshes fonts once the textbox is rendered after initial focus + await document.fonts.ready; + textbox.set("dirty", true); + syncViewport(); - if (!(wrapperRef.current && canvasRef.current && isMounted)) return; + // Hack: Fabric needs a small initial delay to mount before it will accept focus. + // otherwise it goes to the front + if (!readOnly) { + setTimeout(() => focusTextbox(textbox), 200); + } + }, + [readOnly, syncViewport, focusTextbox], + ); - let width = wrapperRef.current.clientWidth; - if (width === 0) { - await new Promise((resolve) => requestAnimationFrame(resolve)); - width = wrapperRef.current?.clientWidth || BASE_WIDTH; - } + useEffect(() => { + if (style && textboxRef.current) { + const textBox = textboxRef.current; + textBox.fontFamily = style.fontFamily || textBox.fontFamily; + textBox.fill = style.fontColor || textBox.fill; + syncViewport(); + } + }, [style, syncViewport]); - // init the fabric instance - const canvas = new fabric.Canvas(canvasRef.current, { - width, - height: DEFAULT_LOGICAL_HEIGHT, - selection: !readOnly, - preserveObjectStacking: true, - allowTouchScrolling: true, - enableRetinaScaling: true, - objectCaching: false, - }); + useEffect(() => { + let isMounted = true; + let resizeObserver: ResizeObserver | null = null; + let lastWidth = 0; - // remove default fabric background to let our CSS show through - // TODO: provision custom bg (color in scope, but how does img fit?) - const wrapperEl = canvas.getElement().parentElement; - if (wrapperEl) wrapperEl.style.background = "transparent"; + const initCanvas = async () => { + // HACK: actual font may change the text-width - small ux improvement + await document.fonts.ready; - fabricRef.current = canvas; + if (!(wrapperRef.current && canvasRef.current && isMounted)) return; - await loadContent(initialData); + let width = wrapperRef.current.clientWidth; + if (width === 0) { + await new Promise((resolve) => requestAnimationFrame(resolve)); + width = wrapperRef.current?.clientWidth || BASE_WIDTH; + } - // sometimes loadData() may be called before the canvas finished the init render - // so we retry that stashed render right after the init - if (deferredDataRef.current) { - await loadContent(deferredDataRef.current); - deferredDataRef.current = null; - } + // init the fabric instance + const canvas = new fabric.Canvas(canvasRef.current, { + width, + height: DEFAULT_LOGICAL_HEIGHT, + selection: !readOnly, + preserveObjectStacking: true, + allowTouchScrolling: true, + enableRetinaScaling: true, + objectCaching: false, + }); - // auto window resizing based width - lastWidth = wrapperRef.current.clientWidth; - resizeObserver = new ResizeObserver(() => { - const nextWidth = wrapperRef.current?.clientWidth; - if (!nextWidth || nextWidth === lastWidth) return; - lastWidth = nextWidth; - syncViewport(); - }); - resizeObserver.observe(wrapperRef.current!); - }; + // remove default fabric background to let our CSS show through + // TODO: provision custom bg (color in scope, but how does img fit?) + const wrapperEl = canvas.getElement().parentElement; + if (wrapperEl) wrapperEl.style.background = "transparent"; - initCanvas().then(); + fabricRef.current = canvas; - return () => { - isMounted = false; - resizeObserver?.disconnect(); - fabricRef.current?.dispose(); - fabricRef.current = null; - textboxRef.current = null; - }; - }, [initialData, loadContent, readOnly, syncViewport]); + await loadContent(initialData); - // WHY?: fabric doesn't work like react with state and props based optimized re-renders. - // everytime we there's a change in the data, we should force the render, - // so we let the parent Editor component take control of this. - useImperativeHandle(ref, () => ({ - addImage: (url: string, file: File) => { - if (!fabricRef.current) return; + // sometimes loadData() may be called before the canvas finished the init render + // so we retry that stashed render right after the init + if (deferredDataRef.current) { + await loadContent(deferredDataRef.current); + deferredDataRef.current = null; + } - fabric.FabricImage.fromURL(url).then((img) => { - img.scaleToWidth(Math.min(300, img.width)); - img.set({ - originX: "left", - originY: "top", - left: PAD, - top: PAD, - noScaleCache: false, - objectCaching: false, - // WHY?: after image object clean-up, its src becomes local blob:// - // but browser won't let us parse this blob:// into file afterwards. so we hold a local copy - _customRawFile: file, - } as Partial); + // auto window resizing based width + lastWidth = wrapperRef.current.clientWidth; + resizeObserver = new ResizeObserver(() => { + const nextWidth = wrapperRef.current?.clientWidth; + if (!nextWidth || nextWidth === lastWidth) return; + lastWidth = nextWidth; + syncViewport(); + }); + resizeObserver.observe(wrapperRef.current!); + }; - fabricRef.current?.add(img); - fabricRef.current?.setActiveObject(img); + initCanvas().then(); - syncViewport(); - // clean up memory - URL.revokeObjectURL(url); - }); - }, + return () => { + isMounted = false; + resizeObserver?.disconnect(); + fabricRef.current?.dispose(); + fabricRef.current = null; + textboxRef.current = null; + }; + }, [initialData, loadContent, readOnly, syncViewport]); - getData: () => { - if (!fabricRef.current) return { objects: [] }; - syncViewport(); + // WHY?: fabric doesn't work like react with state and props based optimized re-renders. + // everytime we there's a change in the data, we should force the render, + // so we let the parent Editor component take control of this. + useImperativeHandle(ref, () => ({ + addImage: (url: string, file: File) => { + if (!fabricRef.current) return; - const json = fabricRef.current.toJSON() as CanvasJSON; - json.canvasWidth = logicalSizeRef.current.width; - json.canvasHeight = logicalSizeRef.current.height; - return json; - }, + fabric.FabricImage.fromURL(url).then((img) => { + img.scaleToWidth(Math.min(300, img.width)); + img.set({ + originX: "left", + originY: "top", + left: PAD, + top: PAD, + noScaleCache: false, + objectCaching: false, + // WHY?: after image object clean-up, its src becomes local blob:// + // but browser won't let us parse this blob:// into file afterwards. so we hold a local copy + _customRawFile: file, + } as Partial); - getImages: () => { - if (!fabricRef.current) return []; - const images = fabricRef.current.getObjects( - "Image", - ) as FabricImageWithFile[]; - return images.map((img) => ({ - src: img.getSrc(), - file: img._customRawFile, - })); - }, + fabricRef.current?.add(img); + fabricRef.current?.setActiveObject(img); - loadData: async (data: CanvasJSON) => { - // if canvas isn't ready yet, stash the data and let the useEffect pick it up - if (!fabricRef.current) { - deferredDataRef.current = data; - return; - } - await loadContent(data); - }, + syncViewport(); + // clean up memory + URL.revokeObjectURL(url); + }); + }, - getStyle: () => { - const textBox = textboxRef.current; + getData: () => { + if (!fabricRef.current) return { objects: [] }; + syncViewport(); - return { - fontFamily: textBox?.fontFamily || DEFAULT_FONT_FAMILY, - fontColor: (textBox?.fill as string) || DEFAULT_FONT_COLOR, - }; - }, - })); + const json = fabricRef.current.toJSON() as CanvasJSON; + json.canvasWidth = logicalSizeRef.current.width; + json.canvasHeight = logicalSizeRef.current.height; + return json; + }, - return ( -
- -
- ); + getImages: () => { + if (!fabricRef.current) return []; + const images = fabricRef.current.getObjects( + "Image", + ) as FabricImageWithFile[]; + return images.map((img) => ({ + src: img.getSrc(), + file: img._customRawFile, + })); + }, + + loadData: async (data: CanvasJSON) => { + // if canvas isn't ready yet, stash the data and let the useEffect pick it up + if (!fabricRef.current) { + deferredDataRef.current = data; + return; + } + await loadContent(data); + }, + + getStyle: () => { + const textBox = textboxRef.current; + + return { + fontFamily: textBox?.fontFamily || DEFAULT_FONT_FAMILY, + fontColor: (textBox?.fill as string) || DEFAULT_FONT_COLOR, + }; + }, + })); + + return ( +
+ +
+ ); } ComposeCanvas.displayName = "ComposeCanvas"; diff --git a/frontend/src/components/login/WelcomeModal.tsx b/frontend/src/components/login/WelcomeModal.tsx index b07b1ce..71be50c 100644 --- a/frontend/src/components/login/WelcomeModal.tsx +++ b/frontend/src/components/login/WelcomeModal.tsx @@ -34,8 +34,7 @@ export default function WelcomeModal({ className="inline text-primary" weight="fill" /> -
-
+ Everything you write here is sealed with your password,{" "} cryptographically , before it leaves your hands. @@ -44,11 +43,11 @@ export default function WelcomeModal({
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index c55de2d..ee16836 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -7,7 +7,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import { z } from "zod"; import { api, publicApi } from "../api/apiClient"; import Logo from "../components/Logo"; -import WelcomeModal from "../components/login/WelcomeModal.tsx"; +import WelcomeModal from "../components/login/WelcomeModal"; import FormField from "../components/ui/FormField"; import Saajan from "../components/ui/Saajan"; import { endpoints } from "../config/endpoints"; @@ -64,7 +64,7 @@ export default function Login() { await setAuthStore(authData.access, userData, masterKey); - navigate(nextRoute, { replace: true }); + navigate(nextRoute, { replace: true, state: location.state }); } catch (err) { let message = "Sorry, we're experiencing technical issues.\nPlease try again later."; -- 2.52.0 From c0fb7aabf8ba9d3b7138e35add16bb2f690d8d77 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 6 May 2026 21:53:20 +0530 Subject: [PATCH 5/5] test: add welcome letter onboarding helper to e2e --- frontend/e2e/letter.spec.ts | 2 +- frontend/e2e/utils/auth.ts | 30 +++++++++++++++++++++++++++++- frontend/playwright.config.ts | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/frontend/e2e/letter.spec.ts b/frontend/e2e/letter.spec.ts index bfc216a..3b90fce 100644 --- a/frontend/e2e/letter.spec.ts +++ b/frontend/e2e/letter.spec.ts @@ -208,7 +208,7 @@ test.describe("Letter Drafting (Real Backend)", () => { // Verify it opens the Reader without a hash logger.info(">> [Drawer] Verifying Reader page..."); // Give it a bit more time for decryption - await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/, { timeout: 15000 }); // UUID without hash + await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/, { timeout: 15000 }); // Reveal and check decrypted content in Reader await expect(page.getByText(/breaking the seal/i)).toBeHidden({ timeout: 10000, diff --git a/frontend/e2e/utils/auth.ts b/frontend/e2e/utils/auth.ts index a9d373e..e0b255e 100644 --- a/frontend/e2e/utils/auth.ts +++ b/frontend/e2e/utils/auth.ts @@ -54,6 +54,34 @@ async function registerAndLogin( await page.getByRole("button", { name: /sign in/i }).click(); await expect(page).toHaveURL(/\/drawer/); + await handleWelcomeLetter(page); logger.info(`[Auth] Successfully authenticated ${email}`); } -export const AuthHelper = { registerAndLogin }; + +/** + * Handles and dismisses the first welocme letter + */ +async function handleWelcomeLetter(page: Page) { + logger.info("[Auth] Handling Welcome Letter..."); + // Click envelope to flip + const envelope = page.locator("#env-front"); + await envelope.waitFor({ state: "visible", timeout: 10000 }); + await envelope.click(); + + // Click seal to open flap + const seal = page.getByAltText("Seal"); + await seal.waitFor({ state: "visible" }); + await seal.click(); + + // Click letter to reveal + await page.locator("#letter").click({ position: { x: 30, y: 15 } }); + + // Click "I'll see you" button + const completeButton = page.getByRole("button", { name: /I'll see you/i }); + await completeButton.waitFor({ state: "visible", timeout: 10000 }); + await completeButton.click(); + + await expect(completeButton).toBeHidden(); +} + +export const AuthHelper = { registerAndLogin, handleWelcomeLetter }; diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 13007d3..8a81b4f 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -15,7 +15,7 @@ const baseUrl = getBaseUrl( ); export default defineConfig({ - timeout: 60000, + timeout: 80000, expect: { timeout: 10000, }, -- 2.52.0