mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
refactor: UX enhancement for drawer
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { GearFineIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface DrawerSectionProps {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -21,7 +23,7 @@ export function DrawerSection({
|
||||
className={`overflow-visible join-item group flex flex-col transition-colors ${isOpen ? "bg-base-300/30" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`transition-all duration-1000 ease-in-out bg-neutral/10 ${
|
||||
className={`transition-normal duration-1000 ease-in-out bg-neutral/10 ${
|
||||
isOpen
|
||||
? "max-h-125 opacity-100 py-3 border-b border-base-content/5"
|
||||
: "max-h-0 opacity-0 pointer-events-none"
|
||||
@@ -33,7 +35,7 @@ export function DrawerSection({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`w-full p-[24px_28px] cursor-pointer flex items-center gap-5 transition-all duration-1000 outline-none focus-visible:ring-2 focus-visible:ring-primary/50 border border-base-content/10 text-left bg-linear-to-r from-transparent to-base-100/40`}
|
||||
className={`w-full p-[24px_28px] cursor-pointer flex items-center gap-5 transition-none outline-none focus-visible:ring-2 focus-visible:ring-primary/50 border border-base-content/10 text-left bg-linear-to-r from-transparent to-base-100/40`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div
|
||||
@@ -49,6 +51,16 @@ export function DrawerSection({
|
||||
{count}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{id === "vault" ? (
|
||||
<GearFineIcon
|
||||
className={
|
||||
"-mt-3 group-hover:animate-[spin_8s_ease-in-out_1] group-hover:text-neutral-content text-neutral"
|
||||
}
|
||||
weight={"duotone"}
|
||||
size={30}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`w-8 h-1 rounded-sm transition-all duration-300 bg-neutral ${
|
||||
isOpen
|
||||
@@ -58,6 +70,7 @@ export function DrawerSection({
|
||||
>
|
||||
<div className="absolute -top-1 left-1.75 w-5 h-px bg-base-content/5" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export function LetterItem({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNavigate}
|
||||
className="p-4 border-base-content/3 flex items-start gap-4 hover:bg-base-300 transition-transform duration-100 group text-left cursor-pointer w-9/12 mx-auto hover:scale-120 hover:h-24 hover:-translate-y-3 hover:pb-4 hover:border-x-5 hover:border-t-5 border-t-2 hover:-mb-2"
|
||||
className="p-4 border-base-content/3 flex items-start gap-4 hover:bg-base-300 transition-all duration-100 group text-left cursor-pointer w-9/12 mx-auto hover:scale-120 hover:h-24 hover:-translate-y-3 hover:pb-4 hover:border-x-5 hover:border-t-5 border-t-2 hover:-mb-2"
|
||||
>
|
||||
<div className="text-[0.85rem] italic text-base-content/40 flex-1 truncate group-hover:text-base-content/60 transition-none">
|
||||
{preview}
|
||||
|
||||
@@ -18,14 +18,8 @@ export default function Drawer() {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const toggleSection = (id: string) =>
|
||||
setOpenSection(openSection === id ? null : id);
|
||||
|
||||
function PasskeyModal() {
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
||||
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
|
||||
|
||||
{isAuthRequired && (
|
||||
<div className="modal modal-open bg-base-100/20 backdrop-blur-md">
|
||||
<div className="modal-box p-12 flex flex-col items-center">
|
||||
<LockKeyIcon
|
||||
@@ -50,7 +44,7 @@ export default function Drawer() {
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const password = formData.get("password") as string;
|
||||
if (!password) return;
|
||||
unlock(password);
|
||||
await unlock(password);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
@@ -61,17 +55,24 @@ export default function Drawer() {
|
||||
className="font-sans validator input input-bordered rounded-r-none"
|
||||
/>
|
||||
<div className="validator-message text-xs text-error"></div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary rounded-l-none"
|
||||
>
|
||||
<button type="submit" className="btn btn-primary rounded-l-none">
|
||||
Unlock
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
const toggleSection = (id: string) =>
|
||||
setOpenSection(openSection === id ? null : id);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
||||
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
|
||||
|
||||
{isAuthRequired && <PasskeyModal />}
|
||||
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-1000">
|
||||
<Logo />
|
||||
<div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2">
|
||||
|
||||
@@ -9,20 +9,6 @@ import Reader from "./Reader";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
// Spy on crypto methods so we don't have to do actual decryption in the UI test
|
||||
const spyDecryptLetter = vi.spyOn(
|
||||
CryptoUtils.prototype,
|
||||
"decryptLetterWithSharingKey",
|
||||
);
|
||||
const spyDecryptMetadata = vi.spyOn(
|
||||
CryptoUtils.prototype,
|
||||
"decryptMetadataWithSharingKey",
|
||||
);
|
||||
const spyDecryptImage = vi.spyOn(
|
||||
CryptoUtils.prototype,
|
||||
"decryptImageWithSharingKey",
|
||||
);
|
||||
|
||||
// Fabric.js needs to know when fonts are loaded
|
||||
Object.defineProperty(document, "fonts", {
|
||||
value: { ready: Promise.resolve() },
|
||||
@@ -30,13 +16,16 @@ Object.defineProperty(document, "fonts", {
|
||||
});
|
||||
|
||||
describe("Reader Page", () => {
|
||||
beforeEach(() => {
|
||||
let masterKey: CryptoKey;
|
||||
let utils: CryptoUtils;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Default mock behavior for successful decryption
|
||||
spyDecryptLetter.mockResolvedValue('{"objects": []}');
|
||||
spyDecryptMetadata.mockResolvedValue({ recipient: "Guest" });
|
||||
spyDecryptImage.mockResolvedValue("blob:url");
|
||||
utils = new CryptoUtils();
|
||||
await utils.initialize();
|
||||
const bundle = await CryptoUtils.deriveKeyBundle("password", "salt");
|
||||
masterKey = bundle.masterKey;
|
||||
|
||||
// Clear the URL hash
|
||||
vi.stubGlobal("location", {
|
||||
@@ -61,19 +50,27 @@ describe("Reader Page", () => {
|
||||
|
||||
it("should load and decrypt the letter when a valid key is provided", async () => {
|
||||
const mockPublicId = "test-uuid";
|
||||
const mockKey = "fake-key";
|
||||
const letterContent = JSON.stringify({ objects: [] });
|
||||
const metadata = { recipient: "Guest" };
|
||||
|
||||
const encryptedLetter = await utils.encryptLetter(letterContent, masterKey);
|
||||
const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey);
|
||||
|
||||
const sharingKey = encryptedLetter.sharingKey as string;
|
||||
|
||||
server.use(
|
||||
http.get(`${API_URL}${endpoints.LETTERS}${mockPublicId}/`, () => {
|
||||
return HttpResponse.json({
|
||||
encrypted_content: "packed-content",
|
||||
encrypted_metadata: "packed-metadata",
|
||||
encrypted_content: encryptedLetter.encrypted_content,
|
||||
encrypted_metadata: encryptedMetadata.encrypted_content, // Reader expects .encrypted_content for metadata too
|
||||
encrypted_dek: encryptedLetter.encrypted_dek,
|
||||
images: [],
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[`/read/${mockPublicId}#${mockKey}`]}>
|
||||
<MemoryRouter initialEntries={[`/read/${mockPublicId}#${sharingKey}`]}>
|
||||
<Routes>
|
||||
<Route path="/read/:public_id" element={<Reader />} />
|
||||
</Routes>
|
||||
@@ -81,11 +78,10 @@ describe("Reader Page", () => {
|
||||
);
|
||||
|
||||
// Should show loading state first
|
||||
expect(screen.getByText(/Decrypting.../i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Breaking the seal.../i)).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByText(/A sealed message for/i),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText(/A sealed letter for/i)).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/Guest/i).length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should display an error message if the server request fails", async () => {
|
||||
|
||||
Reference in New Issue
Block a user