mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 15:56:56 +00:00
Feature/saajan persona (#3)
* feat: add template based email content (html + plaintext fallback) * feat: init saajan component * feat: add aesthetic noise background and implement Saajan component in register and login * feat: add post seal modal for vault * refactor: add proper props interfaces * refactor: expose props on ui components * feat: add ssajan in lots of flows * fix: remove render test with no value and add aria helper for btn identification * refactor: update email notification to account for proper arguments * refactor: refactor E2E auth helper and mail parsing logic --------- Co-authored-by: ramvignesh-b <ramvignesh-b@github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { DotIcon } from "@phosphor-icons/react";
|
||||
import "@fontsource/knewave/400.css";
|
||||
|
||||
export default function Logo({ scale = 2 }) {
|
||||
export default function Logo({ scale = 1 }) {
|
||||
return (
|
||||
<div
|
||||
role="img"
|
||||
@@ -9,16 +9,16 @@ export default function Logo({ scale = 2 }) {
|
||||
className="inline-flex items-baseline justify-center leading-none select-none"
|
||||
style={{ fontFamily: "'Knewave', serif", scale }}
|
||||
>
|
||||
<span className={`text-xl font-light text-accent`}> Pi</span>
|
||||
<span className={`text-3xl font-light text-accent`}>Pi</span>
|
||||
<DotIcon
|
||||
weight="fill"
|
||||
size={6}
|
||||
size={12}
|
||||
className={`text-primary translate-y-1 -mx-px`}
|
||||
/>
|
||||
<span className={`text-xl font-light text-accent`}> Ku</span>
|
||||
<span className={`text-3xl font-light text-accent`}> Ku</span>
|
||||
<DotIcon
|
||||
weight="fill"
|
||||
size={6}
|
||||
size={12}
|
||||
className={`text-primary translate-y-1 -mx-px`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,15 @@ import { LockIcon, LockKeyOpenIcon } from "@phosphor-icons/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PATHS } from "../../config/routes";
|
||||
|
||||
interface LetterItemProps {
|
||||
preview: string;
|
||||
timestamp: string;
|
||||
id: string;
|
||||
status: "DRAFT" | "SEALED" | "BURNED";
|
||||
unlock_at?: string;
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
export function LetterItem({
|
||||
preview,
|
||||
timestamp,
|
||||
@@ -9,14 +18,7 @@ export function LetterItem({
|
||||
status,
|
||||
unlock_at,
|
||||
isLocked = false,
|
||||
}: {
|
||||
preview: string;
|
||||
timestamp: string;
|
||||
id: string;
|
||||
status: "DRAFT" | "SEALED" | "BURNED";
|
||||
unlock_at?: string;
|
||||
isLocked?: boolean;
|
||||
}) {
|
||||
}: LetterItemProps) {
|
||||
const navigate = useNavigate();
|
||||
function handleNavigate(): void {
|
||||
if (isLocked) return;
|
||||
|
||||
@@ -5,11 +5,13 @@ import { PATHS, ROUTES } from "../../config/routes";
|
||||
interface PostSealModalProps {
|
||||
sealedTargetId: string | null;
|
||||
navigate: NavigateFunction;
|
||||
type: "KEPT" | "VAULT";
|
||||
}
|
||||
|
||||
export function PostSealModal({
|
||||
sealedTargetId,
|
||||
navigate,
|
||||
type = "KEPT",
|
||||
}: PostSealModalProps) {
|
||||
if (!sealedTargetId) return null;
|
||||
return (
|
||||
@@ -20,33 +22,61 @@ export function PostSealModal({
|
||||
<p className="text-base-content/60">
|
||||
It's encrypted and always safe in your drawer.
|
||||
</p>
|
||||
<p className="text-base-content font-sans">
|
||||
When you're ready,
|
||||
<br />
|
||||
you can{" "}
|
||||
<span className="text-primary font-bold font-display">read</span> it,{" "}
|
||||
<span className="text-accent font-bold font-display">send</span> it to
|
||||
someone, or{" "}
|
||||
<span className="text-error font-bold font-display">burn</span> it to
|
||||
release
|
||||
</p>
|
||||
{type === "KEPT" ? (
|
||||
<p className="text-base-content/80 text-sm font-sans">
|
||||
When you're ready,
|
||||
<br />
|
||||
you can{" "}
|
||||
<span className="text-primary font-bold font-display">read</span>{" "}
|
||||
it, <span className="text-accent font-bold font-display">send</span>{" "}
|
||||
it to someone, or{" "}
|
||||
<span className="text-error font-bold font-display">burn</span> it
|
||||
to release
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-base-content/80 text-sm font-sans">
|
||||
Be assured that the letter will find you when the time is right.
|
||||
<br />
|
||||
Till then,{" "}
|
||||
<span className="font-bold font-display text-primary">
|
||||
take a deep breath
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="font-bold font-display text-accent">manifest</span>
|
||||
, and{" "}
|
||||
<span className="font-bold font-display text-success">
|
||||
let it rest
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
<div className="modal-action w-full justify-center gap-3 mt-4 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => navigate(ROUTES.DRAWER)}
|
||||
>
|
||||
Keep it to myself
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() =>
|
||||
navigate(PATHS.read(sealedTargetId), { replace: true })
|
||||
}
|
||||
>
|
||||
View letter
|
||||
</button>
|
||||
{type === "KEPT" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => navigate(ROUTES.DRAWER)}
|
||||
>
|
||||
Keep it to myself
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => navigate(PATHS.read(sealedTargetId))}
|
||||
>
|
||||
View letter
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => navigate(ROUTES.DRAWER)}
|
||||
>
|
||||
Step Away...
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,10 +102,24 @@ export function ToolBar({
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Help"
|
||||
onClick={() => setSealBtnClicked(false)}
|
||||
className={`bg-transparent cursor-pointer -mt-2 absolute z-1000001 right-0 text-primary ${sealBtnClicked ? "" : "hidden"}`}
|
||||
>
|
||||
<QuestionIcon weight="duotone" size={20} className={""} />
|
||||
<div className="tooltip tooltip-left">
|
||||
<div className="tooltip-content -translate-x-38 text-left">
|
||||
<span className="font-bold text-accent">Seal</span> puts the letter
|
||||
in an envelope, ready to be read right away.
|
||||
<div className="divider my-0"></div>
|
||||
<span className="font-bold text-success">Vault</span> keeps it
|
||||
locked away until the right moment, even from yourself.
|
||||
</div>
|
||||
<QuestionIcon
|
||||
weight="duotone"
|
||||
size={20}
|
||||
className={"absolute -translate-x-38 -translate-y-3"}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -136,21 +150,23 @@ export function VaultConfirmModal({
|
||||
setUnlockDate,
|
||||
}: VaultConfirmModalProps) {
|
||||
return (
|
||||
<div className={"modal modal-open bg-base-100/20 backdrop-blur-md"}>
|
||||
<div className="modal-box p-12 flex flex-col items-center">
|
||||
<div className={"modal modal-open bg-base-100/10 backdrop-blur-md"}>
|
||||
<div className="modal-box p-12 flex flex-col items-center bg-base-100/90">
|
||||
<VaultIcon
|
||||
size={48}
|
||||
className="text-primary mx-auto mb-8 animate-pulse"
|
||||
/>
|
||||
<h3 className="font-serif text-3xl">Vault this letter?</h3>
|
||||
<h3 className="font-serif text-3xl">Take it away, then?</h3>
|
||||
<p className="text-base-content/60 text-sm text-center mt-4">
|
||||
Vaulting locks the letter permanently and will be{" "}
|
||||
<span className={"font-bold text-primary"}>mailed</span> to you
|
||||
automatically on the unlock date.
|
||||
By vaulting this letter, you ask me to hold on to this.
|
||||
<br />
|
||||
<span className={"underline"}>
|
||||
You cannot edit or view the contents of the letter until then.
|
||||
I'll remember to mail you this on the unlock date.
|
||||
<br />
|
||||
<span className={"font-bold text-primary"}>
|
||||
{" "}
|
||||
But I won't let you read or rewrite this letter until then.
|
||||
</span>
|
||||
<br />
|
||||
</p>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
@@ -158,11 +174,13 @@ export function VaultConfirmModal({
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const unlockDateStr = formData.get("vault-date") as string;
|
||||
const newUnlockDate = new Date(unlockDateStr);
|
||||
console.log(newUnlockDate);
|
||||
setUnlockDate(newUnlockDate);
|
||||
await onSave("VAULT", newUnlockDate);
|
||||
setConfirmModal(null);
|
||||
}}
|
||||
id="vault-form"
|
||||
className="min-w-75"
|
||||
>
|
||||
<div className={"divider tracking-tightest font-display text-sm"}>
|
||||
Set an unlock date
|
||||
@@ -173,21 +191,22 @@ export function VaultConfirmModal({
|
||||
className="input input-bordered w-full"
|
||||
name="vault-date"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary mt-4"
|
||||
type="submit"
|
||||
form="vault-form"
|
||||
>
|
||||
Vault
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost mt-4"
|
||||
onClick={() => setConfirmModal(null)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<div className="w-full flex justify-center gap-8 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-sm mt-4"
|
||||
onClick={() => setConfirmModal(null)}
|
||||
>
|
||||
I need time
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm mt-4"
|
||||
type="submit"
|
||||
form="vault-form"
|
||||
>
|
||||
Take it
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { CampfireIcon, FlameIcon, XCircleIcon } from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface BurnModalProps {
|
||||
burnLetter: () => void;
|
||||
isBurning: boolean;
|
||||
setShowBurnModal: (show: boolean) => void;
|
||||
setRevealState: (state: "sealed" | "revealed" | "burning" | "burned") => void;
|
||||
}
|
||||
|
||||
export function BurnModal({
|
||||
burnLetter,
|
||||
isBurning,
|
||||
setShowBurnModal,
|
||||
setRevealState,
|
||||
}) {
|
||||
}: BurnModalProps) {
|
||||
const [flameOn, setFlameOn] = useState(0);
|
||||
const [rotate, setRotate] = useState(0);
|
||||
const [burnClicked, setBurnClicked] = useState(false);
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface EnvelopeRevealProps {
|
||||
onRevealComplete: () => void;
|
||||
ignite: boolean;
|
||||
isFlip?: boolean;
|
||||
isInteractive?: boolean;
|
||||
}
|
||||
|
||||
export function EnvelopeReveal({
|
||||
@@ -17,6 +18,7 @@ export function EnvelopeReveal({
|
||||
onRevealComplete,
|
||||
ignite,
|
||||
isFlip,
|
||||
isInteractive = true,
|
||||
}: EnvelopeRevealProps) {
|
||||
const [revealLetter, setRevealLetter] = useState(false);
|
||||
const [isFlipped, setIsFlipped] = useState(!!isFlip);
|
||||
@@ -67,6 +69,7 @@ export function EnvelopeReveal({
|
||||
type="checkbox"
|
||||
className="transition checkbox absolute h-full w-full text-transparent bg-transparent z-100"
|
||||
ref={flapCheckbox}
|
||||
disabled={!isInteractive}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
@@ -103,6 +106,7 @@ export function EnvelopeReveal({
|
||||
<button
|
||||
id="env-front"
|
||||
type="button"
|
||||
disabled={!isInteractive}
|
||||
className={`text-left p-10 absolute inset-0 backface-hidden w-110 bg-base-200 z-99 rounded-md -translate-x-2 ${isFlipped ? "pointer-events-none" : ""}`}
|
||||
onClick={() => setIsFlipped((prev) => !prev)}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ROUTES } from "../../config/routes";
|
||||
|
||||
export function PostActionOverlay({ revealState }) {
|
||||
interface PostActionOverlayProps {
|
||||
revealState: "sealed" | "revealed" | "burning" | "burned";
|
||||
}
|
||||
|
||||
export function PostActionOverlay({ revealState }: PostActionOverlayProps) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -4,7 +4,12 @@ import {
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export function ShareModal({ shareLink, setShareLink }) {
|
||||
interface ShareModalProps {
|
||||
shareLink: string | null;
|
||||
setShareLink: (link: string | null) => void;
|
||||
}
|
||||
|
||||
export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
|
||||
const copyToClipboard = async () => {
|
||||
if (!shareLink) return;
|
||||
await navigator.clipboard.writeText(shareLink);
|
||||
|
||||
@@ -6,6 +6,7 @@ interface FormFieldProps {
|
||||
placeholder?: string;
|
||||
registration: UseFormRegisterReturn;
|
||||
error?: string;
|
||||
handleFocus?: () => void;
|
||||
}
|
||||
|
||||
export default function FormField({
|
||||
@@ -14,6 +15,7 @@ export default function FormField({
|
||||
placeholder,
|
||||
registration,
|
||||
error,
|
||||
handleFocus,
|
||||
}: FormFieldProps) {
|
||||
return (
|
||||
<div className="form-control">
|
||||
@@ -31,6 +33,7 @@ export default function FormField({
|
||||
className={`input input-bordered focus:input-primary ${
|
||||
error ? "input-error" : ""
|
||||
}`}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
{error && <p className="text-error">{error}</p>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import sf_mini from "../../assets/sf_mini.png";
|
||||
|
||||
interface SaajanProps {
|
||||
message: string;
|
||||
position?: "top" | "left" | "right" | "bottom";
|
||||
}
|
||||
|
||||
export default function Saajan({ message, position = "right" }: SaajanProps) {
|
||||
const [animate, setAnimate] = useState<boolean>(false);
|
||||
const [tooltipPosition, setTooltipPosition] =
|
||||
useState<string>("tooltip-right");
|
||||
const [alignment, setAlignment] = useState<string>("justify-start");
|
||||
|
||||
useEffect(() => {
|
||||
setAnimate(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setAnimate(false);
|
||||
}, 1000);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full flex ${alignment}`}>
|
||||
<div
|
||||
className={`tooltip tooltip-open ${tooltipPosition} before:max-w-xs before:whitespace-pre-line italic before:text-left`}
|
||||
data-tip={message}
|
||||
>
|
||||
<img
|
||||
src={sf_mini}
|
||||
alt="saajan"
|
||||
className={`sepia-20 w-35 -mb-3 ${animate ? "animate-[pulse_.5s_ease_2]" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user