Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fff90902b5 | |||
| a3a56d4316 | |||
| 2ba5d6964f | |||
| ffe588c3ec | |||
| a7cced71ee | |||
| c6545a11b1 | |||
| 2419b73b15 |
@@ -68,7 +68,8 @@ export function DrawerSection({
|
||||
<div className="font-sans text-xs text-base-content/20 mt-1">
|
||||
<span className="font-mono text-xs md:text-base -mt-1 absolute text-primary/30">
|
||||
{count}
|
||||
</span>{" "}
|
||||
</span>
|
||||
|
||||
<span className="ml-3">{subtext}</span>
|
||||
</div>
|
||||
<div className="absolute right-5 -translate-y-15 text-base-content/4">
|
||||
|
||||
@@ -41,10 +41,15 @@ export function PasskeyModal() {
|
||||
required
|
||||
type="password"
|
||||
placeholder="password"
|
||||
data-testid="passkey-input"
|
||||
className="font-sans validator input input-bordered rounded-r-none"
|
||||
/>
|
||||
<div className="validator-message text-xs text-error"></div>
|
||||
<button type="submit" className="btn btn-primary rounded-l-none">
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="passkey-submit-btn"
|
||||
className="btn btn-primary rounded-l-none"
|
||||
>
|
||||
Unlock
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -25,10 +25,11 @@ export function PostSealModal({
|
||||
<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,{" "}
|
||||
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{" "}
|
||||
someone, or
|
||||
<span className="text-error font-bold font-display">burn</span> it to
|
||||
release
|
||||
</p>
|
||||
@@ -36,12 +37,12 @@ export function PostSealModal({
|
||||
<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,{" "}
|
||||
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{" "}
|
||||
, and
|
||||
<span className="font-bold font-display text-success">
|
||||
let it rest
|
||||
</span>
|
||||
|
||||
@@ -194,6 +194,7 @@ export function ToolBar({
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="vault-trigger-btn"
|
||||
className="btn btn-neutral btn-sm rounded-full px-6 group"
|
||||
onClick={() => setConfirmModal("VAULT")}
|
||||
>
|
||||
@@ -269,8 +270,7 @@ export function VaultConfirmModal({
|
||||
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.
|
||||
But I won't let you read or rewrite this letter until then.
|
||||
</span>
|
||||
<br />
|
||||
</p>
|
||||
@@ -299,6 +299,7 @@ export function VaultConfirmModal({
|
||||
<div className="w-full flex justify-center gap-8 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="vault-cancel-btn"
|
||||
className="btn btn-ghost btn-sm mt-4"
|
||||
onClick={() => setConfirmModal(null)}
|
||||
>
|
||||
@@ -307,6 +308,7 @@ export function VaultConfirmModal({
|
||||
<button
|
||||
className="btn btn-primary btn-sm mt-4"
|
||||
type="submit"
|
||||
data-testid="vault-confirm-btn"
|
||||
form="vault-form"
|
||||
>
|
||||
Take it
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function WelcomeModal({
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={true}>
|
||||
<div className="flex flex-col items-center text-center gap-4">
|
||||
<div className="flex flex-col items-center text-center gap-2 md:gap-4">
|
||||
<div className="bg-primary/10 p-4 rounded-full animate-pulse">
|
||||
<ShieldCheckIcon
|
||||
size={48}
|
||||
@@ -24,40 +24,42 @@ export default function WelcomeModal({
|
||||
/>
|
||||
</div>
|
||||
<h3 className="font-display text-2xl font-bold text-primary">
|
||||
Welcome to
|
||||
<Logo /> !
|
||||
Welcome to
|
||||
<Logo type="inline" />
|
||||
</h3>
|
||||
<p className="text-base-content/80 leading-relaxed">
|
||||
<p className="inline text-sm md:text-base text-base-content/80">
|
||||
Before we begin, let me make a small promise.
|
||||
<HandPalmIcon
|
||||
size={18}
|
||||
className="inline text-primary"
|
||||
weight="fill"
|
||||
/>
|
||||
<span className="divider my-0 block"></span>
|
||||
Everything you write here is sealed with your password,{" "}
|
||||
<span className="divider my-0"></span>
|
||||
Everything you write here is sealed with your password,
|
||||
<span className="font-display text-success">cryptographically</span>
|
||||
, before it leaves your hands.
|
||||
<br />A fancy way of saying, I couldn't if I tried.
|
||||
<br />
|
||||
<br />A fancy way of saying, no one else can read them without your
|
||||
key—not even me.
|
||||
</p>
|
||||
|
||||
<div className="alert alert-warning bg-paper/20 border-paper/20 flex items-start gap-3 text-left py-3">
|
||||
<WarningIcon size={24} weight="fill" className="shrink-0 mt-0.5" />
|
||||
<div className="text-sm font-medium text-primary-content">
|
||||
<div className="alert alert-warning flex items-start gap-3 text-left py-3">
|
||||
<WarningIcon size={24} weight="fill" className="shrink-0" />
|
||||
<div className="text-xs md:text-sm font-medium text-primary-content tracking-tight">
|
||||
If you ever happen to forget your password, your letters are lost
|
||||
to time, forever.
|
||||
<br />
|
||||
<span className="font-bold mt-2 block">
|
||||
I highly, highly recommend storing this password in your{" "}
|
||||
<span className="mt-2 block">
|
||||
I highly, <span className="font-bold italic">highly</span>
|
||||
recommend storing this password in your
|
||||
<a
|
||||
href="https://www.privacyguides.org/en/passwords/"
|
||||
target="_blank"
|
||||
className="link link-primary-content"
|
||||
className="link link-neutral!"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
password manager
|
||||
</a>{" "}
|
||||
or somewhere safe to remember it.
|
||||
</a>
|
||||
or somewhere safe to remember it.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,9 +76,9 @@ export default function WelcomeModal({
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="absolute bottom-0 right-0 z-1000 font-sans w-full">
|
||||
<div className="absolute bottom-0 md:right-5/12 z-1000 font-sans w-full flex justify-center">
|
||||
<Saajan
|
||||
position="top"
|
||||
position="left"
|
||||
message={"I've lost words before.\nI know what it feels like."}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -58,8 +58,8 @@ export function BurnModal({
|
||||
Let the echoes of your unsaid be finally released.
|
||||
</p>
|
||||
<div className="mt-4 font-sans text-sm">
|
||||
<span className="text-error">Press</span> and{" "}
|
||||
<span className="text-error">hold</span> the{" "}
|
||||
<span className="text-error">Press</span> and
|
||||
<span className="text-error">hold</span> the
|
||||
<span className="text-amber-300">flame</span> to proceed.
|
||||
</div>
|
||||
<div className="modal-action w-full justify-center gap-3 mt-2">
|
||||
|
||||
@@ -23,8 +23,8 @@ export function PostActionOverlay({ revealState }: PostActionOverlayProps) {
|
||||
May your <span className="italic text-primary">soul</span> find
|
||||
solace,
|
||||
<br />
|
||||
just like your <span className="text-accent italic">unsaid</span>{" "}
|
||||
words did.
|
||||
just like your <span className="text-accent italic">unsaid</span>
|
||||
words did.
|
||||
</p>
|
||||
<div className="divider mx-auto w-24 text-center"></div>
|
||||
<button
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
|
||||
<p className="text-base-content/80 text-sm font-sans mt-4">
|
||||
You've carried these words long enough.
|
||||
<br />
|
||||
Send your letter now, and let the{" "}
|
||||
Send your letter now, and let the
|
||||
<span className="text-accent font-display">unsaid</span> finally
|
||||
find its home.
|
||||
</p>
|
||||
@@ -59,8 +59,8 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 uppercase tracking-widest text-base-content/30 font-sans">
|
||||
<p className="textarea-xs flex items-center justify-center">
|
||||
<EyeSlashIcon weight="duotone" size={18} className="mr-2" />{" "}
|
||||
Zero-Knowledge Share:
|
||||
<EyeSlashIcon weight="duotone" size={18} className="mr-2" />
|
||||
Zero-Knowledge Share:
|
||||
</p>
|
||||
<p className="textarea-xs font-mono text-center">
|
||||
The key never leaves your or the recipient's browser.
|
||||
@@ -68,7 +68,7 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="absolute bottom-0 z-1000 font-sans w-full">
|
||||
<div className="absolute bottom-0 md:right-5/11 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.`}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function FormField({
|
||||
<div className="form-control">
|
||||
<label
|
||||
htmlFor={registration.name}
|
||||
className="field-label font-display text-base-content/90 font-medium"
|
||||
className="field-label font-display text-neutral-content/80 font-medium"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { XCircleIcon } from "@phosphor-icons/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -15,13 +16,15 @@ export function Modal({
|
||||
"data-testid": testId,
|
||||
}: ModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
// render the modal top of all elements and position them to document viewport (/ the main wrapper).
|
||||
// NOTE: this is recommended approach for modals as it shouldn't be bound to the parent box.
|
||||
const mainContainer = document.querySelector("main") || document.body;
|
||||
return createPortal(
|
||||
<div
|
||||
data-testid={testId}
|
||||
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/textures/noise.gif')]"
|
||||
>
|
||||
<div className="modal-box relative bg-base-100/60 flex flex-col items-center text-center gap-6">
|
||||
<div className="modal-box border border-neutral/60 relative bg-base-100/60 flex flex-col items-center text-center gap-6">
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -35,6 +38,7 @@ export function Modal({
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
mainContainer,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,11 +98,11 @@ function PrivacySection() {
|
||||
</h1>
|
||||
<div className="flex flex-col items-center shrink-0 gap-8 max-w-11/12 w-220">
|
||||
<p className="text-xxs md:text-sm tracking-widester text-neutral-content/80 font-semibold uppercase mt-6">
|
||||
<span className="text-accent">Your letters.</span>{" "}
|
||||
<span className="text-accent">Your letters.</span>
|
||||
<span className="text-error">Nobody else's.</span>
|
||||
</p>
|
||||
<p className="text-sm md:text-lg text-neutral">
|
||||
When you write or upload anything{" "}
|
||||
When you write or upload anything
|
||||
<span className="font-hand">(yes, even images)</span> here, it gets
|
||||
encrypted in your browser before anything leaves your device. What
|
||||
reaches the server is something unreadable—and the server has no
|
||||
@@ -226,29 +226,33 @@ function SpecsSection() {
|
||||
</h1>
|
||||
<div className="flex flex-col items-center shrink-0 gap-6 max-w-11/12 w-220 mt-4 md:mt-12 text-neutral-content/80">
|
||||
<h2 className="text-xl md:text-3xl text-center mx-auto">
|
||||
<Logo type={"inline"} /> uses{" "}
|
||||
<span className="text-accent font-mono">Zero Knowledge</span>{" "}
|
||||
<span className="group ul-wavy font-mono text-success">
|
||||
<Logo type={"inline"} /> uses
|
||||
<span className="text-accent font-mono">Zero Knowledge</span>
|
||||
<button
|
||||
type="button"
|
||||
className="group ul-wavy font-mono text-success"
|
||||
>
|
||||
E
|
||||
<span className="hidden group-hover:inline group-focus-within:inline">
|
||||
<span className="hidden group-hover:inline group-focus-within:inline text-neutral">
|
||||
nd—
|
||||
</span>
|
||||
2
|
||||
<span className="hidden group-hover:inline group-focus-within:inline">
|
||||
<span className="hidden group-hover:inline group-focus-within:inline text-neutral">
|
||||
—
|
||||
</span>
|
||||
E
|
||||
<span className="hidden group-hover:inline group-focus-within:inline">
|
||||
<span className="hidden group-hover:inline group-focus-within:inline text-neutral">
|
||||
nd
|
||||
</span>
|
||||
<span className="hidden group-hover:inline group-focus-within:inline">
|
||||
<span>E</span>
|
||||
<span className="hidden group-hover:inline group-focus-within:inline">
|
||||
<span className="hidden group-hover:inline group-focus-within:inline text-neutral">
|
||||
ncryption
|
||||
</span>
|
||||
</span>
|
||||
</span>{" "}
|
||||
for your <span className="font-hand text-primary">letters</span>, with{" "}
|
||||
</button>
|
||||
for your
|
||||
<span className="font-hand text-primary">letters</span>, with
|
||||
<a
|
||||
href="https://hackernoon.com/what-the-heck-is-envelope-encryption-in-cloud-security"
|
||||
target="_blank"
|
||||
@@ -256,30 +260,32 @@ function SpecsSection() {
|
||||
className="font-mono text-neutral!"
|
||||
>
|
||||
Envelope Encryption
|
||||
</a>{" "}
|
||||
for the <span className="font-hand text-primary">keys</span>.
|
||||
</a>
|
||||
for the <span className="font-hand text-primary">keys</span>.
|
||||
</h2>
|
||||
<div className="text-sm md:text-xl leading-relaxed">
|
||||
This means, both the{" "}
|
||||
<span className="font-display text-info">encryption</span> and{" "}
|
||||
This means, both the
|
||||
<span className="font-display text-info">encryption</span> and
|
||||
<span className="font-display text-info">decryption</span> runs on
|
||||
your device, in your browser.
|
||||
<ul className="list-decimal ml-6 md:ml-10 list-outside text-neutral marker:text-primary/30 marker:font-mono marker:text-xs marker:md:text-base">
|
||||
<li>
|
||||
Every letter has a{" "}
|
||||
Every letter has a
|
||||
<span className="font-mono text-primary/50 font-bold">
|
||||
unique key
|
||||
</span>{" "}
|
||||
which is derived from your original password.
|
||||
</span>
|
||||
which is derived from your original password.
|
||||
</li>
|
||||
<li>
|
||||
Both the letter and the key are encrypted securely and sent to the
|
||||
server.
|
||||
</li>
|
||||
<li>
|
||||
Now, the server holds{" "}
|
||||
<span className="text-primary/50 font-bold">the envelope</span>,{" "}
|
||||
<span className="text-primary/50 font-bold">the seal</span> and{" "}
|
||||
Now, the server holds
|
||||
<span className="text-primary/50 font-bold">the envelope</span>
|
||||
,
|
||||
<span className="text-primary/50 font-bold">the seal</span>
|
||||
and
|
||||
<span className="text-primary/50 font-bold">
|
||||
another locked box
|
||||
</span>
|
||||
@@ -288,7 +294,7 @@ function SpecsSection() {
|
||||
</ul>
|
||||
But you—
|
||||
<span className="italic">only you</span>—hold the very thing
|
||||
that opens that box,{" "}
|
||||
that opens that box,
|
||||
<span className="font-mono text-accent">your password</span>.
|
||||
</div>
|
||||
<div className="text-xs md:text-lg text-right w-full flex items-center justify-end gap-4 leading-relaxed text-neutral-content/80">
|
||||
@@ -296,17 +302,18 @@ function SpecsSection() {
|
||||
Nothing on the server is readable without your actual password.
|
||||
<br />
|
||||
Even if someone were to breach in, all they'd find is encrypted
|
||||
noise and ain't no way they crackin' it.{" "}
|
||||
noise and ain't no way they crackin'
|
||||
<br />
|
||||
<a
|
||||
href="https://xkcd.com/538/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xxs md:text-sm text-neutral! font-hand"
|
||||
>
|
||||
(unless this happens)
|
||||
(unless this happens)
|
||||
</a>
|
||||
</span>
|
||||
<div className="w-18 h-18 flex shrink-0 items-center justify-end bg-success/20 rounded-full p-0 ">
|
||||
<div className="flex shrink-0 items-center justify-end bg-success/20 rounded-full p-4 ">
|
||||
<VaultIcon
|
||||
size={36}
|
||||
weight="duotone"
|
||||
@@ -333,10 +340,10 @@ function SpecsSection() {
|
||||
</Modal>
|
||||
|
||||
<p className="text-sm md:text-lg">
|
||||
Of course, this level of{" "}
|
||||
Of course, this level of
|
||||
<span className="text-success font-bold">privacy</span> comes with a
|
||||
catch. <span className="text-error font-bold">No password reset</span>{" "}
|
||||
for you.
|
||||
catch. <span className="text-error font-bold">No password reset</span>
|
||||
for you.
|
||||
</p>
|
||||
<p className="text-xs md:text-base alert alert-warning font-medium">
|
||||
<InfoIcon weight="duotone" /> Your original password is never stored
|
||||
@@ -357,16 +364,17 @@ function OSSSection() {
|
||||
}
|
||||
>
|
||||
<Logo type={"inline"} />
|
||||
is{" "}
|
||||
is
|
||||
<span className="line-through decoration-6 text-neutral-content/50 decoration-error">
|
||||
private
|
||||
<span className="absolute -translate-y-2 -translate-x-42 md:-translate-x-72 font-hand text-xs md:text-xl opacity-70 rotate-8 tracking-normal inline-flex items-center not-italic w-48 md:w-100 flex-wrap">
|
||||
only for
|
||||
<span className="text-primary"> your letters </span>{" "}
|
||||
<span className="text-primary"> your letters </span>
|
||||
<SmileyIcon weight="duotone" className="text-primary" />
|
||||
<ArrowArcLeftIcon className="text-accent inline rotate-45 -translate-y" />
|
||||
</span>
|
||||
</span>{" "}
|
||||
</span>
|
||||
|
||||
<span className="text-success -rotate-3">open source !</span>
|
||||
</h1>
|
||||
<div className="flex flex-col items-center shrink-0 max-w-11/12 w-220 gap-4 p-4 md:p-6 text-neutral-content/80">
|
||||
@@ -378,8 +386,9 @@ function OSSSection() {
|
||||
don't have to take my word at it.
|
||||
</p>
|
||||
<p className="text-sm md:text-lg">
|
||||
You can also{" "}
|
||||
<span className="uppercase font-mono text-primary">Self-host</span>{" "}
|
||||
You can also
|
||||
<span className="uppercase font-mono text-primary">Self-host</span>
|
||||
|
||||
<Logo type={"inline"} /> in just 4 steps.
|
||||
</p>
|
||||
<div className="mockup-code w-110 max-w-11/12 text-xs">
|
||||
@@ -408,7 +417,7 @@ function OSSSection() {
|
||||
</a>
|
||||
.
|
||||
<p className="text-xs md:text-base opacity-70">
|
||||
Found something to report or request?{" "}
|
||||
Found something to report or request?
|
||||
<a
|
||||
href="https://git.ramvignesh.dev/me/pi-ku/issues"
|
||||
target="_blank"
|
||||
@@ -449,16 +458,16 @@ function OSSSection() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
DaisyUI
|
||||
</a>{" "}
|
||||
·{" "}
|
||||
</a>
|
||||
·
|
||||
<a
|
||||
href="http://fabricjs.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Fabric.js
|
||||
</a>{" "}
|
||||
·{" "}
|
||||
</a>
|
||||
·
|
||||
<a
|
||||
href="https://phosphoricons.com"
|
||||
target="_blank"
|
||||
@@ -533,7 +542,7 @@ function StorySection() {
|
||||
postscript; a note written after the letter is signed.
|
||||
<br />
|
||||
<blockquote className="text-primary/50 italic mt-2 ml-2 border-l-primary/20 leading-none border-l">
|
||||
"the most honest thing was always in the{" "}
|
||||
"the most honest thing was always in the
|
||||
<span className="font-ink">பி. கு.</span>"
|
||||
</blockquote>
|
||||
</li>
|
||||
@@ -559,7 +568,7 @@ function StorySection() {
|
||||
<Logo type={"inline"} /> is an abbreviated transliteration of the
|
||||
<span className="font-ink text-accent"> தமிழ் </span>
|
||||
<span className="italic text-xs md:text-base">(Tamil) </span>word
|
||||
for{" "}
|
||||
for
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
@@ -583,9 +592,10 @@ function StorySection() {
|
||||
cript
|
||||
</span>
|
||||
.
|
||||
</button>{" "}
|
||||
—the thing you add after you've already signed your name, what
|
||||
you write when you thought you were finished, but weren't.
|
||||
</button>
|
||||
—the thing you add after you've already signed your
|
||||
name, what you write when you thought you were finished, but
|
||||
weren't.
|
||||
</p>
|
||||
<p>
|
||||
<span className={"font-medium text-primary"}>
|
||||
@@ -594,7 +604,7 @@ function StorySection() {
|
||||
<br />
|
||||
It sits in drafts , in half-written notes, in the pause before we
|
||||
change the subject. <br />
|
||||
Those words{" "}
|
||||
Those words
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
@@ -602,8 +612,8 @@ function StorySection() {
|
||||
}
|
||||
>
|
||||
don't just disappear. They
|
||||
</button>{" "}
|
||||
stay{" "}
|
||||
</button>
|
||||
stay
|
||||
<span className={"text-primary font-hand font-extrabold"}>
|
||||
unsaid
|
||||
</span>
|
||||
@@ -640,10 +650,9 @@ function ForWhoSection() {
|
||||
<Logo type={"mono"} /> wasn't built for one kind of person, but a
|
||||
particular kind of feeling—
|
||||
<span className="italic font-serif text-stone-900">
|
||||
{" "}
|
||||
the one that lingers very quietly
|
||||
</span>{" "}
|
||||
—fragile, yet never breaks.
|
||||
the one that lingers very quietly
|
||||
</span>
|
||||
—fragile, yet never breaks.
|
||||
</p>
|
||||
|
||||
<div className="pt-8 flex items-center gap-4">
|
||||
@@ -681,8 +690,8 @@ function ArchetypesSection() {
|
||||
open
|
||||
>
|
||||
<summary className="collapse-title md:text-xl leading-tight font-hand flex items-center gap-4">
|
||||
<GhostIcon weight="duotone" className="text-accent" size={32} />{" "}
|
||||
To someone you can't reach anymore.
|
||||
<GhostIcon weight="duotone" className="text-accent" size={32} />
|
||||
To someone you can't reach anymore.
|
||||
</summary>
|
||||
<div className="collapse-content text-sm md:text-lg flex flex-col gap-4">
|
||||
<p>
|
||||
@@ -712,8 +721,8 @@ function ArchetypesSection() {
|
||||
weight="duotone"
|
||||
className="text-accent"
|
||||
size={32}
|
||||
/>{" "}
|
||||
To someone who's still here.
|
||||
/>
|
||||
To someone who's still here.
|
||||
</summary>
|
||||
<div className="collapse-content text-sm md:text-lg flex flex-col gap-4">
|
||||
<p>
|
||||
@@ -742,7 +751,8 @@ function ArchetypesSection() {
|
||||
weight="duotone"
|
||||
className="text-accent"
|
||||
size={14}
|
||||
/>{" "}
|
||||
/>
|
||||
|
||||
<PersonArmsSpreadIcon
|
||||
weight="duotone"
|
||||
className="text-accent"
|
||||
@@ -775,8 +785,8 @@ function ArchetypesSection() {
|
||||
name="my-accordion-det-1"
|
||||
>
|
||||
<summary className="collapse-title text-lg md:text-xl leading-tight font-hand flex items-center gap-4">
|
||||
<SparkleIcon weight="duotone" className="text-accent" size={32} />{" "}
|
||||
For liberation.
|
||||
<SparkleIcon weight="duotone" className="text-accent" size={32} />
|
||||
For liberation.
|
||||
</summary>
|
||||
<div className="collapse-content text-sm md:text-lg flex flex-col gap-4">
|
||||
<p>
|
||||
@@ -864,16 +874,16 @@ function AttributionSection() {
|
||||
<p>
|
||||
<Logo type={"inline"} /> took a while to exist.
|
||||
<br />
|
||||
This started as a{" "}
|
||||
This started as a
|
||||
<a
|
||||
href="https://cs50.harvard.edu/web/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
CS50W
|
||||
</a>{" "}
|
||||
capstone—one I kept postponing until I ran out of excuses.
|
||||
When I sat down to build it, it felt heavier than a typical
|
||||
</a>
|
||||
capstone—one I kept postponing until I ran out of
|
||||
excuses. When I sat down to build it, it felt heavier than a typical
|
||||
assignment—not just because things were difficult. It had to
|
||||
be something that outlasted the grade. I wanted to make this one
|
||||
count more than anything else I'd ever made. Something as close to
|
||||
@@ -885,19 +895,19 @@ function AttributionSection() {
|
||||
Of course, frustrations, id-exisi crises, crept in from time to
|
||||
time. But <Logo type="inline" /> helped me re-kindle the love for
|
||||
the odd hours spent obsessing over the tiniest UX decisions and
|
||||
endlessly polishing the UI{" "}
|
||||
endlessly polishing the UI
|
||||
<span className="font-hand">
|
||||
(only if I could've just made my mind up on one design system
|
||||
sooner, instead of paddling in a sea of muses, muses everywhere)
|
||||
</span>
|
||||
. I know I've shared the nuts and bolts of <Logo type={"inline"} />{" "}
|
||||
here—the core philosophies, how it all works—but the
|
||||
heart of it is really something you have to find by exploring it
|
||||
. I know I've shared the nuts and bolts of <Logo type={"inline"} />
|
||||
here—the core philosophies, how it all works—but
|
||||
the heart of it is really something you have to find by exploring it
|
||||
yourself.
|
||||
</p>
|
||||
<p>
|
||||
The "why" behind all of this didn't just appear out of nowhere. For
|
||||
a while, I kept coming back to{" "}
|
||||
a while, I kept coming back to
|
||||
<span
|
||||
role="tooltip"
|
||||
className="cursor-default ul-wavy text-accent"
|
||||
@@ -918,27 +928,29 @@ function AttributionSection() {
|
||||
onMouseLeave={() => setHover((h) => ({ ...h, visible: false }))}
|
||||
>
|
||||
Saajan
|
||||
</span>{" "}
|
||||
from{" "}
|
||||
</span>
|
||||
from
|
||||
<a
|
||||
href="https://www.themoviedb.org/movie/191714-the-lunchbox"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
The Lunchbox
|
||||
</a>{" "}
|
||||
—brought to life with such subtle brilliance by{" "}
|
||||
</a>
|
||||
—brought to life with such subtle brilliance by
|
||||
<a
|
||||
className="text-accent!"
|
||||
href="https://www.themoviedb.org/person/76793-irrfan-khan"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Irrfan Khan
|
||||
</a>{" "}
|
||||
</a>
|
||||
|
||||
<PeaceIcon weight="duotone" className="inline text-accent" />
|
||||
—the quiet emotional weight he carries through a lonely and
|
||||
mechanized life, right up until those letters arrive and something
|
||||
inside him finally loosens. The ending feels like a deep sigh of{" "}
|
||||
inside him finally loosens. The ending feels like a deep sigh
|
||||
of
|
||||
<span className="font-hand font-bold text-accent">
|
||||
"it is what it is"
|
||||
</span>
|
||||
@@ -947,15 +959,15 @@ function AttributionSection() {
|
||||
that a lot.
|
||||
</p>
|
||||
<p>
|
||||
There's a lot that goes{" "}
|
||||
There's a lot that goes
|
||||
<span className={"text-primary font-hand text-lg md:text-xl"}>
|
||||
unsaid
|
||||
</span>{" "}
|
||||
these days. Not for a lack of feeling, not for the lack of time, but
|
||||
because the ways we reach each other have quietly changed. We're
|
||||
always reachable <span className="italic">digitally,</span> yet
|
||||
somehow the things that actually matter most end up staying
|
||||
inside—a trapped one at that.
|
||||
</span>
|
||||
these days. Not for a lack of feeling, not for the lack of
|
||||
time, but because the ways we reach each other have quietly changed.
|
||||
We're always reachable <span className="italic">digitally,</span>
|
||||
yet somehow the things that actually matter most end up
|
||||
staying inside—a trapped one at that.
|
||||
<br />
|
||||
Maybe writing can/will help. Maybe putting words somewhere
|
||||
deliberate makes them feel less like a weight you're carrying alone.
|
||||
@@ -973,7 +985,7 @@ function AttributionSection() {
|
||||
</p>
|
||||
<p className="text-xs md:text-sm opacity-75 font-mono">
|
||||
P.S. And just so we're clear—I wrote every word of this
|
||||
myself—as I continue to back{" "}
|
||||
myself—as I continue to back
|
||||
<a
|
||||
href="https://em-dash-appreciation.org/"
|
||||
target="_blank"
|
||||
@@ -981,7 +993,7 @@ function AttributionSection() {
|
||||
>
|
||||
Em DASH
|
||||
</a>
|
||||
. Why should AI get to have all the fun with 'em em dashes?{" "}
|
||||
. Why should AI get to have all the fun with 'em em dashes?
|
||||
<span className="font-hand">(get it?)</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -990,10 +1002,10 @@ function AttributionSection() {
|
||||
weight="duotone"
|
||||
size={48}
|
||||
className="rotate-180 text-neutral-content"
|
||||
/>{" "}
|
||||
I think we forget things if there is nobody to tell them.
|
||||
/>
|
||||
I think we forget things if there is nobody to tell them.
|
||||
<span className="block mt-2 text-sm not-italic text-base-200/70 w-full text-right">
|
||||
~ Saajan Fernandes,{" "}
|
||||
~ Saajan Fernandes,
|
||||
<span className="italic underline decoration-dotted">
|
||||
The Lunchbox
|
||||
</span>
|
||||
|
||||
@@ -59,7 +59,8 @@ export default function Activate() {
|
||||
You're in.
|
||||
</h2>
|
||||
<p className="opacity-70 leading-relaxed">
|
||||
Welcome to <Logo scale={1} />
|
||||
Welcome to
|
||||
<Logo type="inline" />
|
||||
<br />
|
||||
Just one more step and you can start writing timeless letters.
|
||||
</p>
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("Drawer Page", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("passkey-modal-title")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
|
||||
expect(screen.getByTestId("passkey-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the welcome letter when firstTime state is present", () => {
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function Drawer() {
|
||||
Personal Archive
|
||||
</div>
|
||||
<div className="mt-6 font-sans text-sm text-base-content flex items-center justify-center gap-2 opacity-60 hover:opacity-100 transition-opacity">
|
||||
Welcome Back{" "}
|
||||
Welcome Back
|
||||
<span className="font-semibold text-primary">{user.full_name}</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -180,7 +180,7 @@ export default function Drawer() {
|
||||
weight="duotone"
|
||||
className="text-primary/30 transition-all duration-300 group-hover:text-primary"
|
||||
/>
|
||||
Write something{" "}
|
||||
Write something
|
||||
<span className="relative inline-flex">
|
||||
<span className="transition-opacity duration-500 opacity-80 group-hover:opacity-0">
|
||||
. . . . . .
|
||||
|
||||
@@ -97,25 +97,22 @@ describe("Editor Page", () => {
|
||||
fireEvent.click(sealBtn);
|
||||
|
||||
// Click Vault to show confirm modal
|
||||
const vaultBtn = screen.getByRole("button", { name: /vault/i });
|
||||
const vaultBtn = screen.getByTestId("vault-trigger-btn");
|
||||
fireEvent.click(vaultBtn);
|
||||
|
||||
// Set date and submit vault form
|
||||
const dateInput = container.querySelector('input[name="vault-date"]');
|
||||
const dateInput = document.body.querySelector('input[name="vault-date"]');
|
||||
if (!dateInput) throw new Error("Date input not found");
|
||||
fireEvent.change(dateInput, { target: { value: "2026-12-31" } });
|
||||
|
||||
const confirmVaultBtn = container.querySelector(
|
||||
'button[form="vault-form"]',
|
||||
);
|
||||
if (!confirmVaultBtn) throw new Error("Confirm vault button not found");
|
||||
const confirmVaultBtn = screen.getByTestId("vault-confirm-btn");
|
||||
fireEvent.click(confirmVaultBtn);
|
||||
|
||||
// Wait for save to complete and check readOnly
|
||||
expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument();
|
||||
|
||||
expect(canvas.getAttribute("data-readonly")).toBe("true");
|
||||
expect(screen.getByLabelText(/recipient/i)).toBeDisabled();
|
||||
expect(screen.getByTestId("recipient-input")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should set canvas to readOnly when status is SEALED", async () => {
|
||||
@@ -135,7 +132,7 @@ describe("Editor Page", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/write/test-id"]}>
|
||||
<Routes>
|
||||
<Route path="/write/:public_id" element={<Editor />} />
|
||||
@@ -149,19 +146,17 @@ describe("Editor Page", () => {
|
||||
|
||||
const canvas = screen.getByTestId("canvas");
|
||||
|
||||
const toolbar = container.querySelector("#writer-toolbar");
|
||||
const sealBtn = toolbar?.querySelector(".btn-primary");
|
||||
if (!sealBtn) throw new Error("Seal button not found");
|
||||
const sealBtn = screen.getByTestId("seal-trigger-btn");
|
||||
fireEvent.click(sealBtn);
|
||||
|
||||
// The secondary seal button appears (it has btn-accent class)
|
||||
const secondarySealBtn = container.querySelector(".btn-accent");
|
||||
const secondarySealBtn = screen.getByTestId("seal-confirm-btn");
|
||||
if (!secondarySealBtn) throw new Error("Secondary seal button not found");
|
||||
fireEvent.click(secondarySealBtn);
|
||||
|
||||
expect(await screen.findByTestId("save-success-toast")).toBeInTheDocument();
|
||||
|
||||
expect(canvas.getAttribute("data-readonly")).toBe("true");
|
||||
expect(screen.getByLabelText(/recipient/i)).toBeDisabled();
|
||||
expect(screen.getByTestId("recipient-input")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function Home() {
|
||||
}}
|
||||
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
||||
>
|
||||
pen down your unsaid words into{" "}
|
||||
pen down your unsaid words into
|
||||
<span className="font-display text-primary font-extralight">
|
||||
letters
|
||||
</span>
|
||||
@@ -171,11 +171,11 @@ export default function Home() {
|
||||
}}
|
||||
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
||||
>
|
||||
seal it{" "}
|
||||
seal it
|
||||
<span className="text-success font-mono tracking-tighter font-extrabold">
|
||||
secure
|
||||
</span>{" "}
|
||||
and{" "}
|
||||
</span>
|
||||
and
|
||||
<span className="text-info font-mono tracking-tighter italic">
|
||||
private
|
||||
</span>
|
||||
@@ -197,7 +197,7 @@ export default function Home() {
|
||||
}}
|
||||
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
||||
>
|
||||
send it to{" "}
|
||||
send it to
|
||||
<motion.span
|
||||
className="font-display text-accent"
|
||||
style={{
|
||||
@@ -225,8 +225,7 @@ export default function Home() {
|
||||
),
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
or{" "}
|
||||
or
|
||||
</motion.span>
|
||||
<span className="font-display text-success">
|
||||
yourself in the future
|
||||
@@ -250,8 +249,8 @@ export default function Home() {
|
||||
}}
|
||||
className="absolute text-4xl md:text-6xl text-center px-10 leading-tight"
|
||||
>
|
||||
and even <span className="font-display text-error">burn it</span>{" "}
|
||||
to release the burden.
|
||||
and even <span className="font-display text-error">burn it</span>
|
||||
to release the burden.
|
||||
</motion.h2>
|
||||
{/* Outro */}
|
||||
<motion.h2
|
||||
|
||||
@@ -27,9 +27,9 @@ describe("Login Page", () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), "test@example.com");
|
||||
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
||||
await userEvent.type(screen.getByTestId("email-input"), "test@example.com");
|
||||
await userEvent.type(screen.getByTestId("password-input"), "password123");
|
||||
await userEvent.click(screen.getByTestId("login-submit-btn"));
|
||||
|
||||
expect(await screen.findByTestId("login-error-message")).toHaveTextContent(
|
||||
/technical issues/i,
|
||||
@@ -87,9 +87,9 @@ describe("Login Page", () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), "test@example.com");
|
||||
await userEvent.type(screen.getByLabelText(/password/i), "password123");
|
||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
||||
await userEvent.type(screen.getByTestId("email-input"), "test@example.com");
|
||||
await userEvent.type(screen.getByTestId("password-input"), "password123");
|
||||
await userEvent.click(screen.getByTestId("login-submit-btn"));
|
||||
|
||||
const expectedTestId =
|
||||
nextRoute.toLowerCase() === "drawer" ? "drawer-page" : "reader-page";
|
||||
|
||||
@@ -120,28 +120,29 @@ export default function Login() {
|
||||
type="submit"
|
||||
name="login"
|
||||
disabled={isLoading}
|
||||
aria-label="Sign In"
|
||||
data-testid="login-submit-btn"
|
||||
className="btn btn-primary w-full shadow-lg"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="loading loading-spinner loading-sm" />
|
||||
) : (
|
||||
"Sign In"
|
||||
"Continue"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm font-medium text-base-content/70">
|
||||
Don't have an account?{" "}
|
||||
<div className="divider text-neutral my-0">or</div>
|
||||
<div className="text-center text-sm font-medium text-neutral">
|
||||
New to <Logo type="inline" />
|
||||
?
|
||||
<button
|
||||
type="button"
|
||||
name="register"
|
||||
onClick={() => navigate(ROUTES.ONBOARD)}
|
||||
className="link link-primary no-underline hover:underline font-bold"
|
||||
className="link link-primary"
|
||||
>
|
||||
Register
|
||||
Start here
|
||||
</button>
|
||||
.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,8 @@ export default function Register() {
|
||||
<div className="glass-card w-full max-w-sm p-2 transition-all duration-500 hover:shadow-2xl fade-zoom">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="card-body gap-4">
|
||||
<div className="card-title font-display text-2xl justify-center text-primary/80 tracking-tight whitespace-nowrap">
|
||||
Create a <Logo type="logo" scale={0.7} /> Account
|
||||
Create a<Logo type="logo" scale={0.7} />
|
||||
Account
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
@@ -143,9 +144,9 @@ export default function Register() {
|
||||
<InfoIcon size={20} weight="duotone" className="mt-0.5 shrink-0" />
|
||||
<p className="text-sm font-semibold">
|
||||
Choose a password you won't forget. <br />
|
||||
Just like life,{" "}
|
||||
<span className="underline decoration-2">there is no reset</span>{" "}
|
||||
here. If you lose it, your letters cannot be recovered.
|
||||
Just like life,
|
||||
<span className="underline decoration-2">there is no reset</span>
|
||||
here. If you lose it, your letters cannot be recovered.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -160,10 +161,23 @@ export default function Register() {
|
||||
{isLoading ? (
|
||||
<span className="loading loading-spinner loading-sm" />
|
||||
) : (
|
||||
"Register"
|
||||
"Begin"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="divider text-neutral my-0">or</div>
|
||||
<div className="text-center text-sm font-medium text-neutral">
|
||||
Been here before?
|
||||
<button
|
||||
type="button"
|
||||
name="register"
|
||||
onClick={() => navigate(ROUTES.LOGIN)}
|
||||
className="link link-primary"
|
||||
>
|
||||
Continue where you left off
|
||||
</button>
|
||||
.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +23,8 @@ export default function VerifyEmail() {
|
||||
Check Your Mailbox
|
||||
</h2>
|
||||
<p className="text-sm opacity-80 leading-relaxed font-sans mt-6">
|
||||
You're one train away from starting your <Logo scale={0.8} />{" "}
|
||||
journey.
|
||||
You're one train away from starting your <Logo scale={0.8} />
|
||||
journey.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user