refactor: modularize envelope reveal and logo components for easier usage across the site

This commit is contained in:
ramvignesh-b
2026-04-26 00:55:19 +05:30
parent f352c298e7
commit dadb688c50
4 changed files with 147 additions and 151 deletions
+10 -10
View File
@@ -1,26 +1,26 @@
import { DotIcon } from "@phosphor-icons/react"; import { DotIcon } from "@phosphor-icons/react";
import "@fontsource/knewave/400.css"; import "@fontsource/knewave/400.css";
export default function Logo() { export default function Logo({ scale = 2 }) {
return ( return (
<span <div
role="img" role="img"
aria-label="Pi Ku" aria-label="Pi Ku"
className="inline-flex items-baseline justify-center leading-none select-none" className="inline-flex items-baseline justify-center leading-none select-none"
style={{ fontFamily: "'Knewave', serif" }} style={{ fontFamily: "'Knewave', serif", scale }}
> >
<span className="text-2xl font-light text-accent">&nbsp;Pi</span> <span className={`text-xl font-light text-accent`}>&nbsp;Pi</span>
<DotIcon <DotIcon
weight="fill" weight="fill"
size={12} size={6}
className="text-accent translate-y-[0.3em] -mx-px" className={`text-primary translate-y-1 -mx-px`}
/> />
<span className="text-2xl font-light text-accent">&nbsp;Ku</span> <span className={`text-xl font-light text-accent`}>&nbsp;Ku</span>
<DotIcon <DotIcon
weight="fill" weight="fill"
size={12} size={6}
className="text-accent translate-y-[0.3em] -mx-px" className={`text-primary translate-y-1 -mx-px`}
/> />
</span> </div>
); );
} }
@@ -8,6 +8,7 @@ export interface EnvelopeRevealProps {
date?: string; date?: string;
onRevealComplete: () => void; onRevealComplete: () => void;
ignite: boolean; ignite: boolean;
isFlip?: boolean;
} }
export function EnvelopeReveal({ export function EnvelopeReveal({
@@ -15,9 +16,14 @@ export function EnvelopeReveal({
date, date,
onRevealComplete, onRevealComplete,
ignite, ignite,
isFlip,
}: EnvelopeRevealProps) { }: EnvelopeRevealProps) {
const [revealLetter, setRevealLetter] = useState(false); const [revealLetter, setRevealLetter] = useState(false);
const [isFlipped, setIsFlipped] = useState(false); const [isFlipped, setIsFlipped] = useState(!!isFlip);
useEffect(() => {
setIsFlipped(!!isFlip);
}, [isFlip]);
const [burn, setBurn] = useState<{ width: number; height: number }>({ const [burn, setBurn] = useState<{ width: number; height: number }>({
width: 0, width: 0,
@@ -27,7 +33,10 @@ export function EnvelopeReveal({
const flapCheckbox = useRef<HTMLInputElement>(null); const flapCheckbox = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
if (!ignite) return; if (!ignite) {
setBurn({ width: 0, height: 0 });
return;
}
const burnInterval = setInterval(() => { const burnInterval = setInterval(() => {
setBurn((prev) => ({ width: prev.width + 4, height: prev.height + 6 })); setBurn((prev) => ({ width: prev.width + 4, height: prev.height + 6 }));
}, 100); }, 100);
@@ -43,90 +52,86 @@ export function EnvelopeReveal({
}; };
return ( return (
<div className="h-screen 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={`relative h-70 w-105 transform-3d transition-transform duration-2000 ${isFlipped ? "rotate-y-180" : ""}`}
>
<div <div
className={`relative h-70 w-105 transform-3d transition-transform duration-2000 ${isFlipped ? "rotate-y-180" : ""}`} className={` flex backface-hidden rotate-y-180 justify-center transition-all duration-1000 ${isFlipped ? "" : "pointer-events-none"}`}
> >
<div <div
className={` flex backface-hidden rotate-y-180 justify-center transition-all duration-1000 ${isFlipped ? "" : "pointer-events-none"}`} id="env-top"
className="z-4 delay-500 transition-all duration-2000 absolute peer h-40 w-54 mt-0 bg-base-200 mask mask-triangle-2 scale-x-234 has-checked:scale-y-[-1] has-checked:-translate-y-full has-checked:z-1 has-checked:duration-1000"
> >
<div <input
id="env-top" type="checkbox"
className="z-4 delay-500 transition-all duration-2000 absolute peer h-40 w-54 mt-0 bg-base-200 mask mask-triangle-2 scale-x-234 has-checked:scale-y-[-1] has-checked:-translate-y-full has-checked:z-1 has-checked:duration-1000" className="transition checkbox absolute h-full w-full text-transparent bg-transparent z-100"
> ref={flapCheckbox}
<input
type="checkbox"
className="transition checkbox absolute h-full w-full text-transparent bg-transparent z-100"
ref={flapCheckbox}
/>
</div>
<img
className={
"translate-y-24 delay-2000 absolute z-6 peer-has-checked:pointer-events-none peer-has-checked:opacity-0 peer-has-checked:delay-0 transition-opacity duration-1000 cursor-pointer"
}
src={waxSeal}
alt="Seal"
onClick={() => flapCheckbox.current?.click()}
onKeyDown={() => flapCheckbox.current?.click()}
/> />
<button
type="button"
id="letter"
className={`absolute mx-auto transition-all peer-has-checked:delay-800 peer-has-checked:duration-1000 duration-1000 mt-2 h-55 w-105 bg-paper peer-has-checked:-mt-12 hover:-mt-24 cursor-pointer ${revealLetter ? "duration-1000 peer-has-checked:duration-3000 w-screen max-w-4xl h-screen z-101 -translate-y-90" : "peer-has-checked:z-1"}`}
onClick={handleClick}
></button>
<div
id="env-right"
className="absolute h-70 w-105 bg-base-300 mask mask-triangle-3 -mr-48 z-3 pointer-events-none"
></div>
<div
id="env-left"
className="absolute h-70 w-105 bg-base-300 mask mask-triangle-4 -ml-48 z-3 pointer-events-none"
></div>
<button
type="button"
id="env-bottom"
className="absolute h-70 w-45 bg-base-200 mask mask-triangle-2 scale-y-[-1] mt-15 scale-x-240 z-3"
></button>
</div> </div>
<img
className={
"translate-y-24 delay-2000 absolute z-6 peer-has-checked:pointer-events-none peer-has-checked:opacity-0 peer-has-checked:delay-0 transition-opacity duration-1000 cursor-pointer"
}
src={waxSeal}
alt="Seal"
onClick={() => flapCheckbox.current?.click()}
onKeyDown={() => flapCheckbox.current?.click()}
/>
<button <button
id="env-front"
type="button" type="button"
className={`text-left p-10 absolute inset-0 backface-hidden w-110 bg-base-200 z-99 rounded-md -translate-x-2 ${isFlipped ? "pointer-events-none" : ""}`} id="letter"
onClick={() => setIsFlipped((prev) => !prev)} className={`absolute mx-auto transition-all peer-has-checked:delay-800 peer-has-checked:duration-1000 duration-1000 mt-2 h-55 w-105 bg-paper peer-has-checked:-mt-12 hover:-mt-24 cursor-pointer ${revealLetter ? "duration-1000 peer-has-checked:duration-3000 w-screen max-w-4xl h-screen z-101 -translate-y-90" : "peer-has-checked:z-1"}`}
> onClick={handleClick}
<span className={"text-neutral-content/60 font-xs font-display"}> ></button>
to
</span> <div
<h1 className="text-3xl font-bold text-base-content"> id="env-right"
{recipient} className="absolute h-70 w-105 bg-base-300 mask mask-triangle-3 -mr-48 z-3 pointer-events-none"
</h1> ></div>
<p className="text-base-content/60 font-display mt-8">{date}</p> <div
<img id="env-left"
src={stamp} className="absolute h-70 w-105 bg-base-300 mask mask-triangle-4 -ml-48 z-3 pointer-events-none"
alt={"stamp"} ></div>
className={ <button
"z-0 rotate-6 opacity-80 text-accent absolute mt-0 mr-1 top-4 right-0" type="button"
} id="env-bottom"
/> className="absolute h-70 w-45 bg-base-200 mask mask-triangle-2 scale-y-[-1] mt-15 scale-x-240 z-3"
<WavesIcon ></button>
className={"absolute mt-0 mr-12 top-18 right-8 text-primary"}
size={50}
/>
<WavesIcon
className={"absolute mt-0 mr-4 top-18 right-8 text-primary"}
size={50}
/>
</button>
</div> </div>
<button
id="env-front"
type="button"
className={`text-left p-10 absolute inset-0 backface-hidden w-110 bg-base-200 z-99 rounded-md -translate-x-2 ${isFlipped ? "pointer-events-none" : ""}`}
onClick={() => setIsFlipped((prev) => !prev)}
>
<span className={"text-neutral-content/60 font-xs font-display"}>
to
</span>
<h1 className="text-3xl font-bold text-base-content">{recipient}</h1>
<p className="text-base-content/60 font-display mt-8">{date}</p>
<img
src={stamp}
alt={"stamp"}
className={
"z-0 rotate-6 opacity-80 text-accent absolute mt-0 mr-1 top-4 right-0"
}
/>
<WavesIcon
className={"absolute mt-0 mr-12 top-18 right-8 text-primary"}
size={50}
/>
<WavesIcon
className={"absolute mt-0 mr-4 top-18 right-8 text-primary"}
size={50}
/>
</button>
</div> </div>
{ignite && ( {ignite && (
<div className="absolute w-90 h-60 bg-transparent z-100 overflow-hidden flex align-baseline"> <div className="absolute w-115 h-70 z-100 overflow-hidden flex align-baseline -translate-y-70 -translate-x-5">
<div <div
className="absolute border-2 border-amber-200 -bottom-3 -right-3 w-0 h-0 transition-all duration-500 bg-base-100 rounded-tl-full rounded-bl-full origin-bottom-right" className="absolute z-1000 border-2 border-amber-200 -bottom-3 -right-3 w-0 h-0 transition-all duration-500 bg-base-100 rounded-tl-full rounded-bl-full origin-bottom-right"
style={{ style={{
width: 2 * burn.width, width: 2 * burn.width,
height: 2 * burn.height, height: 2 * burn.height,
@@ -134,6 +139,6 @@ export function EnvelopeReveal({
></div> ></div>
</div> </div>
)} )}
</div> </>
); );
} }
+41 -54
View File
@@ -2,73 +2,60 @@
@plugin "daisyui"; @plugin "daisyui";
@plugin "daisyui/theme" { @plugin "daisyui/theme" {
name: "piku"; name: "piku";
default: true; default: true;
prefersdark: true; prefersdark: true;
color-scheme: dark; color-scheme: dark;
/* ── Base surfaces ── */ --color-base-100: oklch(14% 0.012 35);
--color-base-100: oklch(14% 0.012 35); /* was 0.018 hue 50 */ --color-base-200: oklch(18% 0.014 33);
--color-base-200: oklch(18% 0.014 33); --color-base-300: oklch(22% 0.016 32);
--color-base-300: oklch(22% 0.016 32); --color-base-content: oklch(82% 0.02 70);
--color-base-content: oklch(
82% 0.02 70
); /* aged parchment, not crisp white */
/* ── Primary: old lamp gold — warm, incandescent ── */ --color-primary: oklch(67% 0.11 78);
--color-primary: oklch(67% 0.11 78); --color-primary-content: oklch(15% 0.03 70);
--color-primary-content: oklch(15% 0.03 70);
/* ── Secondary: dusty plum ── */ --color-secondary: oklch(48% 0.08 305);
--color-secondary: oklch(48% 0.08 305); --color-secondary-content: oklch(92% 0.01 305);
--color-secondary-content: oklch(92% 0.01 305);
/* ── Accent: muted lavender-clay ── */ --color-accent: oklch(55% 0.06 325);
--color-accent: oklch(55% 0.06 325); --color-accent-content: oklch(18% 0.03 295);
--color-accent-content: oklch(18% 0.03 295);
/* ── Neutral: warm stone ── */ --color-neutral: oklch(28% 0.02 45);
--color-neutral: oklch(28% 0.02 45); --color-neutral-content: oklch(80% 0.015 60);
--color-neutral-content: oklch(80% 0.015 60);
/* ── Semantic — desaturated, no alarm ── */ --color-info: oklch(60% 0.07 240);
--color-info: oklch(60% 0.07 240); --color-info-content: oklch(95% 0.01 240);
--color-info-content: oklch(95% 0.01 240); --color-success: oklch(60% 0.08 150);
--color-success: oklch(60% 0.08 150); --color-success-content: oklch(16% 0.03 150);
--color-success-content: oklch(16% 0.03 150); --color-warning: oklch(68% 0.08 72);
--color-warning: oklch(68% 0.08 72); /* honey, not caution-sign amber */ --color-warning-content: oklch(18% 0.03 60);
--color-warning-content: oklch(18% 0.03 60); --color-error: oklch(55% 0.1 22);
--color-error: oklch(55% 0.1 22); --color-error-content: oklch(92% 0.01 22);
--color-error-content: oklch(92% 0.01 22);
/* ── Shape ── */ --radius-selector: 0.5rem;
--radius-selector: 0.5rem; --radius-field: 0.375rem;
--radius-field: 0.375rem; --radius-box: 0.5rem;
--radius-box: 0.5rem;
/* ── Effects ── */ --depth: 1;
--depth: 1; --noise: 0.03;
--noise: 0.03;
/* ── Border ── */ --border: 1px;
--border: 1px;
} }
@theme { @theme {
--font-display: "Playwrite HR Lijeva Variable", cursive; --font-display: "Playwrite HR Lijeva Variable", cursive;
--font-sans: "Jost Variable", sans-serif; --font-sans: "Jost Variable", sans-serif;
--font-serif: "Playfair Display Variable", serif; --font-serif: "Playfair Display Variable", serif;
--color-glass-bg: rgba( --color-glass-bg: rgba(28,
28, 22,
22, 16,
16, 0.45);
0.45 --shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6);
); /* slightly deeper to match new base */ --radius-xl: 1.5rem;
--shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6); --color-paper: oklch(97% 0.008 80);
--radius-xl: 1.5rem;
--color-paper: oklch(97% 0.008 80);
} }
.glass-card { .glass-card {
@apply bg-glass-bg backdrop-blur-xl border border-white/5 shadow-warm rounded-xl; @apply bg-glass-bg backdrop-blur-xl border border-white/5 shadow-warm rounded-xl;
} }
+16 -12
View File
@@ -244,23 +244,27 @@ export default function Reader() {
<section className="min-h-fit w-full bg-base-100 px-4 py-8 md:py-16 font-serif relative overflow-hidden"> <section className="min-h-fit w-full bg-base-100 px-4 py-8 md:py-16 font-serif relative overflow-hidden">
<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 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" && (
<EnvelopeReveal <div className="h-[80vh] mx-auto flex-col items-center flex justify-center">
recipient={metadata?.recipient || "Someone dear"} <div className="perspective-distant scale-80 duration-1000 transition-all animate-[pulse_2s_linear_1]">
date={ <EnvelopeReveal
metadata?.updated_at recipient={metadata?.recipient || "Someone dear"}
? formatDate(new Date(metadata.updated_at)) date={
: undefined metadata?.updated_at
} ? formatDate(new Date(metadata.updated_at))
onRevealComplete={() => setRevealState("revealed")} : undefined
ignite={ignite} }
/> onRevealComplete={() => setRevealState("revealed")}
ignite={ignite}
/>
</div>
</div>
)} )}
</div> </div>
@@ -275,7 +279,7 @@ export default function Reader() {
/> />
{revealState === "revealed" && ( {revealState === "revealed" && (
<div className="max-w-4xl m-8 mx-auto space-y-8 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" />