From 488ded6b5f89355dc4316c3a119477d6c3842525 Mon Sep 17 00:00:00 2001 From: me Date: Tue, 12 May 2026 07:45:10 +0530 Subject: [PATCH 1/4] feat: add PasswordInput component with visibility toggle --- .../src/components/drawer/PasskeyModal.tsx | 36 +- frontend/src/components/ui/FormField.tsx | 38 +- frontend/src/components/ui/PasswordInput.tsx | 37 + frontend/src/pages/About.tsx | 1922 ++++++++--------- 4 files changed, 1040 insertions(+), 993 deletions(-) create mode 100644 frontend/src/components/ui/PasswordInput.tsx diff --git a/frontend/src/components/drawer/PasskeyModal.tsx b/frontend/src/components/drawer/PasskeyModal.tsx index fea647f..f9cd694 100644 --- a/frontend/src/components/drawer/PasskeyModal.tsx +++ b/frontend/src/components/drawer/PasskeyModal.tsx @@ -1,6 +1,7 @@ import { HourglassSimpleMediumIcon } from "@phosphor-icons/react"; import { useAuth } from "../../hooks/useAuth"; import { Modal } from "../ui/Modal"; +import { PasswordInput } from "../ui/PasswordInput"; export function PasskeyModal() { const { unlock } = useAuth(); @@ -27,7 +28,7 @@ export function PasskeyModal() {

) => { e.preventDefault(); const formData = new FormData(e.currentTarget); @@ -36,22 +37,23 @@ export function PasskeyModal() { await unlock(password); }} > - -
- +
+ + +
+
diff --git a/frontend/src/components/ui/FormField.tsx b/frontend/src/components/ui/FormField.tsx index 3204cb5..56811db 100644 --- a/frontend/src/components/ui/FormField.tsx +++ b/frontend/src/components/ui/FormField.tsx @@ -1,4 +1,5 @@ import type { UseFormRegisterReturn } from "react-hook-form"; +import { PasswordInput } from "./PasswordInput"; interface FormFieldProps { label: string; @@ -20,25 +21,36 @@ export default function FormField({ "data-testid": testId, }: FormFieldProps) { return ( -
+
- - {error &&

{error}

} + {type === "password" ? ( + + ) : ( + + )} + {error &&

{error}

}
); } diff --git a/frontend/src/components/ui/PasswordInput.tsx b/frontend/src/components/ui/PasswordInput.tsx new file mode 100644 index 0000000..4e35070 --- /dev/null +++ b/frontend/src/components/ui/PasswordInput.tsx @@ -0,0 +1,37 @@ +import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react"; +import { useState } from "react"; + +interface PasswordInputProps + extends React.InputHTMLAttributes { + error?: boolean; +} + +export function PasswordInput({ + className, + error, + ...props +}: PasswordInputProps) { + const [showPassword, setShowPassword] = useState(false); + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index caf707f..18c08b2 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx @@ -1,25 +1,25 @@ import { - ArrowArcLeftIcon, - ArrowBendDownLeftIcon, - ArrowBendDownRightIcon, - ArrowRightIcon, - CaretUpIcon, - DetectiveIcon, - FlowerTulipIcon, - GhostIcon, - GithubLogoIcon, - InfoIcon, - LockKeyOpenIcon, - LockLaminatedIcon, - PasswordIcon, - PeaceIcon, - PersonArmsSpreadIcon, - PersonIcon, - QuotesIcon, - ScrollIcon, - SmileyIcon, - SparkleIcon, - VaultIcon, + ArrowArcLeftIcon, + ArrowBendDownLeftIcon, + ArrowBendDownRightIcon, + ArrowRightIcon, + CaretUpIcon, + DetectiveIcon, + FlowerTulipIcon, + GhostIcon, + GithubLogoIcon, + InfoIcon, + LockKeyOpenIcon, + LockLaminatedIcon, + PasswordIcon, + PeaceIcon, + PersonArmsSpreadIcon, + PersonIcon, + QuotesIcon, + ScrollIcon, + SmileyIcon, + SparkleIcon, + VaultIcon, } from "@phosphor-icons/react"; import { ReactLenis } from "lenis/react"; import { AnimatePresence, motion, useScroll, useTransform } from "motion/react"; @@ -37,1000 +37,996 @@ import "@fontsource/architects-daughter/index.css"; import { useNavigate } from "react-router-dom"; function HorizontalScroll({ children }: { children: React.ReactNode }) { - const ref = useRef(null); - const { scrollYProgress } = useScroll({ target: ref }); - const x = useTransform(scrollYProgress, [0, 1], ["0%", "-50%"]); + const ref = useRef(null); + const { scrollYProgress } = useScroll({ target: ref }); + const x = useTransform(scrollYProgress, [0, 1], ["0%", "-50%"]); - return ( -
-
- - {children} - -
-
- ); + return ( +
+
+ + {children} + +
+
+ ); } export default function About() { - useEffect(() => { - window.scrollTo(0, 0); - }, []); - return ( - -
- + useEffect(() => { + window.scrollTo(0, 0); + }, []); + return ( + +
+ - - - - + + + + - + - - - - + + + + - -
-
- ); + +
+
+ ); } function PrivacySection() { - return ( -
-

- The   Promise - - privacy - - +

+ The   Promise + + privacy + + +

+
+

+ Your letters.  + Nobody else's. +

+

+ When you write or upload anything  + (yes, even images) here, it gets + encrypted in your browser before anything leaves your device. What + reaches the server is something unreadable—and the server has no + way to change that, because the key never left you. +

+
+
+
+
+

+ you see +

+ -

-
-

- Your letters.  - Nobody else's. +

+ Your Password +

+

+
+ B@z1ng4A

-

- When you write or upload anything  - (yes, even images) here, it gets - encrypted in your browser before anything leaves your device. What - reaches the server is something unreadable—and the server has no - way to change that, because the key never left you. -

-
-
-
-
-

- you see -

- -

- Your Password -

-

-
- B@z1ng4A -

-
-
- -
-
- -

- Your Letter -

-
-

Hello friend,

-

I've never told anyone this...

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut - semper, justo eget vehicula vestibulum, enim enim suscipit - lectus, et sagittis nibh risus vel metus. Quisque eu ornare - ante, et gravida mauris. Vivamus massa justo, sagittis non - viverra sed, sodales non nisi. Nunc semper, massa a aliquet - dictum, enim nisi malesuada orci, et elementum lectus turpis - et velit. Nam vel felis vitae tortor dignissim malesuada. - Nam suscipit, justo eu elementum pulvinar, magna sem tempor - ex, vitae iaculis tellus odio non nisl. Duis dolor orci, - viverra ut finibus sed, aliquet vitae tortor. Proin sodales - ipsum ac ipsum hendrerit tempus. Nunc nec nibh nibh. Aenean - consequat auctor posuere. Integer sed magna volutpat, - efficitur nisl ut, dignissim neque. Vestibulum convallis nec - dui a euismod. Duis dignissim magna in mattis pulvinar. Sed - blandit nibh quis arcu ornare, sit amet fermentum nisi - rhoncus. -

-
-
-
-
-
-
-
-

- server see -

- -

- Your Password -

-

- 9e54d05f88bdd67a675b03bf1cd0a1647e2109b5aa18185ff6a9ba4c6959a19d -

-
-
- -
-
- -

- Your Letter -

-
-

- SZ0Mq9M9sCZsdDB8HGjk7JfWG56Kaot8Lgma74MCusDUYibUGoR7VviWgvc341pvFV9/IAyot9KtlDvwIX1ZmUw9Oh340JMaajRQ7iNgVjHgAwmJAr2cLbReNqlF6xzaf3mIYkiK9BXNQekk2h/9XufklsqoIXpaK1re7xWQ8mdddzy6z4EQFVH/Ev3np5ERW/ss7Z1kqYWUnANK7olWNL/7GgZmhU+L29rgbR52kcH9fng7gnEI3KEuISYExYCg81G1VaJYspkW3A4qwcet+jXdgmbKvkux5qNw6gyNi9d/YqKV7OUNrmoH190rHdJ5A7HOIv3/SvPhb3Zm4sNF5PcMxmhM0+T9m5PejV1GhV9bMBHbbgacay7hZJU3O0+q+7fBAE/+pqfvZdv78lLDFSdtHAXUpYOvHPrI5BNNwuS3T+FK1zjurLnUPThlOSYRICoZSUcxVswXz897PoRmFNNvbal0dpKUmCFrBwV5c/W3d1+iZor5msbm/JxpbNtys59e0StSTwHKsxvxm/rTuUAxWSOmzt13MDBxxd2zyVnX8rtQ7mEjMJ8IHHpvhKjONoa2S11VBJY68Ee1vNrw7htu+wajvmXhHAyfh1lYql8pu8VvPUG7leEQ9I0pMY35Y/C1cYCBLkDT5zf8NeZFtbp0BNgHd+QDVSFH+GSnvTskU2BCio3YE+zE6cDhvLUOMy3e5RAtPqsi5VzpEUcdCwph+Z+1pFlTxiEZ62i4wNpqw2lhS3b/E9ifJgnncSgRHLtfw/VxHZCRc4tBQ24xSZ507lSlQch+5lQeO7rx2htgd2D7aGNx/UN/xmeuEd4a28AxNOVS3uYh3wTDh8CSXyBRCRPxrANOV1ZBojdfK+v5fOJNPgDn3r5/pG80L3FTkecRB0zFuKNG8jIzi5ADx9k4SlhRNo17gPl2if8gRA6tzTae4kbzieG+woxhUWj/qvXg0MQmg59VTK2HHS34exdKDP9a561svlw+lJ2AtM1EL9srJk8i3kiyEPUeIlaLl3AfgbbSuC2RhlzFFAYuQ06rbsSvEoe4rrYeMXxL9jwVsXX0xrp8H25mOJu3ahn5pFYzADMSGf4L11H1vDArpefj/lW+8zcmogxxBktYYNF/qU4v+9367hp4MEn/84tQPpmb47TL+XpVnl9tQ3r9OfOaW3zX7NkWZbqoX7OgdgHOtTLP/euQujSs2MAzMO4BmbuCS7pR/GTZwDqF1sXiWAkunjo2qpKHieqlvSVmtwEhh6wsNwYTKEkddmTqvKSx0fHRvs3D9lMGJfg7wLSz/3Otx3G65tk9l/3B3r87qQTvbqXmcfnFdEIaR8mO/yMyCKnxtJkJb3lEzNUOrvnSxwL7Gyn54TLTWA== -

-
-
-
-
-
-
+
+
+ +
+
+ +

+ Your Letter +

+
+

Hello friend,

+

I've never told anyone this...

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut + semper, justo eget vehicula vestibulum, enim enim suscipit + lectus, et sagittis nibh risus vel metus. Quisque eu ornare + ante, et gravida mauris. Vivamus massa justo, sagittis non + viverra sed, sodales non nisi. Nunc semper, massa a aliquet + dictum, enim nisi malesuada orci, et elementum lectus turpis + et velit. Nam vel felis vitae tortor dignissim malesuada. + Nam suscipit, justo eu elementum pulvinar, magna sem tempor + ex, vitae iaculis tellus odio non nisl. Duis dolor orci, + viverra ut finibus sed, aliquet vitae tortor. Proin sodales + ipsum ac ipsum hendrerit tempus. Nunc nec nibh nibh. Aenean + consequat auctor posuere. Integer sed magna volutpat, + efficitur nisl ut, dignissim neque. Vestibulum convallis nec + dui a euismod. Duis dignissim magna in mattis pulvinar. Sed + blandit nibh quis arcu ornare, sit amet fermentum nisi + rhoncus. +

+
+
-
- ); + +
+
+
+

+ server see +

+ +

+ Your Password +

+

+ 9e54d05f88bdd67a675b03bf1cd0a1647e2109b5aa18185ff6a9ba4c6959a19d +

+
+
+ +
+
+ +

+ Your Letter +

+
+

+ SZ0Mq9M9sCZsdDB8HGjk7JfWG56Kaot8Lgma74MCusDUYibUGoR7VviWgvc341pvFV9/IAyot9KtlDvwIX1ZmUw9Oh340JMaajRQ7iNgVjHgAwmJAr2cLbReNqlF6xzaf3mIYkiK9BXNQekk2h/9XufklsqoIXpaK1re7xWQ8mdddzy6z4EQFVH/Ev3np5ERW/ss7Z1kqYWUnANK7olWNL/7GgZmhU+L29rgbR52kcH9fng7gnEI3KEuISYExYCg81G1VaJYspkW3A4qwcet+jXdgmbKvkux5qNw6gyNi9d/YqKV7OUNrmoH190rHdJ5A7HOIv3/SvPhb3Zm4sNF5PcMxmhM0+T9m5PejV1GhV9bMBHbbgacay7hZJU3O0+q+7fBAE/+pqfvZdv78lLDFSdtHAXUpYOvHPrI5BNNwuS3T+FK1zjurLnUPThlOSYRICoZSUcxVswXz897PoRmFNNvbal0dpKUmCFrBwV5c/W3d1+iZor5msbm/JxpbNtys59e0StSTwHKsxvxm/rTuUAxWSOmzt13MDBxxd2zyVnX8rtQ7mEjMJ8IHHpvhKjONoa2S11VBJY68Ee1vNrw7htu+wajvmXhHAyfh1lYql8pu8VvPUG7leEQ9I0pMY35Y/C1cYCBLkDT5zf8NeZFtbp0BNgHd+QDVSFH+GSnvTskU2BCio3YE+zE6cDhvLUOMy3e5RAtPqsi5VzpEUcdCwph+Z+1pFlTxiEZ62i4wNpqw2lhS3b/E9ifJgnncSgRHLtfw/VxHZCRc4tBQ24xSZ507lSlQch+5lQeO7rx2htgd2D7aGNx/UN/xmeuEd4a28AxNOVS3uYh3wTDh8CSXyBRCRPxrANOV1ZBojdfK+v5fOJNPgDn3r5/pG80L3FTkecRB0zFuKNG8jIzi5ADx9k4SlhRNo17gPl2if8gRA6tzTae4kbzieG+woxhUWj/qvXg0MQmg59VTK2HHS34exdKDP9a561svlw+lJ2AtM1EL9srJk8i3kiyEPUeIlaLl3AfgbbSuC2RhlzFFAYuQ06rbsSvEoe4rrYeMXxL9jwVsXX0xrp8H25mOJu3ahn5pFYzADMSGf4L11H1vDArpefj/lW+8zcmogxxBktYYNF/qU4v+9367hp4MEn/84tQPpmb47TL+XpVnl9tQ3r9OfOaW3zX7NkWZbqoX7OgdgHOtTLP/euQujSs2MAzMO4BmbuCS7pR/GTZwDqF1sXiWAkunjo2qpKHieqlvSVmtwEhh6wsNwYTKEkddmTqvKSx0fHRvs3D9lMGJfg7wLSz/3Otx3G65tk9l/3B3r87qQTvbqXmcfnFdEIaR8mO/yMyCKnxtJkJb3lEzNUOrvnSxwL7Gyn54TLTWA== +

+
+
+
+
+
+ + + + ); } function SpecsSection() { - const [isModalOpen, setIsModalOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); - return ( -
-

- S'more  - - Specs -

-
-

- uses  - Zero Knowledge  - -   for your  - letters, with  - - Envelope Encryption - -   for the keys. -

-
- This means, both the  - encryption and  - decryption runs on - your device, in your browser. -
    -
  • - Every letter has a  - - unique key - -   which is derived from your original password. -
  • -
  • - Both the letter and the key are encrypted securely and sent to the - server. -
  • -
  • - Now, the server holds  - the envelope - ,  - the seal  - and  - - another locked box - - —with a key inside that unseals your letter. -
  • -
- But you— - only you—hold the very thing - that opens that box,  - your password. -
-
- - Nothing on the server is readable without your actual password. -
- Even if someone were to breach in, all they'd find is encrypted - noise and ain't no way they crackin' -
- -  (unless this happens) - -
-
- -
-
+ return ( +
+

+ S'more  + + Specs +

+
+

+ uses  + Zero Knowledge  + +   for your  + letters, with  + + Envelope Encryption + +   for the keys. +

+
+ This means, both the  + encryption and  + decryption runs on + your device, in your browser. +
    +
  • + Every letter has a  + + unique key + +   which is derived from your original password. +
  • +
  • + Both the letter and the key are encrypted securely and sent to the + server. +
  • +
  • + Now, the server holds  + the envelope + ,  + the seal  + and  + + another locked box + + —with a key inside that unseals your letter. +
  • +
+ But you— + only you—hold the very thing + that opens that box,  + your password. +
+
+ + Nothing on the server is readable without your actual password. +
+ Even if someone were to breach in, all they'd find is encrypted + noise and ain't no way they crackin' +
+ +  (unless this happens) + +
+
+ +
+
- + - setIsModalOpen(false)}> -
- pi ku e2e diagram -
-
+ setIsModalOpen(false)}> +
+ pi ku e2e diagram +
+
-

- Of course, this level of  - privacy comes with a - catch. No password reset -   for you. -

-

- Your original password is never stored - on the server. So, if it's forgotten, the letters stay sealed - foreeeeveer. -

-
-
- ); +

+ Of course, this level of  + privacy comes with a + catch. No password reset +   for you. +

+

+ Your original password is never stored + on the server. So, if it's forgotten, the letters stay sealed + foreeeeveer. +

+
+
+ ); } function OSSSection() { - return ( -
-

+

+ + is  + +  private + + only for +  your letters   + + + + +   + open source ! +

+
+

+ is + ...uhhh... pretty + secure. Every claim + about privacy and encryption is publicly available in the code so you + don't have to take my word at it. +

+

+ You can also  + Self-host +   + in just   + 4 steps. +

+
+
+            git clone https://git.ramvignesh.dev/me/pi-ku.git
+          
+
+            cd pi-ku
+          
+
+            ./scripts/setup.sh
+          
+
+            ./scripts/start.sh
+          
+
+ +
+ + View Source + + . +

+ Found something to report or request?  + - - is  - -  private - - only for -  your letters   - - - - -   - open source ! - -

-

- is - ...uhhh... pretty - secure. Every claim - about privacy and encryption is publicly available in the code so you - don't have to take my word at it. -

-

- You can also  - Self-host -   - in just   - 4 steps. -

-
-
-                        git clone https://git.ramvignesh.dev/me/pi-ku.git
-                    
-
-                        cd pi-ku
-                    
-
-                        ./scripts/setup.sh
-                    
-
-                        ./scripts/start.sh
-                    
-
+ Please say so. +
+

+
-
- - View Source - - . -

- Found something to report or request?  - - Please say so. - -

-
+
-
+

+ Built on the shoulders of open source. +

-

- Built on the shoulders of open source. -

+

+ wouldn't exist without the work of people who + chose to build in the open. +

+

+ a big thanks to +

+

+ + Web Crypto API + + : Browser-native cryptography that runs entirely on your device. The + backbone of everything secure—your letters, keys—here. +

-

- wouldn't exist without the work of people who - chose to build in the open. -

-

- a big thanks to -

-

- - Web Crypto API - - : Browser-native cryptography that runs entirely on your device. The - backbone of everything secure—your letters, keys—here. -

+

+ + DaisyUI + +   ·  + + Fabric.js + +   ·  + + Phosphor Icons + + : The brilliant work by others that let me focus on the core + experience instead of re-inventing the wheel. +

-

- - DaisyUI - -   ·  - - Fabric.js - -   ·  - - Phosphor Icons - - : The brilliant work by others that let me focus on the core - experience instead of re-inventing the wheel. -

- -

- Open source is what made possible. It always - feels right to give it back the same way. -

-
-
- ); +

+ Open source is what made possible. It always + feels right to give it back the same way. +

+ + + ); } function StorySection() { - return ( -
-

- The Story -

-
-
- -
-
- - - - -
- {/* Dict Card */} -
-
-
-
-
pin·ku·rip·pu
-
-
- /noun/ tamil -
- -
    -
  1. - postscript; a note written after the letter is signed. -
    -
    - "the most honest thing was always in the  - பி. கு." -
    -
  2. -
  3. the thing you almost didn't say.
  4. -
-
-
-
-
-
-
-
-
-
-
-
-
-

- is an abbreviated transliteration of the - தமிழ் - (Tamil) word - for  - -   —the thing you add after you've already signed your - name, what you write when you thought you were finished, but - weren't. -

-

- - Most of what we actually mean to say never gets said. - -
- It sits in drafts , in half-written notes, in the pause before we - change the subject.
- Those words  - -   stay  - - unsaid - - — - a quiet weight difficult to bear. -

-

- And that's okay... -

-

- - - was built for putting that weight down. - -
A space for the letters you meant to send, the afterthoughts - that deserved more than silence. -

-
-
+ return ( +
+

+ The Story +

+
+
+
- ); +
+ + + + +
+ {/* Dict Card */} +
+
+
+
+
pin·ku·rip·pu
+
+
+ /noun/ tamil +
+ +
    +
  1. + postscript; a note written after the letter is signed. +
    +
    + "the most honest thing was always in the  + பி. கு." +
    +
  2. +
  3. the thing you almost didn't say.
  4. +
+
+
+
+
+
+
+
+
+
+
+
+
+

+ is an abbreviated transliteration of the + தமிழ் + (Tamil) word + for  + +   —the thing you add after you've already signed your + name, what you write when you thought you were finished, but + weren't. +

+

+ + Most of what we actually mean to say never gets said. + +
+ It sits in drafts , in half-written notes, in the pause before we + change the subject.
+ Those words  + +   stay  + + unsaid + + — + a quiet weight difficult to bear. +

+

+ And that's okay... +

+

+ + + was built for putting that weight down. + +
A space for the letters you meant to send, the afterthoughts + that deserved more than silence. +

+
+
+
+ ); } function ForWhoSection() { - return ( -
-
-

- Who is
this for? -

+ return ( +
+
+

+ Who is
this for? +

-
-

- wasn't built for one kind of person, but a - particular kind of feeling— - - the one that lingers very quietly - - —fragile, yet never breaks. -

+
+

+ wasn't built for one kind of person, but a + particular kind of feeling— + + the one that lingers very quietly + + —fragile, yet never breaks. +

-
- - See if any of these feel too familiar to you - -
- -
-
-
+
+ + See if any of these feel too familiar to you + +
+
- -
+
- ); +
+ +
+
+ ); } function ArchetypesSection() { - return ( -
-

- The Archetypes -

-

of writing

-
-
-
- - -   To someone you can't reach anymore. - -
-

- A person who left. A relationship that ended without a real - ending. Someone who's still in your life but will never know - what you felt. Some conversations just close before they're - finished. -
-

-

- - Write the letter anyway. Keep it close. -

-
-
- - 01 - -
- -
-
- - -   To someone who's still here. - -
-

- Not every letter is about distance. Sometimes you just need to - say something properly—without a text thread, without the - noise of a conversation already in motion. A letter slows it - down. -

-

- - Give people their due flowers while they can still smell them. -

-
-
- - 02 - -
-
-
- -
- -   - -
- To yourself, further along. -
-
-

- Not a journal. Not a note-to-self. A proper letter—to - whoever you'll be in a year, or five, or ten. -
- Ask yourself of the healed wounds, forgotten fears, or the - things you finally learned to live with. -

-

- - Set a date and let a letter surprise you when you've long - forgotten writing it. -

-
-
- - 03 - -
-
-
- - -   For liberation. - -
-

- Some unsaid words just need to leave your headspace. There's no - recipient, no subject line, no send button. Just the act of - putting it somewhere outside of yourself.
- That's sometimes enough. -

-

- - Say it once. All of it. Then let it fade. -

-
-
- - 04 - -
- + return ( +
+

+ The Archetypes +

+

of writing

+
+
+
+ + +   To someone you can't reach anymore. + +
+

+ A person who left. A relationship that ended without a real + ending. Someone who's still in your life but will never know + what you felt. Some conversations just close before they're + finished. +
+

+

+ + Write the letter anyway. Keep it close. +

+
+ + 01 +
- ); + +
+
+ + +   To someone who's still here. + +
+

+ Not every letter is about distance. Sometimes you just need to + say something properly—without a text thread, without the + noise of a conversation already in motion. A letter slows it + down. +

+

+ + Give people their due flowers while they can still smell them. +

+
+
+ + 02 + +
+
+
+ +
+ +   + +
+ To yourself, further along. +
+
+

+ Not a journal. Not a note-to-self. A proper letter—to + whoever you'll be in a year, or five, or ten. +
+ Ask yourself of the healed wounds, forgotten fears, or the + things you finally learned to live with. +

+

+ + Set a date and let a letter surprise you when you've long + forgotten writing it. +

+
+
+ + 03 + +
+
+
+ + +   For liberation. + +
+

+ Some unsaid words just need to leave your headspace. There's no + recipient, no subject line, no send button. Just the act of + putting it somewhere outside of yourself.
+ That's sometimes enough. +

+

+ + Say it once. All of it. Then let it fade. +

+
+
+ + 04 + +
+ +
+
+ ); } function AttributionSection() { - const [hover, setHover] = useState<{ - visible: boolean; - x: number; - y: number; - }>({ visible: false, x: 0, y: 0 }); + const [hover, setHover] = useState<{ + visible: boolean; + x: number; + y: number; + }>({ visible: false, x: 0, y: 0 }); - const navigate = useNavigate(); + const navigate = useNavigate(); - return ( -
- {/* Saajan hover image */} - - {hover.visible && ( - - )} - + return ( +
+ {/* Saajan hover image */} + + {hover.visible && ( + + )} + -

+ Honest Speak +

+
+
+ Hi. +

Thank you so much for making it this far. Really.

+

+ took a while to exist. +
+ This started as a  + - Honest Speak - -

-
- Hi. -

Thank you so much for making it this far. Really.

-

- took a while to exist. -
- This started as a  -
- CS50W - -   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 - perfect as I could get it. Something to be remembered for—a - Swan Song if you will. -

-

So, I gave it all I've got.

-

- Of course, frustrations, id-exisi crises, crept in from time to - time. But helped me re-kindle the love for - the odd hours spent obsessing over the tiniest UX decisions and - endlessly polishing the UI  - - (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) - - . I know I've shared the nuts and bolts of -   here—the core philosophies, how it all works—but - the heart of it is really something you have to find by exploring it - yourself. -

-

- The "why" behind all of this didn't just appear out of nowhere. For - a while, I kept coming back to  - - setHover({ - visible: true, - x: e.clientX, - y: e.clientY, - }) - } - onMouseMove={(e) => - setHover((h) => ({ - ...h, - x: e.clientX, - y: e.clientY, - })) - } - onMouseLeave={() => setHover((h) => ({ ...h, visible: false }))} - > - Saajan - -   from  - - The Lunchbox - -   —brought to life with such subtle brilliance by  - - Irrfan Khan - -   - - —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  - - "it is what it is" - - , but the simple act of writing—of letting the unsaid - out—offered him a brief, yet necessary ease. I think about - that a lot. -

-

- There's a lot that goes  - - unsaid - -   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 digitally, -   yet somehow the things that actually matter most end up - staying inside—a trapped one at that. -
- Maybe writing can/will help. Maybe putting words somewhere - deliberate makes them feel less like a weight you're carrying alone. -

-

Or maybe it won't—but it's worth a try.

-

- is for that try. I hope it helps. Really. -

-

- —Ram -

-

- P.S. And just so we're clear—I wrote every word of this - myself—as I continue to back  - - Em DASH - - . Why should AI get to have all the fun with 'em em dashes?  - (get it?) -

-
-
- -   I think we forget things if there is nobody to tell them. - - ~ Saajan Fernandes,  - - The Lunchbox - - -
-
-
- -
+ CS50W + +   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 + perfect as I could get it. Something to be remembered for—a + Swan Song if you will. +

+

So, I gave it all I've got.

+

+ Of course, frustrations, id-exisi crises, crept in from time to + time. But helped me re-kindle the love for + the odd hours spent obsessing over the tiniest UX decisions and + endlessly polishing the UI  + + (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) + + . I know I've shared the nuts and bolts of +   here—the core philosophies, how it all works—but + the heart of it is really something you have to find by exploring it + yourself. +

+

+ The "why" behind all of this didn't just appear out of nowhere. For + a while, I kept coming back to  + + setHover({ + visible: true, + x: e.clientX, + y: e.clientY, + }) + } + onMouseMove={(e) => + setHover((h) => ({ + ...h, + x: e.clientX, + y: e.clientY, + })) + } + onMouseLeave={() => setHover((h) => ({ ...h, visible: false }))} + > + Saajan + +   from  + + The Lunchbox + +   —brought to life with such subtle brilliance by  + + Irrfan Khan + +   + + —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  + + "it is what it is" + + , but the simple act of writing—of letting the unsaid + out—offered him a brief, yet necessary ease. I think about + that a lot. +

+

+ There's a lot that goes  + + unsaid + +   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 digitally, +   yet somehow the things that actually matter most end up + staying inside—a trapped one at that. +
+ Maybe writing can/will help. Maybe putting words somewhere + deliberate makes them feel less like a weight you're carrying alone. +

+

Or maybe it won't—but it's worth a try.

+

+ is for that try. I hope it helps. Really. +

+

+ —Ram +

+

+ P.S. And just so we're clear—I wrote every word of this + myself—as I continue to back  + + Em DASH + + . Why should AI get to have all the fun with 'em em dashes?  + (get it?) +

- ); +
+ +   I think we forget things if there is nobody to tell them. + + ~ Saajan Fernandes,  + + The Lunchbox + + +
+
+
+ +
+
+ ); } -- 2.52.0 From e098ff51f1da05ca6332523eec334393cfe56870 Mon Sep 17 00:00:00 2001 From: me Date: Thu, 14 May 2026 05:59:40 +0530 Subject: [PATCH 2/4] style: update glass card layout padding and form copy --- frontend/src/index.css | 102 +++++------ frontend/src/pages/Login.tsx | 238 ++++++++++++------------ frontend/src/pages/Register.tsx | 316 ++++++++++++++++---------------- 3 files changed, 327 insertions(+), 329 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index a35eb7f..c6f21c6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,77 +2,75 @@ @plugin "daisyui"; @plugin "daisyui/theme" { - name: "piku"; - default: true; - prefersdark: true; - color-scheme: dark; + name: "piku"; + default: true; + prefersdark: true; + color-scheme: dark; - --color-base-100: oklch(14% 0.012 35); - --color-base-200: oklch(18% 0.014 33); - --color-base-300: oklch(22% 0.016 32); - --color-base-content: oklch(82% 0.02 70); + --color-base-100: oklch(14% 0.012 35); + --color-base-200: oklch(18% 0.014 33); + --color-base-300: oklch(22% 0.016 32); + --color-base-content: oklch(82% 0.02 70); - --color-primary: oklch(67% 0.11 78); - --color-primary-content: oklch(15% 0.03 70); + --color-primary: oklch(67% 0.11 78); + --color-primary-content: oklch(15% 0.03 70); - --color-secondary: oklch(48% 0.08 305); - --color-secondary-content: oklch(92% 0.01 305); + --color-secondary: oklch(48% 0.08 305); + --color-secondary-content: oklch(92% 0.01 305); - --color-accent: oklch(55% 0.06 325); - --color-accent-content: oklch(18% 0.03 295); + --color-accent: oklch(55% 0.06 325); + --color-accent-content: oklch(18% 0.03 295); - --color-neutral: oklch(38% 0.02 45); - --color-neutral-content: oklch(80% 0.015 60); + --color-neutral: oklch(38% 0.02 45); + --color-neutral-content: oklch(80% 0.015 60); - --color-info: oklch(60% 0.06 250); - --color-info-content: oklch(95% 0.01 240); - --color-success: oklch(65% 0.05 140); - --color-success-content: oklch(16% 0.03 150); - --color-warning: oklch(68% 0.08 72); - --color-warning-content: oklch(18% 0.03 60); - --color-error: oklch(55% 0.1 22); - --color-error-content: oklch(92% 0.01 22); + --color-info: oklch(60% 0.06 250); + --color-info-content: oklch(95% 0.01 240); + --color-success: oklch(65% 0.05 140); + --color-success-content: oklch(16% 0.03 150); + --color-warning: oklch(68% 0.08 72); + --color-warning-content: oklch(18% 0.03 60); + --color-error: oklch(55% 0.1 22); + --color-error-content: oklch(92% 0.01 22); - --radius-selector: 0.5rem; - --radius-field: 0.375rem; - --radius-box: 0.5rem; + --radius-selector: 0.5rem; + --radius-field: 0.375rem; + --radius-box: 0.5rem; - --depth: 1; - --noise: 0.03; + --depth: 1; + --noise: 0.03; - --border: 1px; + --border: 1px; } @theme { - --font-display: "Playwrite HR Lijeva Variable", cursive; - --font-sans: "Jost Variable", sans-serif; - --font-serif: "Fraunces Variable", serif; - --font-mono: "Space Mono", monospace; - --font-ink: "Kavivanar", sans-serif; - --font-redact: "Redacted Script", cursive; - --font-slab: "Josefin Slab Variable", serif; - --font-hand: "Architects Daughter", cursive; - --color-glass-bg: rgba(28, 22, 16, 0.45); - --shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6); - --radius-xl: 1.5rem; - --color-paper: oklch(97% 0.008 80); - --text-xxs: 10px; - --tracking-widester: 0.5em; - --background-image-vig: radial-gradient( - circle at center, - transparent 0%, - rgba(0, 0, 0, 0.4) 100% - ); + --font-display: "Playwrite HR Lijeva Variable", cursive; + --font-sans: "Jost Variable", sans-serif; + --font-serif: "Fraunces Variable", serif; + --font-mono: "Space Mono", monospace; + --font-ink: "Kavivanar", sans-serif; + --font-redact: "Redacted Script", cursive; + --font-slab: "Josefin Slab Variable", serif; + --font-hand: "Architects Daughter", cursive; + --color-glass-bg: rgba(28, 22, 16, 0.45); + --shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6); + --radius-xl: 1.5rem; + --color-paper: oklch(97% 0.008 80); + --text-xxs: 10px; + --tracking-widester: 0.5em; + --background-image-vig: radial-gradient(circle at center, + transparent 0%, + rgba(0, 0, 0, 0.4) 100%); } .glass-card { - @apply bg-glass-bg max-w-xs md:max-w-sm backdrop-blur-xl border border-neutral-content/10 shadow-warm rounded-xl m-4; + @apply bg-glass-bg max-w-xs md:max-w-sm backdrop-blur-xl border border-neutral-content/10 shadow-warm rounded-xl m-2 md:m-4; } .ul-wavy { - @apply decoration-primary/40 underline decoration-wavy underline-offset-4; + @apply decoration-primary/40 underline decoration-wavy underline-offset-4; } a { - @apply text-primary underline decoration-base-content/20 underline-offset-4 hover:decoration-primary/60 transition-colors; + @apply text-primary underline decoration-base-content/20 underline-offset-4 hover:decoration-primary/60 transition-colors; } diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 98b319f..e9f0b94 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -16,136 +16,136 @@ import { useAuth } from "../hooks/useAuth"; import { CryptoUtils } from "../utils/crypto"; const loginSchema = z.object({ - email: z.email("Please enter a valid email"), - password: z.string().min(1, "Password is required"), + email: z.email("Please enter a valid email"), + password: z.string().min(1, "Password is required"), }); type LoginInputs = z.infer; export default function Login() { - const navigate = useNavigate(); - const location = useLocation(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const { setAuthStore } = useAuth(); - const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); - const [saajanMessage, setSaajanMessage] = useState( - "I was wondering when you'd return.", - ); - const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; + const navigate = useNavigate(); + const location = useLocation(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const { setAuthStore } = useAuth(); + const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); + const [saajanMessage, setSaajanMessage] = useState( + "I was wondering, if you'd ever return.", + ); + const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(loginSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); - const onSubmit = async (data: LoginInputs) => { - setIsLoading(true); - setApiError(null); - try { - // client side key derivation for e2e encryption - const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: LoginInputs) => { + setIsLoading(true); + setApiError(null); + try { + // client side key derivation for e2e encryption + const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - // send just the authHash as the password to the server - const { data: authData } = await publicApi.post(endpoints.LOGIN, { - email: data.email, - password: authHash, - }); + // send just the authHash as the password to the server + const { data: authData } = await publicApi.post(endpoints.LOGIN, { + email: data.email, + password: authHash, + }); - const { data: userData } = await api.get(endpoints.ME, { - headers: { Authorization: `Bearer ${authData.access}` }, - }); + const { data: userData } = await api.get(endpoints.ME, { + headers: { Authorization: `Bearer ${authData.access}` }, + }); - await setAuthStore(authData.access, userData, masterKey); + await setAuthStore(authData.access, userData, masterKey); - navigate(nextRoute, { replace: true, state: location.state }); - } catch (err) { - let message = - "Sorry, we're experiencing technical issues.\nPlease try again later."; - if (axios.isAxiosError(err) && err.response?.status !== 500) { - message = err.response?.data?.detail || err.response?.data?.message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; - - return ( -
- {!showWelcome && } - {showWelcome && } -
-
-

-   Enter Archive -

- - {apiError && ( -
- {apiError} -
- )} - - setSaajanMessage("I remember you.")} - /> - - - setSaajanMessage("The one thing I cannot know for you.") + navigate(nextRoute, { replace: true, state: location.state }); + } catch (err) { + let message = + "Sorry, we're experiencing technical issues.\nPlease try again later."; + if (axios.isAxiosError(err) && err.response?.status !== 500) { + message = err.response?.data?.detail || err.response?.data?.message; } - /> + setApiError(message); + } finally { + setIsLoading(false); + } + }; -
- -
-
or
-
- New to - ?  - - . -
- -
-
- ); + return ( +
+ {!showWelcome && } + {showWelcome && } +
+
+

+  Unlock Archive +

+ + {apiError && ( +
+ {apiError} +
+ )} + + setSaajanMessage("I remember you.")} + /> + + + setSaajanMessage("The one thing I cannot know for you.") + } + /> + +
+ +
+
or
+
+ New to + ?  + + . +
+ +
+
+ ); } diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index f7b82a7..db1c8e8 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -14,172 +14,172 @@ import { ROUTES } from "../config/routes"; import { CryptoUtils } from "../utils/crypto"; const registerSchema = z - .object({ - full_name: z.string().min(2, "Name must be at least 2 characters"), - email: z.email("Please enter a valid email"), - password: z.string().min(8, "Password must be at least 8 characters"), - confirm_password: z.string(), - }) - .refine((data) => data.password === data.confirm_password, { - message: "Passwords don't match", - path: ["confirm_password"], - }); + .object({ + full_name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Please enter a valid email"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirm_password: z.string(), + }) + .refine((data) => data.password === data.confirm_password, { + message: "Passwords don't match", + path: ["confirm_password"], + }); type RegisterInputs = z.infer; export default function Register() { - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const [saajanMessage, setSaajanMessage] = useState( - "I didn't think I'd be here either.\nAnd yet, here we are.", - ); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const [saajanMessage, setSaajanMessage] = useState( + "I didn't think I'd be here either.\nAnd yet, here we are.", + ); - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(registerSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); - const onSubmit = async (data: RegisterInputs) => { - setSaajanMessage("Good. I'll remember that."); - setIsLoading(true); - setApiError(null); - try { - // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. - const { authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: RegisterInputs) => { + setSaajanMessage("Good. I'll remember that."); + setIsLoading(true); + setApiError(null); + try { + // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. + const { authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - await publicApi.post(endpoints.REGISTER, { - full_name: data.full_name, - email: data.email, - password: authHash, - }); - navigate(ROUTES.VERIFY_EMAIL, { replace: true }); - } catch (err) { - let message = "Registration failed. Please try again."; - if (axios.isAxiosError(err)) { - message = err.response?.data?.message || message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + await publicApi.post(endpoints.REGISTER, { + full_name: data.full_name, + email: data.email, + password: authHash, + }); + navigate(ROUTES.VERIFY_EMAIL, { replace: true }); + } catch (err) { + let message = "Registration failed. Please try again."; + if (axios.isAxiosError(err)) { + message = err.response?.data?.message || message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- -
-
-
- Create a - Account -
+ return ( +
+ +
+ +
+ Create a + Account +
- {apiError && ( -
- {apiError} + {apiError && ( +
+ {apiError} +
+ )} + + + setSaajanMessage("Hello friend. What should I call you?") + } + /> + + + setSaajanMessage( + "Where should I send your letters?\nNo empty lunchboxes, please.", + ) + } + /> + + + setSaajanMessage( + "Something only you know.\nI have one of those too.", + ) + } + /> + + + setSaajanMessage( + "Just once? Trust me, \nsome things are worth repeating twice.", + ) + } + /> + +
+ +

+ Choose a password you won't forget.
+ Just like life,  + there is no reset +   here. If you lose it, your letters cannot be recovered. +

+
+ +
+ +
+
or
+
+ Been here before?  + + . +
+
- )} - - - setSaajanMessage("Hello friend. What should I call you?") - } - /> - - - setSaajanMessage( - "Where should I send your letters?\nNo empty lunchboxes, please.", - ) - } - /> - - - setSaajanMessage( - "Something only you know.\nI have one of those too.", - ) - } - /> - - - setSaajanMessage( - "Just once? Trust me, \nsome things are worth repeating twice.", - ) - } - /> - -
- -

- Choose a password you won't forget.
- Just like life,  - there is no reset -   here. If you lose it, your letters cannot be recovered. -

-
- -
- -
-
or
-
- Been here before?  - - . -
- -
-
- ); +
+ ); } -- 2.52.0 From f0c6b608fd6426fd4b4397343c31a663151f171c Mon Sep 17 00:00:00 2001 From: me Date: Thu, 14 May 2026 06:00:01 +0530 Subject: [PATCH 3/4] style: adjust Saajan image width --- frontend/src/components/ui/Saajan.tsx | 82 +-- frontend/src/pages/Home.tsx | 750 +++++++++++++------------- 2 files changed, 416 insertions(+), 416 deletions(-) diff --git a/frontend/src/components/ui/Saajan.tsx b/frontend/src/components/ui/Saajan.tsx index 62b3d06..975de3d 100644 --- a/frontend/src/components/ui/Saajan.tsx +++ b/frontend/src/components/ui/Saajan.tsx @@ -2,52 +2,52 @@ import { useEffect, useState } from "react"; import sf_mini from "../../assets/sf_mini.png"; interface SaajanProps { - message: string; - position?: "top" | "left" | "right" | "bottom"; + message: string; + position?: "top" | "left" | "right" | "bottom"; } export default function Saajan({ message, position = "right" }: SaajanProps) { - const [animate, setAnimate] = useState(false); - const [tooltipPosition, setTooltipPosition] = - useState("tooltip-right"); - const [alignment, setAlignment] = useState("justify-start"); + const [animate, setAnimate] = useState(false); + const [tooltipPosition, setTooltipPosition] = + useState("tooltip-right"); + const [alignment, setAlignment] = useState("justify-start"); - useEffect(() => { - setAnimate(true); - const timeout = setTimeout(() => { - setAnimate(false); - }, 1000); + useEffect(() => { + setAnimate(true); + const timeout = setTimeout(() => { + setAnimate(false); + }, 1000); - return () => { - clearTimeout(timeout); - }; - }, []); + return () => { + clearTimeout(timeout); + }; + }, []); - useEffect(() => { - setTooltipPosition(`tooltip-${position}`); - if (position === "top") { - setAlignment("justify-center"); - } - if (position === "right") { - setAlignment("justify-start"); - } - if (position === "left") { - setAlignment("justify-end"); - } - }, [position]); + useEffect(() => { + setTooltipPosition(`tooltip-${position}`); + if (position === "top") { + setAlignment("justify-center"); + } + if (position === "right") { + setAlignment("justify-start"); + } + if (position === "left") { + setAlignment("justify-end"); + } + }, [position]); - return ( -
-
- saajan -
-
- ); + return ( +
+
+ saajan +
+
+ ); } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 53cd4ba..857dc77 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,10 +1,10 @@ import { InfoIcon } from "@phosphor-icons/react"; import { ReactLenis } from "lenis/react"; import { - motion, - useMotionValueEvent, - useScroll, - useTransform, + motion, + useMotionValueEvent, + useScroll, + useTransform, } from "motion/react"; import { useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -19,382 +19,382 @@ import "@fontsource/space-mono/index.css"; import "@fontsource/architects-daughter/index.css"; export default function Home() { - const sectionContainer1 = useRef(null); - const { scrollYProgress } = useScroll({ - target: sectionContainer1, - }); - const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); - const [flapOpen, setFlapOpen] = useState(false); - const [recipient, setRecipient] = useState("someone dear"); - const [ignite, setIgnite] = useState(false); + const sectionContainer1 = useRef(null); + const { scrollYProgress } = useScroll({ + target: sectionContainer1, + }); + const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); + const [flapOpen, setFlapOpen] = useState(false); + const [recipient, setRecipient] = useState("someone dear"); + const [ignite, setIgnite] = useState(false); - const navigate = useNavigate(); + const navigate = useNavigate(); - useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { - if (latestScrollValue > 0.54) { - setFlapOpen(false); - } else { - setFlapOpen(true); - } - if (latestScrollValue <= 0.6) { - setIsEnvelopeFlipped(true); - } else { - setIsEnvelopeFlipped(false); - } - if (latestScrollValue > 0.68) { - setRecipient("future me"); - } else { - setRecipient("someone dear"); - } - if (latestScrollValue > 0.77) { - setIgnite(true); - } else { - setIgnite(false); - } - }); + useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { + if (latestScrollValue > 0.54) { + setFlapOpen(false); + } else { + setFlapOpen(true); + } + if (latestScrollValue <= 0.6) { + setIsEnvelopeFlipped(true); + } else { + setIsEnvelopeFlipped(false); + } + if (latestScrollValue > 0.68) { + setRecipient("future me"); + } else { + setRecipient("someone dear"); + } + if (latestScrollValue > 0.77) { + setIgnite(true); + } else { + setIgnite(false); + } + }); - return ( - -
-
- {/* Intro */} - -

- You've been carrying something -

- - unsaid - -
+ return ( + +
+
+ {/* Intro */} + +

+ You've been carrying something +

+ + unsaid + +
- -
- and that's okay... -
-
- {/* pi. ku. */} - - - - is a{" "} - - safe space - - ,
- - where you can - -
-
+ +
+ and that's okay... +
+
+ {/* pi. ku. */} + + + + is a{" "} + + safe space + + ,
+ + where you can + +
+
-
- - pen down your unsaid words into  - - letters - - . - - {/* Seal */} - - seal it  - - secure - -   and  - - private - - . - - {/* Send / vault */} - - send it to  - - someone dear - - - -   or  - - - yourself in the future - - . - - - {/* Burn */} - - and even burn it -   to release the burden. - - {/* Outro */} - - You've been carrying it long enough. - - {/* CTA */} - - - - -
+
+ + pen down your unsaid words into  + + letters + + . + + {/* Seal */} + + seal it  + + secure + +   and  + + private + + . + + {/* Send / vault */} + + send it to  + + someone dear + + + +   or  + + + yourself in the future + + . + + + {/* Burn */} + + and even burn it +   to release the burden. + + {/* Outro */} + + You've been carrying it long enough. + + {/* CTA */} + + + + +
-
- -
-
-
- letter +
+ +
+
+
+ letter +
+
+
+ {/* Envelope */} + + { }} + isFlip={isEnvelopeFlipped} + openFlap={flapOpen} + /> + + {/* Saajan */} + + + + {/* Orb */} + +
+
-
- - {/* Envelope */} - - {}} - isFlip={isEnvelopeFlipped} - openFlap={flapOpen} - /> - - {/* Saajan */} - - - - {/* Orb */} - -
-
-
-
-
- ); +
+
+ ); } -- 2.52.0 From bcc8d95814d3ce923c55ddafd14a7f7fb5572b5b Mon Sep 17 00:00:00 2001 From: me Date: Thu, 14 May 2026 06:01:04 +0530 Subject: [PATCH 4/4] refactor: fix whitespaces --- frontend/src/components/ui/PasswordInput.tsx | 55 +- frontend/src/components/ui/Saajan.tsx | 82 +- frontend/src/index.css | 102 +-- frontend/src/pages/Home.tsx | 750 +++++++++---------- frontend/src/pages/Login.tsx | 239 +++--- frontend/src/pages/Register.tsx | 319 ++++---- 6 files changed, 778 insertions(+), 769 deletions(-) diff --git a/frontend/src/components/ui/PasswordInput.tsx b/frontend/src/components/ui/PasswordInput.tsx index 4e35070..ff5bad4 100644 --- a/frontend/src/components/ui/PasswordInput.tsx +++ b/frontend/src/components/ui/PasswordInput.tsx @@ -2,36 +2,37 @@ import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react"; import { useState } from "react"; interface PasswordInputProps - extends React.InputHTMLAttributes { - error?: boolean; + extends React.InputHTMLAttributes { + error?: boolean; } export function PasswordInput({ - className, - error, - ...props + className, + error, + ...props }: PasswordInputProps) { - const [showPassword, setShowPassword] = useState(false); + const [showPassword, setShowPassword] = useState(false); - return ( -
- - -
- ); + return ( +
+ + +
+ ); } diff --git a/frontend/src/components/ui/Saajan.tsx b/frontend/src/components/ui/Saajan.tsx index 975de3d..b6c0cb6 100644 --- a/frontend/src/components/ui/Saajan.tsx +++ b/frontend/src/components/ui/Saajan.tsx @@ -2,52 +2,52 @@ import { useEffect, useState } from "react"; import sf_mini from "../../assets/sf_mini.png"; interface SaajanProps { - message: string; - position?: "top" | "left" | "right" | "bottom"; + message: string; + position?: "top" | "left" | "right" | "bottom"; } export default function Saajan({ message, position = "right" }: SaajanProps) { - const [animate, setAnimate] = useState(false); - const [tooltipPosition, setTooltipPosition] = - useState("tooltip-right"); - const [alignment, setAlignment] = useState("justify-start"); + const [animate, setAnimate] = useState(false); + const [tooltipPosition, setTooltipPosition] = + useState("tooltip-right"); + const [alignment, setAlignment] = useState("justify-start"); - useEffect(() => { - setAnimate(true); - const timeout = setTimeout(() => { - setAnimate(false); - }, 1000); + useEffect(() => { + setAnimate(true); + const timeout = setTimeout(() => { + setAnimate(false); + }, 1000); - return () => { - clearTimeout(timeout); - }; - }, []); + return () => { + clearTimeout(timeout); + }; + }, []); - useEffect(() => { - setTooltipPosition(`tooltip-${position}`); - if (position === "top") { - setAlignment("justify-center"); - } - if (position === "right") { - setAlignment("justify-start"); - } - if (position === "left") { - setAlignment("justify-end"); - } - }, [position]); + useEffect(() => { + setTooltipPosition(`tooltip-${position}`); + if (position === "top") { + setAlignment("justify-center"); + } + if (position === "right") { + setAlignment("justify-start"); + } + if (position === "left") { + setAlignment("justify-end"); + } + }, [position]); - return ( -
-
- saajan -
-
- ); + return ( +
+
+ saajan +
+
+ ); } diff --git a/frontend/src/index.css b/frontend/src/index.css index c6f21c6..1ecefeb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,75 +2,77 @@ @plugin "daisyui"; @plugin "daisyui/theme" { - name: "piku"; - default: true; - prefersdark: true; - color-scheme: dark; + name: "piku"; + default: true; + prefersdark: true; + color-scheme: dark; - --color-base-100: oklch(14% 0.012 35); - --color-base-200: oklch(18% 0.014 33); - --color-base-300: oklch(22% 0.016 32); - --color-base-content: oklch(82% 0.02 70); + --color-base-100: oklch(14% 0.012 35); + --color-base-200: oklch(18% 0.014 33); + --color-base-300: oklch(22% 0.016 32); + --color-base-content: oklch(82% 0.02 70); - --color-primary: oklch(67% 0.11 78); - --color-primary-content: oklch(15% 0.03 70); + --color-primary: oklch(67% 0.11 78); + --color-primary-content: oklch(15% 0.03 70); - --color-secondary: oklch(48% 0.08 305); - --color-secondary-content: oklch(92% 0.01 305); + --color-secondary: oklch(48% 0.08 305); + --color-secondary-content: oklch(92% 0.01 305); - --color-accent: oklch(55% 0.06 325); - --color-accent-content: oklch(18% 0.03 295); + --color-accent: oklch(55% 0.06 325); + --color-accent-content: oklch(18% 0.03 295); - --color-neutral: oklch(38% 0.02 45); - --color-neutral-content: oklch(80% 0.015 60); + --color-neutral: oklch(38% 0.02 45); + --color-neutral-content: oklch(80% 0.015 60); - --color-info: oklch(60% 0.06 250); - --color-info-content: oklch(95% 0.01 240); - --color-success: oklch(65% 0.05 140); - --color-success-content: oklch(16% 0.03 150); - --color-warning: oklch(68% 0.08 72); - --color-warning-content: oklch(18% 0.03 60); - --color-error: oklch(55% 0.1 22); - --color-error-content: oklch(92% 0.01 22); + --color-info: oklch(60% 0.06 250); + --color-info-content: oklch(95% 0.01 240); + --color-success: oklch(65% 0.05 140); + --color-success-content: oklch(16% 0.03 150); + --color-warning: oklch(68% 0.08 72); + --color-warning-content: oklch(18% 0.03 60); + --color-error: oklch(55% 0.1 22); + --color-error-content: oklch(92% 0.01 22); - --radius-selector: 0.5rem; - --radius-field: 0.375rem; - --radius-box: 0.5rem; + --radius-selector: 0.5rem; + --radius-field: 0.375rem; + --radius-box: 0.5rem; - --depth: 1; - --noise: 0.03; + --depth: 1; + --noise: 0.03; - --border: 1px; + --border: 1px; } @theme { - --font-display: "Playwrite HR Lijeva Variable", cursive; - --font-sans: "Jost Variable", sans-serif; - --font-serif: "Fraunces Variable", serif; - --font-mono: "Space Mono", monospace; - --font-ink: "Kavivanar", sans-serif; - --font-redact: "Redacted Script", cursive; - --font-slab: "Josefin Slab Variable", serif; - --font-hand: "Architects Daughter", cursive; - --color-glass-bg: rgba(28, 22, 16, 0.45); - --shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6); - --radius-xl: 1.5rem; - --color-paper: oklch(97% 0.008 80); - --text-xxs: 10px; - --tracking-widester: 0.5em; - --background-image-vig: radial-gradient(circle at center, - transparent 0%, - rgba(0, 0, 0, 0.4) 100%); + --font-display: "Playwrite HR Lijeva Variable", cursive; + --font-sans: "Jost Variable", sans-serif; + --font-serif: "Fraunces Variable", serif; + --font-mono: "Space Mono", monospace; + --font-ink: "Kavivanar", sans-serif; + --font-redact: "Redacted Script", cursive; + --font-slab: "Josefin Slab Variable", serif; + --font-hand: "Architects Daughter", cursive; + --color-glass-bg: rgba(28, 22, 16, 0.45); + --shadow-warm: 0 20px 50px -12px rgba(30, 20, 12, 0.6); + --radius-xl: 1.5rem; + --color-paper: oklch(97% 0.008 80); + --text-xxs: 10px; + --tracking-widester: 0.5em; + --background-image-vig: radial-gradient( + circle at center, + transparent 0%, + rgba(0, 0, 0, 0.4) 100% + ); } .glass-card { - @apply bg-glass-bg max-w-xs md:max-w-sm backdrop-blur-xl border border-neutral-content/10 shadow-warm rounded-xl m-2 md:m-4; + @apply bg-glass-bg max-w-xs md:max-w-sm backdrop-blur-xl border border-neutral-content/10 shadow-warm rounded-xl m-2 md:m-4; } .ul-wavy { - @apply decoration-primary/40 underline decoration-wavy underline-offset-4; + @apply decoration-primary/40 underline decoration-wavy underline-offset-4; } a { - @apply text-primary underline decoration-base-content/20 underline-offset-4 hover:decoration-primary/60 transition-colors; + @apply text-primary underline decoration-base-content/20 underline-offset-4 hover:decoration-primary/60 transition-colors; } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 857dc77..c14c556 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,10 +1,10 @@ import { InfoIcon } from "@phosphor-icons/react"; import { ReactLenis } from "lenis/react"; import { - motion, - useMotionValueEvent, - useScroll, - useTransform, + motion, + useMotionValueEvent, + useScroll, + useTransform, } from "motion/react"; import { useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -19,382 +19,382 @@ import "@fontsource/space-mono/index.css"; import "@fontsource/architects-daughter/index.css"; export default function Home() { - const sectionContainer1 = useRef(null); - const { scrollYProgress } = useScroll({ - target: sectionContainer1, - }); - const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); - const [flapOpen, setFlapOpen] = useState(false); - const [recipient, setRecipient] = useState("someone dear"); - const [ignite, setIgnite] = useState(false); + const sectionContainer1 = useRef(null); + const { scrollYProgress } = useScroll({ + target: sectionContainer1, + }); + const [isEnvelopeFlipped, setIsEnvelopeFlipped] = useState(true); + const [flapOpen, setFlapOpen] = useState(false); + const [recipient, setRecipient] = useState("someone dear"); + const [ignite, setIgnite] = useState(false); - const navigate = useNavigate(); + const navigate = useNavigate(); - useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { - if (latestScrollValue > 0.54) { - setFlapOpen(false); - } else { - setFlapOpen(true); - } - if (latestScrollValue <= 0.6) { - setIsEnvelopeFlipped(true); - } else { - setIsEnvelopeFlipped(false); - } - if (latestScrollValue > 0.68) { - setRecipient("future me"); - } else { - setRecipient("someone dear"); - } - if (latestScrollValue > 0.77) { - setIgnite(true); - } else { - setIgnite(false); - } - }); + useMotionValueEvent(scrollYProgress, "change", (latestScrollValue) => { + if (latestScrollValue > 0.54) { + setFlapOpen(false); + } else { + setFlapOpen(true); + } + if (latestScrollValue <= 0.6) { + setIsEnvelopeFlipped(true); + } else { + setIsEnvelopeFlipped(false); + } + if (latestScrollValue > 0.68) { + setRecipient("future me"); + } else { + setRecipient("someone dear"); + } + if (latestScrollValue > 0.77) { + setIgnite(true); + } else { + setIgnite(false); + } + }); - return ( - -
+
+
+ {/* Intro */} + +

+ You've been carrying something +

+ + unsaid + +
+ + +
+ and that's okay... +
+
+ {/* pi. ku. */} + + + -
- {/* Intro */} - -

- You've been carrying something -

- - unsaid - -
+ is a{" "} + + safe space + + ,
+ + where you can + + + - -
- and that's okay... -
-
- {/* pi. ku. */} - - - - is a{" "} - - safe space - - ,
- - where you can - -
-
+
+ + pen down your unsaid words into  + + letters + + . + + {/* Seal */} + + seal it  + + secure + +   and  + + private + + . + + {/* Send / vault */} + + send it to  + + someone dear + + + +   or  + + + yourself in the future + + . + + + {/* Burn */} + + and even burn it +   to release the burden. + + {/* Outro */} + + You've been carrying it long enough. + + {/* CTA */} + + + + +
-
- - pen down your unsaid words into  - - letters - - . - - {/* Seal */} - - seal it  - - secure - -   and  - - private - - . - - {/* Send / vault */} - - send it to  - - someone dear - - - -   or  - - - yourself in the future - - . - - - {/* Burn */} - - and even burn it -   to release the burden. - - {/* Outro */} - - You've been carrying it long enough. - - {/* CTA */} - - - - -
- -
- -
-
-
- letter -
-
-
- {/* Envelope */} - - { }} - isFlip={isEnvelopeFlipped} - openFlap={flapOpen} - /> - - {/* Saajan */} - - - - {/* Orb */} - -
-
+
+ +
+
+
+ letter
-
- - ); +
+ + {/* Envelope */} + + {}} + isFlip={isEnvelopeFlipped} + openFlap={flapOpen} + /> + + {/* Saajan */} + + + + {/* Orb */} + +
+
+
+ + + ); } diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index e9f0b94..e304194 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -16,136 +16,139 @@ import { useAuth } from "../hooks/useAuth"; import { CryptoUtils } from "../utils/crypto"; const loginSchema = z.object({ - email: z.email("Please enter a valid email"), - password: z.string().min(1, "Password is required"), + email: z.email("Please enter a valid email"), + password: z.string().min(1, "Password is required"), }); type LoginInputs = z.infer; export default function Login() { - const navigate = useNavigate(); - const location = useLocation(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const { setAuthStore } = useAuth(); - const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); - const [saajanMessage, setSaajanMessage] = useState( - "I was wondering, if you'd ever return.", - ); - const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; + const navigate = useNavigate(); + const location = useLocation(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const { setAuthStore } = useAuth(); + const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); + const [saajanMessage, setSaajanMessage] = useState( + "I was wondering, if you'd ever return.", + ); + const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER; - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(loginSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); - const onSubmit = async (data: LoginInputs) => { - setIsLoading(true); - setApiError(null); - try { - // client side key derivation for e2e encryption - const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: LoginInputs) => { + setIsLoading(true); + setApiError(null); + try { + // client side key derivation for e2e encryption + const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - // send just the authHash as the password to the server - const { data: authData } = await publicApi.post(endpoints.LOGIN, { - email: data.email, - password: authHash, - }); + // send just the authHash as the password to the server + const { data: authData } = await publicApi.post(endpoints.LOGIN, { + email: data.email, + password: authHash, + }); - const { data: userData } = await api.get(endpoints.ME, { - headers: { Authorization: `Bearer ${authData.access}` }, - }); + const { data: userData } = await api.get(endpoints.ME, { + headers: { Authorization: `Bearer ${authData.access}` }, + }); - await setAuthStore(authData.access, userData, masterKey); + await setAuthStore(authData.access, userData, masterKey); - navigate(nextRoute, { replace: true, state: location.state }); - } catch (err) { - let message = - "Sorry, we're experiencing technical issues.\nPlease try again later."; - if (axios.isAxiosError(err) && err.response?.status !== 500) { - message = err.response?.data?.detail || err.response?.data?.message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + navigate(nextRoute, { replace: true, state: location.state }); + } catch (err) { + let message = + "Sorry, we're experiencing technical issues.\nPlease try again later."; + if (axios.isAxiosError(err) && err.response?.status !== 500) { + message = err.response?.data?.detail || err.response?.data?.message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- {!showWelcome && } - {showWelcome && } -
-
-

-  Unlock Archive -

+ return ( +
+ {!showWelcome && } + {showWelcome && } +
+ +

+  Unlock Archive +

- {apiError && ( -
- {apiError} -
- )} - - setSaajanMessage("I remember you.")} - /> - - - setSaajanMessage("The one thing I cannot know for you.") - } - /> - -
- -
-
or
-
- New to - ?  - - . -
- + {apiError && ( +
+ {apiError}
-
- ); + )} + + setSaajanMessage("I remember you.")} + /> + + + setSaajanMessage("The one thing I cannot know for you.") + } + /> + +
+ +
+
or
+
+ New to + ?  + + . +
+ +
+
+ ); } diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index db1c8e8..05cfbed 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -14,172 +14,175 @@ import { ROUTES } from "../config/routes"; import { CryptoUtils } from "../utils/crypto"; const registerSchema = z - .object({ - full_name: z.string().min(2, "Name must be at least 2 characters"), - email: z.email("Please enter a valid email"), - password: z.string().min(8, "Password must be at least 8 characters"), - confirm_password: z.string(), - }) - .refine((data) => data.password === data.confirm_password, { - message: "Passwords don't match", - path: ["confirm_password"], - }); + .object({ + full_name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Please enter a valid email"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirm_password: z.string(), + }) + .refine((data) => data.password === data.confirm_password, { + message: "Passwords don't match", + path: ["confirm_password"], + }); type RegisterInputs = z.infer; export default function Register() { - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(null); - const [saajanMessage, setSaajanMessage] = useState( - "I didn't think I'd be here either.\nAnd yet, here we are.", - ); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const [saajanMessage, setSaajanMessage] = useState( + "I didn't think I'd be here either.\nAnd yet, here we are.", + ); - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(registerSchema), - }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); - const onSubmit = async (data: RegisterInputs) => { - setSaajanMessage("Good. I'll remember that."); - setIsLoading(true); - setApiError(null); - try { - // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. - const { authHash } = await CryptoUtils.deriveKeyBundle( - data.password, - data.email, - ); + const onSubmit = async (data: RegisterInputs) => { + setSaajanMessage("Good. I'll remember that."); + setIsLoading(true); + setApiError(null); + try { + // we generate the key bundle here to get the authHash (password) to be haSHed and stored in the db. + const { authHash } = await CryptoUtils.deriveKeyBundle( + data.password, + data.email, + ); - await publicApi.post(endpoints.REGISTER, { - full_name: data.full_name, - email: data.email, - password: authHash, - }); - navigate(ROUTES.VERIFY_EMAIL, { replace: true }); - } catch (err) { - let message = "Registration failed. Please try again."; - if (axios.isAxiosError(err)) { - message = err.response?.data?.message || message; - } - setApiError(message); - } finally { - setIsLoading(false); - } - }; + await publicApi.post(endpoints.REGISTER, { + full_name: data.full_name, + email: data.email, + password: authHash, + }); + navigate(ROUTES.VERIFY_EMAIL, { replace: true }); + } catch (err) { + let message = "Registration failed. Please try again."; + if (axios.isAxiosError(err)) { + message = err.response?.data?.message || message; + } + setApiError(message); + } finally { + setIsLoading(false); + } + }; - return ( -
- -
-
-
- Create a - Account -
+ return ( +
+ +
+ +
+ Create a + Account +
- {apiError && ( -
- {apiError} -
- )} - - - setSaajanMessage("Hello friend. What should I call you?") - } - /> - - - setSaajanMessage( - "Where should I send your letters?\nNo empty lunchboxes, please.", - ) - } - /> - - - setSaajanMessage( - "Something only you know.\nI have one of those too.", - ) - } - /> - - - setSaajanMessage( - "Just once? Trust me, \nsome things are worth repeating twice.", - ) - } - /> - -
- -

- Choose a password you won't forget.
- Just like life,  - there is no reset -   here. If you lose it, your letters cannot be recovered. -

-
- -
- -
-
or
-
- Been here before?  - - . -
- + {apiError && ( +
+ {apiError}
-
- ); + )} + + + setSaajanMessage("Hello friend. What should I call you?") + } + /> + + + setSaajanMessage( + "Where should I send your letters?\nNo empty lunchboxes, please.", + ) + } + /> + + + setSaajanMessage( + "Something only you know.\nI have one of those too.", + ) + } + /> + + + setSaajanMessage( + "Just once? Trust me, \nsome things are worth repeating twice.", + ) + } + /> + +
+ +

+ Choose a password you won't forget.
+ Just like life,  + there is no reset +   here. If you lose it, your letters cannot be recovered. +

+
+ +
+ +
+
or
+
+ Been here before?  + + . +
+ +
+
+ ); } -- 2.52.0