refactor: implement reusable Modal component

This commit is contained in:
ramvignesh-b
2026-04-28 22:52:06 +05:30
parent 8b28949d73
commit faee0b45d6
11 changed files with 316 additions and 323 deletions
+40 -41
View File
@@ -1,4 +1,5 @@
import { LockKeyIcon } from "@phosphor-icons/react"; import { LockKeyIcon } from "@phosphor-icons/react";
import { Modal } from "../ui/Modal";
interface PasskeyModalProps { interface PasskeyModalProps {
onUnlock: (password: string) => Promise<void>; onUnlock: (password: string) => Promise<void>;
@@ -6,47 +7,45 @@ interface PasskeyModalProps {
export function PasskeyModal({ onUnlock }: PasskeyModalProps) { export function PasskeyModal({ onUnlock }: PasskeyModalProps) {
return ( return (
<div className="modal modal-open bg-base-100/20 backdrop-blur-md z-100"> <Modal isOpen={true}>
<div className="modal-box p-12 flex flex-col items-center"> <LockKeyIcon
<LockKeyIcon size={48}
size={48} className="text-primary mx-auto mb-8 animate-pulse"
className="text-primary mx-auto mb-8 animate-pulse" />
/> <h3 className="font-bold text-lg font-display text-primary">
<h3 className="font-bold text-lg font-display text-primary"> Authentication Required
Authentication Required </h3>
</h3> <p className="py-4 font-sans">
<p className="py-4 font-sans"> We need your passkey to open your letters
We need your passkey to open your letters </p>
</p> <div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div> <p className="text-xs text-neutral-content/30 font-mono italic">
<p className="text-xs text-neutral-content/30 font-mono italic"> Your passkey is used to decrypt your data locally.
Your passkey is used to decrypt your data locally. </p>
</p> <div className="modal-action items-center gap-4">
<div className="modal-action items-center gap-4"> <form
<form className="form-control w-full inline-flex"
className="form-control w-full inline-flex" onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => {
onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => { e.preventDefault();
e.preventDefault(); const formData = new FormData(e.currentTarget);
const formData = new FormData(e.currentTarget); const password = formData.get("password") as string;
const password = formData.get("password") as string; if (!password) return;
if (!password) return; await onUnlock(password);
await onUnlock(password); }}
}} >
> <input
<input name="password"
name="password" required
required type="password"
type="password" placeholder="password"
placeholder="password" className="font-sans validator input input-bordered rounded-r-none"
className="font-sans validator input input-bordered rounded-r-none" />
/> <div className="validator-message text-xs text-error"></div>
<div className="validator-message text-xs text-error"></div> <button type="submit" className="btn btn-primary rounded-l-none">
<button type="submit" className="btn btn-primary rounded-l-none"> Unlock
Unlock </button>
</button> </form>
</form>
</div>
</div> </div>
</div> </Modal>
); );
} }
@@ -1,6 +1,7 @@
import { LockIcon } from "@phosphor-icons/react"; import { LockIcon } from "@phosphor-icons/react";
import type { NavigateFunction } from "react-router-dom"; import type { NavigateFunction } from "react-router-dom";
import { PATHS, ROUTES } from "../../config/routes"; import { PATHS, ROUTES } from "../../config/routes";
import { Modal } from "../ui/Modal";
interface PostSealModalProps { interface PostSealModalProps {
sealedTargetId: string | null; sealedTargetId: string | null;
@@ -13,72 +14,68 @@ export function PostSealModal({
navigate, navigate,
type = "KEPT", type = "KEPT",
}: PostSealModalProps) { }: PostSealModalProps) {
if (!sealedTargetId) return null;
return ( return (
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-1000"> <Modal isOpen={!!sealedTargetId}>
<div className="modal-box flex flex-col items-center text-center gap-6"> <LockIcon size={32} weight="duotone" className="text-primary mt-3" />
<LockIcon size={32} weight="duotone" className="text-primary mt-3" /> <h3 className="font-serif text-2xl">Your letter is sealed</h3>
<h3 className="font-serif text-2xl">Your letter is sealed</h3> <p className="text-base-content/60">
<p className="text-base-content/60"> It's encrypted and always safe in your drawer.
It's encrypted and always safe in your drawer. </p>
{type === "KEPT" ? (
<p className="text-base-content/80 text-sm font-sans">
When you're ready,
<br />
you can{" "}
<span className="text-primary font-bold font-display">read</span> it,{" "}
<span className="text-accent font-bold font-display">send</span> it to
someone, or{" "}
<span className="text-error font-bold font-display">burn</span> it to
release
</p> </p>
) : (
<p className="text-base-content/80 text-sm font-sans">
Be assured that the letter will find you when the time is right.
<br />
Till then,{" "}
<span className="font-bold font-display text-primary">
take a deep breath
</span>
, <span className="font-bold font-display text-accent">manifest</span>
, and{" "}
<span className="font-bold font-display text-success">
let it rest
</span>
.
</p>
)}
<div className="modal-action w-full justify-center gap-3 mt-4 mb-4">
{type === "KEPT" ? ( {type === "KEPT" ? (
<p className="text-base-content/80 text-sm font-sans"> <>
When you're ready,
<br />
you can{" "}
<span className="text-primary font-bold font-display">read</span>{" "}
it, <span className="text-accent font-bold font-display">send</span>{" "}
it to someone, or{" "}
<span className="text-error font-bold font-display">burn</span> it
to release
</p>
) : (
<p className="text-base-content/80 text-sm font-sans">
Be assured that the letter will find you when the time is right.
<br />
Till then,{" "}
<span className="font-bold font-display text-primary">
take a deep breath
</span>
,{" "}
<span className="font-bold font-display text-accent">manifest</span>
, and{" "}
<span className="font-bold font-display text-success">
let it rest
</span>
.
</p>
)}
<div className="modal-action w-full justify-center gap-3 mt-4 mb-4">
{type === "KEPT" ? (
<>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => navigate(ROUTES.DRAWER)}
>
Keep it to myself
</button>
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() => navigate(PATHS.read(sealedTargetId))}
>
View letter
</button>
</>
) : (
<button <button
type="button" type="button"
className="btn btn-ghost btn-sm" className="btn btn-ghost btn-sm"
onClick={() => navigate(ROUTES.DRAWER)} onClick={() => navigate(ROUTES.DRAWER)}
> >
Step Away... Keep it to myself
</button> </button>
)} <button
</div> type="button"
className="btn btn-primary btn-sm"
onClick={() => navigate(PATHS.read(sealedTargetId!))}
>
View letter
</button>
</>
) : (
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => navigate(ROUTES.DRAWER)}
>
Step Away...
</button>
)}
</div> </div>
</div> </Modal>
); );
} }
+57 -59
View File
@@ -6,6 +6,7 @@ import {
TrayIcon, TrayIcon,
VaultIcon, VaultIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { Modal } from "../ui/Modal";
interface ToolBarProps { interface ToolBarProps {
fileInputRef: React.RefObject<HTMLInputElement | null>; fileInputRef: React.RefObject<HTMLInputElement | null>;
@@ -150,65 +151,62 @@ export function VaultConfirmModal({
setUnlockDate, setUnlockDate,
}: VaultConfirmModalProps) { }: VaultConfirmModalProps) {
return ( return (
<div className={"modal modal-open bg-base-100/10 backdrop-blur-md"}> <Modal isOpen={true}>
<div className="modal-box p-12 flex flex-col items-center bg-base-100/90"> <VaultIcon
<VaultIcon size={48}
size={48} className="text-primary mx-auto mb-8 animate-pulse"
className="text-primary mx-auto mb-8 animate-pulse" />
<h3 className="font-serif text-3xl">Take it away, then?</h3>
<p className="text-base-content/60 text-sm text-center mt-4">
By vaulting this letter, you ask me to hold on to this.
<br />
I'll remember to mail you this on the unlock date.
<br />
<span className={"font-bold text-primary"}>
{" "}
But I won't let you read or rewrite this letter until then.
</span>
<br />
</p>
<form
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const unlockDateStr = formData.get("vault-date") as string;
const newUnlockDate = new Date(unlockDateStr);
setUnlockDate(newUnlockDate);
await onSave("VAULT", newUnlockDate);
setConfirmModal(null);
}}
id="vault-form"
className="min-w-75"
>
<div className={"divider tracking-tightest font-display text-sm"}>
Set an unlock date
</div>
<input
required
type="date"
className="input input-bordered w-full"
name="vault-date"
/> />
<h3 className="font-serif text-3xl">Take it away, then?</h3> <div className="w-full flex justify-center gap-8 mt-4">
<p className="text-base-content/60 text-sm text-center mt-4"> <button
By vaulting this letter, you ask me to hold on to this. type="button"
<br /> className="btn btn-ghost btn-sm mt-4"
I'll remember to mail you this on the unlock date. onClick={() => setConfirmModal(null)}
<br /> >
<span className={"font-bold text-primary"}> I need time
{" "} </button>
But I won't let you read or rewrite this letter until then. <button
</span> className="btn btn-primary btn-sm mt-4"
<br /> type="submit"
</p> form="vault-form"
<form >
onSubmit={async (e) => { Take it
e.preventDefault(); </button>
const formData = new FormData(e.currentTarget); </div>
const unlockDateStr = formData.get("vault-date") as string; </form>
const newUnlockDate = new Date(unlockDateStr); </Modal>
console.log(newUnlockDate);
setUnlockDate(newUnlockDate);
await onSave("VAULT", newUnlockDate);
setConfirmModal(null);
}}
id="vault-form"
className="min-w-75"
>
<div className={"divider tracking-tightest font-display text-sm"}>
Set an unlock date
</div>
<input
required
type="date"
className="input input-bordered w-full"
name="vault-date"
/>
<div className="w-full flex justify-center gap-8 mt-4">
<button
type="button"
className="btn btn-ghost btn-sm mt-4"
onClick={() => setConfirmModal(null)}
>
I need time
</button>
<button
className="btn btn-primary btn-sm mt-4"
type="submit"
form="vault-form"
>
Take it
</button>
</div>
</form>
</div>
</div>
); );
} }
+7 -14
View File
@@ -1,11 +1,12 @@
import { CampfireIcon, FlameIcon, XCircleIcon } from "@phosphor-icons/react"; import { CampfireIcon, FlameIcon } from "@phosphor-icons/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Modal } from "../ui/Modal";
interface BurnModalProps { interface BurnModalProps {
burnLetter: () => void; burnLetter: () => void;
isBurning: boolean; isBurning: boolean;
setShowBurnModal: (show: boolean) => void; setShowBurnModal: (show: boolean) => void;
setRevealState: (state: "sealed" | "revealed" | "burning" | "burned") => void; setRevealState: (state: "SEALED" | "REVEALED" | "BURNING" | "BURNED") => void;
} }
export function BurnModal({ export function BurnModal({
@@ -20,7 +21,7 @@ export function BurnModal({
useEffect(() => { useEffect(() => {
if (!burnClicked) return; if (!burnClicked) return;
if (flameOn === 100) { if (flameOn === 100) {
setRevealState("sealed"); setRevealState("SEALED");
burnLetter(); burnLetter();
} }
const interval = setInterval(() => { const interval = setInterval(() => {
@@ -33,23 +34,15 @@ export function BurnModal({
const burnStyle = flameOn < 30 ? "" : `contrast(${flameOn / 30})`; const burnStyle = flameOn < 30 ? "" : `contrast(${flameOn / 30})`;
return ( return (
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md"> <Modal isOpen={true} onClose={() => setShowBurnModal(false)}>
<div <div
className={`modal-box flex flex-col items-center gap-4 py-8 text-center transition-all duration-200 ease-in-out ${burnClicked ? "animate-[pulse_15s_linear_infinite]" : ""}`} className={`flex flex-col items-center gap-4 text-center transition-all duration-200 ease-in-out ${burnClicked ? "animate-[pulse_15s_linear_infinite]" : ""}`}
style={ style={
{ {
transform: `rotate(${rotate}deg)`, transform: `rotate(${rotate}deg)`,
} as React.CSSProperties } as React.CSSProperties
} }
> >
<button
type="button"
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
onClick={() => setShowBurnModal(false)}
aria-label="Close"
>
<XCircleIcon size={18} weight="bold" />
</button>
<CampfireIcon <CampfireIcon
size={48} size={48}
weight="duotone" weight="duotone"
@@ -101,6 +94,6 @@ export function BurnModal({
</button> </button>
</div> </div>
</div> </div>
</div> </Modal>
); );
} }
@@ -2,22 +2,22 @@ import { useNavigate } from "react-router-dom";
import { ROUTES } from "../../config/routes"; import { ROUTES } from "../../config/routes";
interface PostActionOverlayProps { interface PostActionOverlayProps {
revealState: "sealed" | "revealed" | "burning" | "burned"; revealState: "SEALED" | "REVEALED" | "BURNING" | "BURNED";
} }
export function PostActionOverlay({ revealState }: PostActionOverlayProps) { export function PostActionOverlay({ revealState }: PostActionOverlayProps) {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div <div
className={`flex flex-col items-center justify-center min-h-screen bg-base-100 ${revealState === "burned" ? "opacity-100" : "opacity-0"} transition-all delay-300 duration-1000`} className={`flex flex-col items-center justify-center min-h-screen bg-base-100 ${revealState === "BURNED" ? "opacity-100" : "opacity-0"} transition-all delay-1000 duration-1000`}
> >
<h1 <h1
className={`text-6xl ${revealState === "burned" ? "opacity-100" : "opacity-0"} lg:text-9xl italic font-extralight text-base-content animate-[pulse_3s_ease-in-out_3]`} className={`text-6xl ${revealState === "BURNED" ? "opacity-100" : "opacity-0"} lg:text-9xl italic font-extralight text-base-content animate-[pulse_3s_ease-in-out_3]`}
> >
It is done It is done
</h1> </h1>
<div <div
className={`text-xl ${revealState === "burned" ? "opacity-100" : "opacity-0"} lg:text-4xl text-center font-extralight text-base-content font-display mt-8 delay-3000 transition-all duration-2000 tracking-wide`} className={`text-xl ${revealState === "BURNED" ? "opacity-100" : "opacity-0"} lg:text-4xl text-center font-extralight text-base-content font-display mt-8 delay-3000 transition-all duration-2000 tracking-wide`}
> >
<p className="w-full"> <p className="w-full">
May your <span className="italic text-primary">soul</span> find May your <span className="italic text-primary">soul</span> find
+20 -21
View File
@@ -1,8 +1,6 @@
import { import { EyeSlashIcon, PaperPlaneTiltIcon } from "@phosphor-icons/react";
EyeSlashIcon, import { Modal } from "../ui/Modal";
PaperPlaneTiltIcon, import Saajan from "../ui/Saajan";
XCircleIcon,
} from "@phosphor-icons/react";
interface ShareModalProps { interface ShareModalProps {
shareLink: string | null; shareLink: string | null;
@@ -15,16 +13,8 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
await navigator.clipboard.writeText(shareLink); await navigator.clipboard.writeText(shareLink);
}; };
return ( return (
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-100"> <>
<div className="modal-box bg-base-100 border border-base-content/5 shadow-2xl relative"> <Modal isOpen={!!shareLink} onClose={() => setShareLink(null)}>
<button
type="button"
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
onClick={() => setShareLink(null)}
aria-label="Close"
>
<XCircleIcon size={18} weight="bold" />
</button>
<div className="flex flex-col items-center justify-center text-center gap-6 py-4"> <div className="flex flex-col items-center justify-center text-center gap-6 py-4">
<div className="space-y-2"> <div className="space-y-2">
<PaperPlaneTiltIcon <PaperPlaneTiltIcon
@@ -34,14 +24,17 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
/> />
<h3 className="font-serif text-3xl">Send this letter</h3> <h3 className="font-serif text-3xl">Send this letter</h3>
<p className="text-base-content/80 text-sm font-sans mt-4"> <p className="text-base-content/80 text-sm font-sans mt-4">
You've carried these words long enough. Send your letter now, and You've carried these words long enough.
let the <span className="text-accent font-display">unsaid</span>{" "} <br />
finally find its home. Send your letter now, and let the{" "}
<span className="text-accent font-display">unsaid</span> finally
find its home.
</p> </p>
<div className="divider mx-auto" /> <div className="divider mx-auto" />
<blockquote className="text-sm info text-neutral-content/60 font-sans"> <blockquote className="text-sm info text-neutral-content/60 font-sans">
The recipient will have the same viewing experience like you do They'll receive it exactly as you're seeing it now.
now. <br />
Nothing more, nothing less.
</blockquote> </blockquote>
</div> </div>
<div className="w-full flex items-center gap-2 bg-base-300 p-2 rounded-xl"> <div className="w-full flex items-center gap-2 bg-base-300 p-2 rounded-xl">
@@ -69,7 +62,13 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
</p> </p>
</div> </div>
</div> </div>
</Modal>
<div className="absolute bottom-0 z-1000 font-sans w-full">
<Saajan
position="top"
message={`Someone once said,\n"To send a letter is a good way to go somewhere without moving anything but your heart."\nThey were not wrong.`}
/>
</div> </div>
</div> </>
); );
} }
+19 -34
View File
@@ -1,4 +1,5 @@
import { WarningIcon, XCircleIcon, XIcon } from "@phosphor-icons/react"; import { WarningIcon } from "@phosphor-icons/react";
import { Modal } from "./Modal";
interface LogModalContent { interface LogModalContent {
status: "WARN" | "ERROR" | "RESET" | "SUCCESS"; status: "WARN" | "ERROR" | "RESET" | "SUCCESS";
@@ -15,40 +16,24 @@ export const LogModal = ({
onClose, onClose,
status, status,
}: LogModalContent) => { }: LogModalContent) => {
return status === "RESET" || !isOpen ? ( return (
<div></div> <Modal isOpen={isOpen && status !== "RESET"} onClose={onClose}>
) : ( <div
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-100"> className={`alert ${status === "WARN" ? "alert-warning" : "alert-error"} flex flex-col items-center text-center gap-6 py-4`}
<div className="modal-box bg-transparent border-none shadow-none relative"> >
<div {status === "WARN" && (
className={`alert ${status === "WARN" ? "alert-warning" : "alert-error"} flex flex-col items-center text-center gap-6 py-4`} <WarningIcon className="text-warning" size={16} weight="duotone" />
> )}
{status === "WARN" && ( {message}
<WarningIcon className="text-warning" size={16} weight="bold" /> <div className="divider text-primary-content text-xs uppercase tracking-widest">
)} Error Stack
{status === "ERROR" && ( </div>
<XCircleIcon className="text-error" size={16} weight="bold" /> <div className="mockup-code bg-base-100 text-error w-full">
)} <pre>
{message} <code>{String(log)}</code>
<div className="divider text-primary-content text-xs uppercase tracking-widest"> </pre>
Error Stack
</div>
<div className="mockup-code bg-base-100 text-error w-full">
<pre>
<code>{String(log)}</code>
</pre>
</div>
<form method="dialog">
<button
type="button"
onClick={onClose}
className="btn btn-sm btn-circle btn-ghost absolute right-6 top-6"
>
<XIcon size={6} weight="bold" />
</button>
</form>
</div> </div>
</div> </div>
</div> </Modal>
); );
}; };
+30
View File
@@ -0,0 +1,30 @@
import { XCircleIcon } from "@phosphor-icons/react";
import type { ReactNode } from "react";
interface ModalProps {
isOpen: boolean;
onClose?: () => void;
children: ReactNode;
}
export function Modal({ isOpen, onClose, children }: ModalProps) {
if (!isOpen) return null;
return (
<div className="modal modal-open modal-middle backdrop-blur-md before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-10 before:pointer-events-none before:bg-[url('assets/noise.gif')]">
<div className="modal-box relative bg-base-100/60 flex flex-col items-center text-center gap-6">
{onClose && (
<button
type="button"
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2 z-20"
onClick={onClose}
aria-label="Close"
>
<XCircleIcon size={18} weight="bold" />
</button>
)}
{children}
</div>
</div>
);
}
+46 -51
View File
@@ -23,6 +23,7 @@ import {
} from "../components/editor/ToolBar"; } from "../components/editor/ToolBar";
import DateDisplay from "../components/ui/DateDisplay"; import DateDisplay from "../components/ui/DateDisplay";
import { LogModal } from "../components/ui/LogModal"; import { LogModal } from "../components/ui/LogModal";
import { Modal } from "../components/ui/Modal";
import { Navbar } from "../components/ui/Navbar"; import { Navbar } from "../components/ui/Navbar";
import { endpoints } from "../config/endpoints"; import { endpoints } from "../config/endpoints";
@@ -356,59 +357,53 @@ export default function Editor() {
)} )}
{saveOverlay !== "idle" && ( {saveOverlay !== "idle" && (
<div <Modal isOpen={showSaveOverlay}>
className={`modal modal-open bg-base-100/20 backdrop-blur-md transition-opacity duration-300 ${ {saveOverlay === "saving" && (
showSaveOverlay ? "opacity-100" : "opacity-0" <div
}`} role="alert"
> className={`alert text-center alert-neutral shadow-lg transition-all ease-in-out duration-2000 ${
<div className="modal-box p-0 bg-transparent shadow-none transition-all duration-300"> showSaveOverlay
{saveOverlay === "saving" && ( ? "opacity-100 scale-100 translate-y-0"
<div : "opacity-0 scale-95 translate-y-1"
role="alert" }`}
className={`alert text-center alert-neutral shadow-lg transition-all ease-in-out duration-2000 ${ >
showSaveOverlay <SpinnerGapIcon
? "opacity-100 scale-100 translate-y-0" size={18}
: "opacity-0 scale-95 translate-y-1" weight="bold"
}`} className="animate-spin"
> />
<SpinnerGapIcon <span className="font-bold">Securing your letter...</span>
size={18} </div>
weight="bold" )}
className="animate-spin"
/>
<span className="font-bold">Securing your letter...</span>
</div>
)}
{saveOverlay === "saved" && ( {saveOverlay === "saved" && (
<div <div
role="alert" role="alert"
className={`alert alert-success shadow-lg transition-all ease-in-out duration-2000 ${ className={`alert alert-success shadow-lg transition-all ease-in-out duration-2000 ${
showSaveOverlay showSaveOverlay
? "opacity-100 scale-100 translate-y-0" ? "opacity-100 scale-100 translate-y-0"
: "opacity-0 scale-95 translate-y-1" : "opacity-0 scale-95 translate-y-1"
}`} }`}
> >
<DownloadSimpleIcon size={18} weight="bold" /> <DownloadSimpleIcon size={18} weight="bold" />
<span className="font-bold">Your letter is saved!</span> <span className="font-bold">Your letter is saved!</span>
</div> </div>
)} )}
{saveOverlay === "error" && ( {saveOverlay === "error" && (
<div <div
role="alert" role="alert"
className={`alert alert-error shadow-lg transition-all duration-300 ${ className={`alert alert-error shadow-lg transition-all duration-300 ${
showSaveOverlay showSaveOverlay
? "opacity-100 scale-100 translate-y-0" ? "opacity-100 scale-100 translate-y-0"
: "opacity-0 scale-95 translate-y-1" : "opacity-0 scale-95 translate-y-1"
}`} }`}
> >
<XIcon size={18} weight="bold" /> <XIcon size={18} weight="bold" />
<span className="font-bold">Failed to save letter</span> <span className="font-bold">Failed to save letter</span>
</div> </div>
)} )}
</div> </Modal>
</div>
)} )}
{confirmModal === "VAULT" && ( {confirmModal === "VAULT" && (
+10 -8
View File
@@ -12,6 +12,7 @@ import { z } from "zod";
import { api, publicApi } from "../api/apiClient"; import { api, publicApi } from "../api/apiClient";
import Logo from "../components/Logo"; import Logo from "../components/Logo";
import FormField from "../components/ui/FormField"; import FormField from "../components/ui/FormField";
import { Modal } from "../components/ui/Modal";
import Saajan from "../components/ui/Saajan"; import Saajan from "../components/ui/Saajan";
import { endpoints } from "../config/endpoints"; import { endpoints } from "../config/endpoints";
import { ROUTES } from "../config/routes"; import { ROUTES } from "../config/routes";
@@ -31,13 +32,8 @@ function WelcomeModal({
setShowWelcome: (show: boolean) => void; setShowWelcome: (show: boolean) => void;
}) { }) {
return ( return (
<div className="modal modal-open backdrop-blur-sm transition-all duration-1000"> <>
<div className="absolute bottom-1"> <Modal isOpen={true}>
<Saajan
message={"I've lost words before.\nI know what it feels like."}
/>
</div>
<div className="modal-box border bg-base-100/20 border-primary/20 shadow-2xl p-8">
<div className="flex flex-col items-center text-center gap-4"> <div className="flex flex-col items-center text-center gap-4">
<div className="bg-primary/10 p-4 rounded-full animate-pulse"> <div className="bg-primary/10 p-4 rounded-full animate-pulse">
<ShieldCheckIcon <ShieldCheckIcon
@@ -96,8 +92,14 @@ function WelcomeModal({
</button> </button>
</div> </div>
</div> </div>
</Modal>
<div className="absolute bottom-0 z-1000 font-sans w-full">
<Saajan
position="top"
message={"I've lost words before.\nI know what it feels like."}
/>
</div> </div>
</div> </>
); );
} }
+29 -34
View File
@@ -33,6 +33,7 @@ interface LetterMetadata {
updated_at?: string; updated_at?: string;
} }
const WAIT_FOR_BURN_MS = 18000;
export default function Reader() { export default function Reader() {
const { public_id } = useParams(); const { public_id } = useParams();
const location = useLocation(); const location = useLocation();
@@ -44,13 +45,10 @@ export default function Reader() {
const [isDecrypting, setIsDecrypting] = useState(true); const [isDecrypting, setIsDecrypting] = useState(true);
const [revealState, setRevealState] = useState< const [revealState, setRevealState] = useState<
"sealed" | "revealed" | "burned" | "burning" "SEALED" | "REVEALED" | "BURNED" | "BURNING"
>("sealed"); >("SEALED");
const [error, setError] = useState<{ const [logTrace, setLogTrace] = useState<{
message: string; type: "WARN" | "ERROR";
log: string;
} | null>(null);
const [warning, setWarning] = useState<{
message: string; message: string;
log: string; log: string;
} | null>(null); } | null>(null);
@@ -92,8 +90,8 @@ export default function Reader() {
setShowBurnModal(false); setShowBurnModal(false);
setIgnite(true); setIgnite(true);
setTimeout(() => { setTimeout(() => {
setRevealState("burned"); setRevealState("BURNED");
}, 13000); }, WAIT_FOR_BURN_MS);
} }
}; };
@@ -180,17 +178,19 @@ export default function Reader() {
); );
} }
} catch (err) { } catch (err) {
setWarning({ setLogTrace({
message: message:
"Failed to decrypt elements. Images might not render in the letter as intended.", "Failed to decrypt elements. Images might not render in the letter as intended.",
log: err instanceof Error ? err.message : "Unknown error", log: err instanceof Error ? err.message : "Unknown error",
type: "WARN",
}); });
} }
setDecryptedCanvasData(canvasData); setDecryptedCanvasData(canvasData);
} catch (err) { } catch (err) {
setError({ setLogTrace({
message: `Failed to load letter :(`, message: `Failed to load letter `,
log: err instanceof Error ? err.message : "Unknown error", log: err instanceof Error ? err.message : "Unknown error",
type: "ERROR",
}); });
} finally { } finally {
setIsDecrypting(false); setIsDecrypting(false);
@@ -203,7 +203,7 @@ export default function Reader() {
useEffect(() => { useEffect(() => {
if ( if (
!isDecrypting && !isDecrypting &&
revealState === "revealed" && revealState === "REVEALED" &&
decryptedCanvasData && decryptedCanvasData &&
canvasRef.current canvasRef.current
) { ) {
@@ -214,12 +214,12 @@ export default function Reader() {
if (isDecrypting) { if (isDecrypting) {
return ( return (
<div className="flex items-center justify-center bg-base-100 font-serif"> <div className="flex items-center justify-center bg-base-100 font-serif">
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.4)_100%)] pointer-events-none z-0" /> <div className="fixed w-screen h-screen inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.4)_100%)] pointer-events-none z-0" />
<div className="text-center space-y-6 z-10"> <div className="text-center space-y-6 z-10">
<Logo /> <Logo />
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<span className="loading loading-ring loading-md text-primary/40"></span> <span className="loading loading-ring loading-md text-primary/40"></span>
<p className="text-[10px] uppercase tracking-[0.4em] text-base-content/20 animate-pulse"> <p className="text-xs uppercase tracking-widest text-base-content/20 animate-pulse">
Breaking the seal... Breaking the seal...
</p> </p>
</div> </div>
@@ -228,14 +228,17 @@ export default function Reader() {
); );
} }
if (error) { if (logTrace) {
return ( return (
<LogModal <LogModal
isOpen={!!error} isOpen={!!logTrace}
onClose={() => (window.location.href = "/")} onClose={() => {
message={error.message} if (logTrace.type === "ERROR") window.location.href = "/";
log={error.log} setLogTrace(null);
status="ERROR" }}
message={logTrace.message}
log={logTrace.log}
status={logTrace.type}
/> />
); );
} }
@@ -245,12 +248,12 @@ export default function Reader() {
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" /> <div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
<div <div
className={`transition-all delay-300 duration-1000 relative ${ className={`transition-all delay-300 duration-1000 relative ${
revealState === "revealed" revealState === "REVEALED"
? "opacity-0 w-0 h-0 overflow-hidden invisible" ? "opacity-0 w-0 h-0 overflow-hidden invisible"
: "opacity-100" : "opacity-100"
}`} }`}
> >
{revealState === "sealed" && ( {revealState === "SEALED" && (
<div className="h-[80vh] mx-auto flex-col items-center flex justify-center"> <div className="h-[80vh] mx-auto flex-col items-center flex justify-center">
<div className="perspective-distant scale-80 duration-1000 transition-all animate-[pulse_2s_linear_1]"> <div className="perspective-distant scale-80 duration-1000 transition-all animate-[pulse_2s_linear_1]">
<EnvelopeReveal <EnvelopeReveal
@@ -260,7 +263,7 @@ export default function Reader() {
? formatDate(new Date(metadata.updated_at)) ? formatDate(new Date(metadata.updated_at))
: undefined : undefined
} }
onRevealComplete={() => setRevealState("revealed")} onRevealComplete={() => setRevealState("REVEALED")}
ignite={ignite} ignite={ignite}
/> />
</div> </div>
@@ -270,15 +273,7 @@ export default function Reader() {
{ignite && <PostActionOverlay revealState={revealState} />} {ignite && <PostActionOverlay revealState={revealState} />}
<LogModal {revealState === "REVEALED" && (
isOpen={!!warning}
onClose={() => setWarning(null)}
message={warning?.message || ""}
log={warning?.log || ""}
status="WARN"
/>
{revealState === "revealed" && (
<div className="max-w-4xl m-8 mx-auto space-y-8 h-full relative inset-0 z-100"> <div className="max-w-4xl m-8 mx-auto space-y-8 h-full relative inset-0 z-100">
<div className="relative group perspective-1000"> <div className="relative group perspective-1000">
<div className="absolute inset-0 bg-primary/5 blur-3xl rounded-full scale-75 opacity-0 group-hover:opacity-100 transition-opacity duration-1000 pointer-events-none" /> <div className="absolute inset-0 bg-primary/5 blur-3xl rounded-full scale-75 opacity-0 group-hover:opacity-100 transition-opacity duration-1000 pointer-events-none" />
@@ -309,7 +304,7 @@ export default function Reader() {
/> />
)} )}
{isAuthor && revealState !== "burned" && ( {isAuthor && revealState !== "BURNED" && (
<div className="flex justify-center gap-2 mt-8 z-10 relative"> <div className="flex justify-center gap-2 mt-8 z-10 relative">
<button <button
id="share-letter-btn" id="share-letter-btn"