mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
feat: add a post-seal navigation flow to the reader page
This commit is contained in:
+43
-22
@@ -76,7 +76,9 @@ test.describe("Letter Drafting (Real Backend)", () => {
|
|||||||
await expect(canvasInput).toHaveValue(/It should persist/i);
|
await expect(canvasInput).toHaveValue(/It should persist/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should seal a letter and show sharing link", async ({ page }) => {
|
test("should seal a letter and navigate to Reader, then share on demand", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
const timestamp = Date.now() + Math.random();
|
const timestamp = Date.now() + Math.random();
|
||||||
const email = `seal-${timestamp}@example.com`;
|
const email = `seal-${timestamp}@example.com`;
|
||||||
const name = `Seal Author ${timestamp}`;
|
const name = `Seal Author ${timestamp}`;
|
||||||
@@ -94,36 +96,53 @@ test.describe("Letter Drafting (Real Backend)", () => {
|
|||||||
await canvasInput.focus();
|
await canvasInput.focus();
|
||||||
await canvasInput.fill("This letter will be sealed and shared.");
|
await canvasInput.fill("This letter will be sealed and shared.");
|
||||||
|
|
||||||
// Click Seal
|
// Click Seal (open menu, then confirm)
|
||||||
logger.info(">> [Seal] Clicking Seal...");
|
logger.info(">> [Seal] Clicking Seal...");
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: /seal/i })
|
.getByRole("button", { name: /seal/i })
|
||||||
.filter({ visible: true })
|
.filter({ visible: true })
|
||||||
.click(); // Open menu
|
.click();
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: /seal/i })
|
.getByRole("button", { name: /seal/i })
|
||||||
.filter({ visible: true })
|
.filter({ visible: true })
|
||||||
.click(); // Click confirm Seal
|
.click();
|
||||||
|
|
||||||
// Verify "Sealed & Ready" modal
|
// Should show sealed confirmation modal
|
||||||
logger.info(">> [Seal] Verifying sharing modal...");
|
logger.info(">> [Seal] Verifying sealed modal...");
|
||||||
await expect(page.getByText(/sealed & ready/i)).toBeVisible();
|
await expect(page.getByText(/your letter is sealed/i)).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
// Verify sharing link contains a hash (the key)
|
// Navigate to Reader via "View letter"
|
||||||
const linkInput = page.locator("input[readOnly]");
|
await page.getByRole("button", { name: /view letter/i }).click();
|
||||||
|
|
||||||
|
// Should be on Reader URL
|
||||||
|
await expect(page).toHaveURL(/\/read\/[a-f0-9-]{36}$/, { timeout: 15000 });
|
||||||
|
|
||||||
|
// Open the envelope to reveal the letter
|
||||||
|
await expect(page.getByText(/breaking the seal/i)).toBeHidden({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
await page.getByAltText("Seal").click();
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
await page.locator("#letter").click({ position: { x: 30, y: 15 } });
|
||||||
|
await expect(page.locator("#letter")).toBeHidden({ timeout: 20000 });
|
||||||
|
|
||||||
|
// Share on demand
|
||||||
|
logger.info(">> [Seal] Clicking Share button in Reader...");
|
||||||
|
await page.locator("#share-letter-btn").click();
|
||||||
|
|
||||||
|
// Verify share modal with a valid link
|
||||||
|
await expect(page.getByText(/share this letter/i)).toBeVisible();
|
||||||
|
const linkInput = page.locator("#share-link-input");
|
||||||
const linkValue = await linkInput.inputValue();
|
const linkValue = await linkInput.inputValue();
|
||||||
|
|
||||||
expect(linkValue).toContain("/read/");
|
expect(linkValue).toContain("/read/");
|
||||||
expect(linkValue).toContain("#");
|
expect(linkValue).toContain("#");
|
||||||
|
logger.info(`>> [Seal] Sharing link: ${linkValue}`);
|
||||||
|
|
||||||
logger.info(`>> [Seal] Sharing link generated: ${linkValue}`);
|
|
||||||
|
|
||||||
// Verify "Copy" button works
|
|
||||||
await expect(page.getByRole("button", { name: /copy/i })).toBeVisible();
|
await expect(page.getByRole("button", { name: /copy/i })).toBeVisible();
|
||||||
|
|
||||||
// Close modal
|
|
||||||
await page.getByRole("button", { name: /close/i }).click();
|
await page.getByRole("button", { name: /close/i }).click();
|
||||||
await expect(page.getByText(/sealed & ready/i)).toBeHidden();
|
await expect(page.getByText(/share this letter/i)).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should allow author to access sealed letter from drawer without sharing key", async ({
|
test("should allow author to access sealed letter from drawer without sharing key", async ({
|
||||||
@@ -148,19 +167,21 @@ test.describe("Letter Drafting (Real Backend)", () => {
|
|||||||
await canvasInput.focus();
|
await canvasInput.focus();
|
||||||
await canvasInput.fill(letterContent);
|
await canvasInput.fill(letterContent);
|
||||||
|
|
||||||
// Click Seal
|
// Click Seal (open menu, then confirm)
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: /seal/i })
|
.getByRole("button", { name: /seal/i })
|
||||||
.filter({ visible: true })
|
.filter({ visible: true })
|
||||||
.click(); // Open menu
|
.click();
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: /seal/i })
|
.getByRole("button", { name: /seal/i })
|
||||||
.filter({ visible: true })
|
.filter({ visible: true })
|
||||||
.click(); // Click confirm Seal
|
.click();
|
||||||
await expect(page.getByText(/sealed & ready/i)).toBeVisible();
|
|
||||||
|
|
||||||
// Close modal
|
// Sealed modal should appear — click "Keep it" to go to Drawer
|
||||||
await page.getByRole("button", { name: /close/i }).click();
|
await expect(page.getByText(/your letter is sealed/i)).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
await page.getByRole("button", { name: /keep it/i }).click();
|
||||||
|
|
||||||
// Navigate to Drawer - use ID or precise label
|
// Navigate to Drawer - use ID or precise label
|
||||||
logger.info(">> [Drawer] Navigating to Drawer...");
|
logger.info(">> [Drawer] Navigating to Drawer...");
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
StampIcon,
|
StampIcon,
|
||||||
TrayIcon,
|
TrayIcon,
|
||||||
VaultIcon,
|
VaultIcon,
|
||||||
XCircleIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
@@ -26,7 +25,7 @@ import DateDisplay from "../components/ui/DateDisplay";
|
|||||||
import { LogModal } from "../components/ui/LogModal";
|
import { LogModal } from "../components/ui/LogModal";
|
||||||
import { Navbar } from "../components/ui/Navbar";
|
import { Navbar } from "../components/ui/Navbar";
|
||||||
import { endpoints } from "../config/endpoints";
|
import { endpoints } from "../config/endpoints";
|
||||||
import { PATHS } from "../config/routes";
|
import { PATHS, ROUTES } from "../config/routes";
|
||||||
import { useKeyStore } from "../store/useKeyStore";
|
import { useKeyStore } from "../store/useKeyStore";
|
||||||
import { CryptoUtils } from "../utils/crypto";
|
import { CryptoUtils } from "../utils/crypto";
|
||||||
import { formatRelativeDate } from "../utils/dateFormat";
|
import { formatRelativeDate } from "../utils/dateFormat";
|
||||||
@@ -60,7 +59,7 @@ export default function Editor() {
|
|||||||
}>({ status: "RESET", message: "", log: "" });
|
}>({ status: "RESET", message: "", log: "" });
|
||||||
|
|
||||||
const [isInitialLoading, setIsInitialLoading] = useState(false);
|
const [isInitialLoading, setIsInitialLoading] = useState(false);
|
||||||
const [shareLink, setShareLink] = useState<string | null>(null);
|
const [sealedTargetId, setSealedTargetId] = useState<string | null>(null);
|
||||||
const [lastSaved, setLastSaved] = useState<string | null>(null);
|
const [lastSaved, setLastSaved] = useState<string | null>(null);
|
||||||
const [status, setLetterStatus] = useState<"DRAFT" | "SEALED" | "VAULT">(
|
const [status, setLetterStatus] = useState<"DRAFT" | "SEALED" | "VAULT">(
|
||||||
"DRAFT",
|
"DRAFT",
|
||||||
@@ -165,8 +164,6 @@ export default function Editor() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(canvasData);
|
|
||||||
|
|
||||||
if (canvasRef.current) {
|
if (canvasRef.current) {
|
||||||
await canvasRef.current.loadData(canvasData);
|
await canvasRef.current.loadData(canvasData);
|
||||||
}
|
}
|
||||||
@@ -271,7 +268,6 @@ export default function Editor() {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (status === "VAULT") {
|
if (status === "VAULT") {
|
||||||
const finalDate = vaultDate || unlockDate;
|
const finalDate = vaultDate || unlockDate;
|
||||||
console.log(finalDate?.toISOString());
|
|
||||||
formData.append("type", "VAULT");
|
formData.append("type", "VAULT");
|
||||||
if (finalDate) {
|
if (finalDate) {
|
||||||
formData.append("unlock_at", finalDate.toISOString());
|
formData.append("unlock_at", finalDate.toISOString());
|
||||||
@@ -305,27 +301,59 @@ export default function Editor() {
|
|||||||
setLetterStatus(status);
|
setLetterStatus(status);
|
||||||
setLastSavedPulseTick((prev) => prev + 1);
|
setLastSavedPulseTick((prev) => prev + 1);
|
||||||
|
|
||||||
if (status === "SEALED" && encrypted_letter.sharingKey) {
|
if (status === "SEALED") {
|
||||||
const link = `${window.location.origin}${PATHS.read(
|
setSealedTargetId(targetId);
|
||||||
targetId,
|
|
||||||
)}#${encrypted_letter.sharingKey}`;
|
|
||||||
setShareLink(link);
|
|
||||||
setShowSaveOverlay(false);
|
|
||||||
setTimeout(() => setSaveOverlay("idle"), OVERLAY_FADE_MS);
|
|
||||||
} else {
|
|
||||||
setSaveOverlay("saved");
|
|
||||||
setShowSaveOverlay(true);
|
|
||||||
}
|
}
|
||||||
|
setSaveOverlay("saved");
|
||||||
|
setShowSaveOverlay(true);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
setSaveOverlay("error");
|
setSaveOverlay("error");
|
||||||
setShowSaveOverlay(true);
|
setShowSaveOverlay(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
function SealedModal() {
|
||||||
if (!shareLink) return;
|
if (!sealedTargetId) return null;
|
||||||
await navigator.clipboard.writeText(shareLink);
|
return (
|
||||||
};
|
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-1000">
|
||||||
|
<div className="modal-box flex flex-col items-center text-center gap-6">
|
||||||
|
<LockIcon size={32} weight="duotone" className="text-primary mt-3" />
|
||||||
|
<h3 className="font-serif text-2xl">Your letter is sealed</h3>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ToolBar() {
|
function ToolBar() {
|
||||||
return (
|
return (
|
||||||
@@ -547,53 +575,7 @@ export default function Editor() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shareLink && (
|
{saveOverlay !== "idle" && (
|
||||||
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-100">
|
|
||||||
<div className="modal-box bg-base-100 border border-base-content/5 shadow-2xl relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
|
||||||
onClick={() => setShareLink(null)}
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
<XCircleIcon size={18} weight="bold" />
|
|
||||||
</button>
|
|
||||||
<div className="flex flex-col items-center text-center gap-6 py-4">
|
|
||||||
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center">
|
|
||||||
<LockIcon size={32} weight="fill" className="text-primary" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-serif text-3xl">Sealed & Ready</h3>
|
|
||||||
<p className="text-base-content/60 text-sm max-w-xs">
|
|
||||||
This letter is now encrypted. Share this secret link with
|
|
||||||
your recipient.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full flex items-center gap-2 bg-base-300 p-2 rounded-xl group relative">
|
|
||||||
<input
|
|
||||||
readOnly
|
|
||||||
value={shareLink}
|
|
||||||
className="flex-1 bg-transparent text-xs font-mono px-2 overflow-hidden text-ellipsis whitespace-nowrap outline-none"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
className="btn btn-primary btn-sm rounded-lg"
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-[10px] uppercase tracking-widest text-base-content/30">
|
|
||||||
Zero-Knowledge: The key is in the link, not our servers.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{saveOverlay !== "idle" && !shareLink && (
|
|
||||||
<div
|
<div
|
||||||
className={`modal modal-open bg-base-100/20 backdrop-blur-md transition-opacity duration-300 ${
|
className={`modal modal-open bg-base-100/20 backdrop-blur-md transition-opacity duration-300 ${
|
||||||
showSaveOverlay ? "opacity-100" : "opacity-0"
|
showSaveOverlay ? "opacity-100" : "opacity-0"
|
||||||
@@ -650,6 +632,7 @@ export default function Editor() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{confirmModal === "VAULT" && <VaultConfirm />}
|
{confirmModal === "VAULT" && <VaultConfirm />}
|
||||||
|
{sealedTargetId && <SealedModal />}
|
||||||
|
|
||||||
<div className="max-w-180 mx-auto px-1 md:px-0">
|
<div className="max-w-180 mx-auto px-1 md:px-0">
|
||||||
<div className="flex justify-between items-end mb-16 border-b border-base-content/5 pb-8 px-0">
|
<div className="flex justify-between items-end mb-16 border-b border-base-content/5 pb-8 px-0">
|
||||||
|
|||||||
+151
-31
@@ -1,4 +1,10 @@
|
|||||||
import { CampfireIcon, FlameIcon } from "@phosphor-icons/react";
|
import {
|
||||||
|
CampfireIcon,
|
||||||
|
EyeSlashIcon,
|
||||||
|
FlameIcon,
|
||||||
|
PaperPlaneTiltIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import { api } from "../api/apiClient";
|
import { api } from "../api/apiClient";
|
||||||
@@ -51,10 +57,92 @@ export default function Reader() {
|
|||||||
const [showBurnModal, setShowBurnModal] = useState(false);
|
const [showBurnModal, setShowBurnModal] = useState(false);
|
||||||
const [isBurning, setIsBurning] = useState(false);
|
const [isBurning, setIsBurning] = useState(false);
|
||||||
const [ignite, setIgnite] = useState(false);
|
const [ignite, setIgnite] = useState(false);
|
||||||
|
const [encryptedDek, setEncryptedDek] = useState<string | null>(null);
|
||||||
|
const [shareLink, setShareLink] = useState<string | null>(null);
|
||||||
|
|
||||||
const { masterKey } = useKeyStore();
|
const { masterKey } = useKeyStore();
|
||||||
|
|
||||||
const isOwner = !!masterKey && !sharingKey;
|
const isAuthor = !!masterKey && !sharingKey;
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
if (!encryptedDek || !masterKey || !public_id) return;
|
||||||
|
const cryptoUtils = new CryptoUtils();
|
||||||
|
const key = await cryptoUtils.extractSharingKey(encryptedDek, masterKey);
|
||||||
|
try {
|
||||||
|
await api.patch(`${endpoints.LETTERS}${public_id}/`, { type: "SENT" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update letter:", err);
|
||||||
|
} finally {
|
||||||
|
setShareLink(`${window.location.origin}${PATHS.read(public_id)}#${key}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
if (!shareLink) return;
|
||||||
|
await navigator.clipboard.writeText(shareLink);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ShareModal() {
|
||||||
|
return (
|
||||||
|
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-100">
|
||||||
|
<div className="modal-box bg-base-100 border border-base-content/5 shadow-2xl relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||||
|
onClick={() => setShareLink(null)}
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<XCircleIcon size={18} weight="bold" />
|
||||||
|
</button>
|
||||||
|
<div className="flex flex-col items-center justify-center text-center gap-6 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<PaperPlaneTiltIcon
|
||||||
|
size={48}
|
||||||
|
weight="bold"
|
||||||
|
className="mb-4 text-primary mx-auto animate-[bounce_3s_ease-in-out_infinite]"
|
||||||
|
/>
|
||||||
|
<h3 className="font-serif text-3xl">Send this letter</h3>
|
||||||
|
<p className="text-base-content/80 text-sm font-sans mt-4">
|
||||||
|
You've carried these words long enough. Send your letter now,
|
||||||
|
and let the{" "}
|
||||||
|
<span className="text-accent font-display">unsaid</span> finally
|
||||||
|
find its home.
|
||||||
|
</p>
|
||||||
|
<div className="divider mx-auto" />
|
||||||
|
<blockquote className="text-sm info text-neutral-content/60 font-sans">
|
||||||
|
The recipient will have the same viewing experience like you do
|
||||||
|
now.
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex items-center gap-2 bg-base-300 p-2 rounded-xl">
|
||||||
|
<input
|
||||||
|
id="share-link-input"
|
||||||
|
readOnly
|
||||||
|
value={shareLink ?? ""}
|
||||||
|
className="flex-1 bg-transparent text-xs font-mono px-2 overflow-hidden text-ellipsis whitespace-nowrap outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="btn btn-primary font-sans btn-sm rounded-tl-xl rounded-bl-xl rounded-tr-full rounded-br-full"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</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:
|
||||||
|
</p>
|
||||||
|
<p className="textarea-xs font-mono text-center">
|
||||||
|
The key never leaves your or the recipient's browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const burnLetter = async () => {
|
const burnLetter = async () => {
|
||||||
console.log("Burning letter...");
|
console.log("Burning letter...");
|
||||||
@@ -95,27 +183,42 @@ export default function Reader() {
|
|||||||
const burnStyle = flameOn < 30 ? "" : `contrast(${flameOn / 30})`;
|
const burnStyle = flameOn < 30 ? "" : `contrast(${flameOn / 30})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal modal-open bg-base-100/20 backdrop-blur-md">
|
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md">
|
||||||
<div
|
<div
|
||||||
className={`modal-box flex flex-col items-center gap-4 text-center transition-all duration-200 ease-in-out ${burnClicked ? "animate-[pulse_15s_linear_infinite]" : ""}`}
|
className={`modal-box flex flex-col items-center gap-4 py-8 text-center transition-all duration-200 ease-in-out ${burnClicked ? "animate-[pulse_15s_linear_infinite]" : ""}`}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
transform: `rotate(${rotate}deg)`,
|
transform: `rotate(${rotate}deg)`,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CampfireIcon size={36} weight="duotone" className="text-error" />
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||||
|
onClick={() => setShowBurnModal(false)}
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<XCircleIcon size={18} weight="bold" />
|
||||||
|
</button>
|
||||||
|
<CampfireIcon
|
||||||
|
size={48}
|
||||||
|
weight="duotone"
|
||||||
|
className="text-error animate-pulse"
|
||||||
|
/>
|
||||||
<h3 className="font-serif text-2xl">
|
<h3 className="font-serif text-2xl">
|
||||||
Are you ready to burn this letter?
|
Are you ready to burn this letter?
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm font-sans text-base-content/60 max-w-xs">
|
<p className="text-sm font-sans text-base-content/80 mt-4">
|
||||||
The ashes will be released into the winds.
|
Some words are meant to be unsaid, but they don't have to linger
|
||||||
|
forever.
|
||||||
<br />
|
<br />
|
||||||
<span className="text-error font-semibold">Press</span> and{" "}
|
Let the echoes of your unsaid be finally released.
|
||||||
<span className="text-error font-semibold">hold</span> the{" "}
|
|
||||||
<span className="text-amber-300 font-semibold">flame</span> to
|
|
||||||
proceed.
|
|
||||||
</p>
|
</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-amber-300">flame</span> to proceed.
|
||||||
|
</div>
|
||||||
<div className="modal-action w-full justify-center gap-3 mt-2">
|
<div className="modal-action w-full justify-center gap-3 mt-2">
|
||||||
<div
|
<div
|
||||||
className="absolute -mt-2 w-28 h-28 radial-progress pointer-events-none text-amber-200/60"
|
className="absolute -mt-2 w-28 h-28 radial-progress pointer-events-none text-amber-200/60"
|
||||||
@@ -179,6 +282,8 @@ export default function Reader() {
|
|||||||
if (status === "BURNED")
|
if (status === "BURNED")
|
||||||
throw new Error("This letter has been burned.");
|
throw new Error("This letter has been burned.");
|
||||||
|
|
||||||
|
if (encrypted_dek) setEncryptedDek(encrypted_dek);
|
||||||
|
|
||||||
const cryptoUtils = new CryptoUtils();
|
const cryptoUtils = new CryptoUtils();
|
||||||
const isShared = !!sharingKey;
|
const isShared = !!sharingKey;
|
||||||
|
|
||||||
@@ -333,13 +438,16 @@ export default function Reader() {
|
|||||||
<div
|
<div
|
||||||
className={`text-xl ${revealState === "burned" ? "opacity-100" : "opacity-0"} lg:text-4xl text-center font-extralight text-base-content font-display mt-8 delay-3000 transition-all duration-2000 tracking-wide`}
|
className={`text-xl ${revealState === "burned" ? "opacity-100" : "opacity-0"} lg:text-4xl text-center font-extralight text-base-content font-display mt-8 delay-3000 transition-all duration-2000 tracking-wide`}
|
||||||
>
|
>
|
||||||
<p className="w-full overflow-hidden">
|
<p className="w-full">
|
||||||
May your soul find solace like your{" "}
|
May your <span className="italic text-primary">soul</span> find
|
||||||
<span className="text-accent italic">unsaid</span> words did.
|
solace,
|
||||||
|
<br />
|
||||||
|
just like your <span className="text-accent italic">unsaid</span>{" "}
|
||||||
|
words did.
|
||||||
</p>
|
</p>
|
||||||
<div className="divider mx-auto w-24 text-center"></div>
|
<div className="divider mx-auto w-24 text-center"></div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-ghost text-xs text-neutral-content/60 font-sans"
|
className="btn btn-ghost text-sm text-neutral-content/60 font-sans"
|
||||||
onClick={() => navigate(ROUTES.DRAWER)}
|
onClick={() => navigate(ROUTES.DRAWER)}
|
||||||
>
|
>
|
||||||
Turn the page
|
Turn the page
|
||||||
@@ -372,27 +480,39 @@ export default function Reader() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOwner && (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<button
|
|
||||||
id="burn-letter-btn"
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost btn-sm text-error/40 hover:text-error hover:bg-error/10 gap-1.5"
|
|
||||||
onClick={() => setShowBurnModal(true)}
|
|
||||||
>
|
|
||||||
<FlameIcon size={26} weight="duotone" />
|
|
||||||
<span className="text-[10px] uppercase font-sans tracking-widest">
|
|
||||||
Burn this letter
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shareLink && <ShareModal />}
|
||||||
{showBurnModal && <BurnModal />}
|
{showBurnModal && <BurnModal />}
|
||||||
|
|
||||||
|
{isAuthor && revealState !== "burned" && (
|
||||||
|
<div className="flex justify-center gap-2 mt-8 z-10 relative">
|
||||||
|
<button
|
||||||
|
id="share-letter-btn"
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm text-base-content/30 hover:text-base-content hover:bg-base-content/10 gap-1.5"
|
||||||
|
onClick={handleShare}
|
||||||
|
>
|
||||||
|
<PaperPlaneTiltIcon size={16} weight="duotone" />
|
||||||
|
<span className="text-md uppercase font-sans tracking-widest">
|
||||||
|
Send to someone...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="burn-letter-btn"
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm text-error/40 hover:text-error hover:bg-error/10 gap-1.5"
|
||||||
|
onClick={() => setShowBurnModal(true)}
|
||||||
|
>
|
||||||
|
<FlameIcon size={16} weight="duotone" />
|
||||||
|
<span className="text-md uppercase font-sans tracking-widest">
|
||||||
|
Burn the letter
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<footer className="mt-16 text-center z-10 opacity-10 pointer-events-none">
|
<footer className="mt-16 text-center z-10 opacity-10 pointer-events-none">
|
||||||
<p className="text-xs font-sans uppercase tracking-[0.5em]">
|
<p className="text-xs font-sans uppercase tracking-[0.5em]">
|
||||||
Read. Remember. Release.
|
Read. Remember. Release.
|
||||||
|
|||||||
Reference in New Issue
Block a user