refactor: implement reusable Modal component
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { LockKeyIcon } from "@phosphor-icons/react";
|
||||
import { Modal } from "../ui/Modal";
|
||||
|
||||
interface PasskeyModalProps {
|
||||
onUnlock: (password: string) => Promise<void>;
|
||||
@@ -6,47 +7,45 @@ interface PasskeyModalProps {
|
||||
|
||||
export function PasskeyModal({ onUnlock }: PasskeyModalProps) {
|
||||
return (
|
||||
<div className="modal modal-open bg-base-100/20 backdrop-blur-md z-100">
|
||||
<div className="modal-box p-12 flex flex-col items-center">
|
||||
<LockKeyIcon
|
||||
size={48}
|
||||
className="text-primary mx-auto mb-8 animate-pulse"
|
||||
/>
|
||||
<h3 className="font-bold text-lg font-display text-primary">
|
||||
Authentication Required
|
||||
</h3>
|
||||
<p className="py-4 font-sans">
|
||||
We need your passkey to open your letters
|
||||
</p>
|
||||
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
|
||||
<p className="text-xs text-neutral-content/30 font-mono italic">
|
||||
Your passkey is used to decrypt your data locally.
|
||||
</p>
|
||||
<div className="modal-action items-center gap-4">
|
||||
<form
|
||||
className="form-control w-full inline-flex"
|
||||
onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const password = formData.get("password") as string;
|
||||
if (!password) return;
|
||||
await onUnlock(password);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
name="password"
|
||||
required
|
||||
type="password"
|
||||
placeholder="password"
|
||||
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">
|
||||
Unlock
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<Modal isOpen={true}>
|
||||
<LockKeyIcon
|
||||
size={48}
|
||||
className="text-primary mx-auto mb-8 animate-pulse"
|
||||
/>
|
||||
<h3 className="font-bold text-lg font-display text-primary">
|
||||
Authentication Required
|
||||
</h3>
|
||||
<p className="py-4 font-sans">
|
||||
We need your passkey to open your letters
|
||||
</p>
|
||||
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
|
||||
<p className="text-xs text-neutral-content/30 font-mono italic">
|
||||
Your passkey is used to decrypt your data locally.
|
||||
</p>
|
||||
<div className="modal-action items-center gap-4">
|
||||
<form
|
||||
className="form-control w-full inline-flex"
|
||||
onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const password = formData.get("password") as string;
|
||||
if (!password) return;
|
||||
await onUnlock(password);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
name="password"
|
||||
required
|
||||
type="password"
|
||||
placeholder="password"
|
||||
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">
|
||||
Unlock
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LockIcon } from "@phosphor-icons/react";
|
||||
import type { NavigateFunction } from "react-router-dom";
|
||||
import { PATHS, ROUTES } from "../../config/routes";
|
||||
import { Modal } from "../ui/Modal";
|
||||
|
||||
interface PostSealModalProps {
|
||||
sealedTargetId: string | null;
|
||||
@@ -13,72 +14,68 @@ export function PostSealModal({
|
||||
navigate,
|
||||
type = "KEPT",
|
||||
}: PostSealModalProps) {
|
||||
if (!sealedTargetId) return null;
|
||||
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.
|
||||
<Modal isOpen={!!sealedTargetId}>
|
||||
<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>
|
||||
{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">
|
||||
{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">
|
||||
{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...
|
||||
Keep it to myself
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TrayIcon,
|
||||
VaultIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Modal } from "../ui/Modal";
|
||||
|
||||
interface ToolBarProps {
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
@@ -150,65 +151,62 @@ export function VaultConfirmModal({
|
||||
setUnlockDate,
|
||||
}: VaultConfirmModalProps) {
|
||||
return (
|
||||
<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"
|
||||
<Modal isOpen={true}>
|
||||
<VaultIcon
|
||||
size={48}
|
||||
className="text-primary mx-auto mb-8 animate-pulse"
|
||||
/>
|
||||
<h3 className="font-serif text-3xl">Take it away, then?</h3>
|
||||
<p className="text-base-content/60 text-sm text-center mt-4">
|
||||
By vaulting this letter, you ask me to hold on to this.
|
||||
<br />
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const unlockDateStr = formData.get("vault-date") as string;
|
||||
const newUnlockDate = new Date(unlockDateStr);
|
||||
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
|
||||
</div>
|
||||
<input
|
||||
required
|
||||
type="date"
|
||||
className="input input-bordered w-full"
|
||||
name="vault-date"
|
||||
/>
|
||||
<h3 className="font-serif text-3xl">Take it away, then?</h3>
|
||||
<p className="text-base-content/60 text-sm text-center mt-4">
|
||||
By vaulting this letter, you ask me to hold on to this.
|
||||
<br />
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
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
|
||||
</div>
|
||||
<input
|
||||
required
|
||||
type="date"
|
||||
className="input input-bordered w-full"
|
||||
name="vault-date"
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { CampfireIcon, FlameIcon, XCircleIcon } from "@phosphor-icons/react";
|
||||
import { CampfireIcon, FlameIcon } from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal } from "../ui/Modal";
|
||||
|
||||
interface BurnModalProps {
|
||||
burnLetter: () => void;
|
||||
isBurning: boolean;
|
||||
setShowBurnModal: (show: boolean) => void;
|
||||
setRevealState: (state: "sealed" | "revealed" | "burning" | "burned") => void;
|
||||
setRevealState: (state: "SEALED" | "REVEALED" | "BURNING" | "BURNED") => void;
|
||||
}
|
||||
|
||||
export function BurnModal({
|
||||
@@ -20,7 +21,7 @@ export function BurnModal({
|
||||
useEffect(() => {
|
||||
if (!burnClicked) return;
|
||||
if (flameOn === 100) {
|
||||
setRevealState("sealed");
|
||||
setRevealState("SEALED");
|
||||
burnLetter();
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
@@ -33,23 +34,15 @@ export function BurnModal({
|
||||
const burnStyle = flameOn < 30 ? "" : `contrast(${flameOn / 30})`;
|
||||
|
||||
return (
|
||||
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md">
|
||||
<Modal isOpen={true} onClose={() => setShowBurnModal(false)}>
|
||||
<div
|
||||
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]" : ""}`}
|
||||
className={`flex flex-col items-center gap-4 text-center transition-all duration-200 ease-in-out ${burnClicked ? "animate-[pulse_15s_linear_infinite]" : ""}`}
|
||||
style={
|
||||
{
|
||||
transform: `rotate(${rotate}deg)`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<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"
|
||||
@@ -101,6 +94,6 @@ export function BurnModal({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@ import { useNavigate } from "react-router-dom";
|
||||
import { ROUTES } from "../../config/routes";
|
||||
|
||||
interface PostActionOverlayProps {
|
||||
revealState: "sealed" | "revealed" | "burning" | "burned";
|
||||
revealState: "SEALED" | "REVEALED" | "BURNING" | "BURNED";
|
||||
}
|
||||
|
||||
export function PostActionOverlay({ revealState }: PostActionOverlayProps) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-center min-h-screen bg-base-100 ${revealState === "burned" ? "opacity-100" : "opacity-0"} transition-all delay-300 duration-1000`}
|
||||
className={`flex flex-col items-center justify-center min-h-screen bg-base-100 ${revealState === "BURNED" ? "opacity-100" : "opacity-0"} transition-all delay-1000 duration-1000`}
|
||||
>
|
||||
<h1
|
||||
className={`text-6xl ${revealState === "burned" ? "opacity-100" : "opacity-0"} lg:text-9xl italic font-extralight text-base-content animate-[pulse_3s_ease-in-out_3]`}
|
||||
className={`text-6xl ${revealState === "BURNED" ? "opacity-100" : "opacity-0"} lg:text-9xl italic font-extralight text-base-content animate-[pulse_3s_ease-in-out_3]`}
|
||||
>
|
||||
It is done
|
||||
</h1>
|
||||
<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">
|
||||
May your <span className="italic text-primary">soul</span> find
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
EyeSlashIcon,
|
||||
PaperPlaneTiltIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { EyeSlashIcon, PaperPlaneTiltIcon } from "@phosphor-icons/react";
|
||||
import { Modal } from "../ui/Modal";
|
||||
import Saajan from "../ui/Saajan";
|
||||
|
||||
interface ShareModalProps {
|
||||
shareLink: string | null;
|
||||
@@ -15,16 +13,8 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
|
||||
await navigator.clipboard.writeText(shareLink);
|
||||
};
|
||||
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>
|
||||
<>
|
||||
<Modal isOpen={!!shareLink} onClose={() => setShareLink(null)}>
|
||||
<div className="flex flex-col items-center justify-center text-center gap-6 py-4">
|
||||
<div className="space-y-2">
|
||||
<PaperPlaneTiltIcon
|
||||
@@ -34,14 +24,17 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
|
||||
/>
|
||||
<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.
|
||||
You've carried these words long enough.
|
||||
<br />
|
||||
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.
|
||||
They'll receive it exactly as you're seeing it now.
|
||||
<br />
|
||||
Nothing more, nothing less.
|
||||
</blockquote>
|
||||
</div>
|
||||
<div className="w-full flex items-center gap-2 bg-base-300 p-2 rounded-xl">
|
||||
@@ -69,7 +62,13 @@ export function ShareModal({ shareLink, setShareLink }: ShareModalProps) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="absolute bottom-0 z-1000 font-sans w-full">
|
||||
<Saajan
|
||||
position="top"
|
||||
message={`Someone once said,\n"To send a letter is a good way to go somewhere without moving anything but your heart."\nThey were not wrong.`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WarningIcon, XCircleIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { WarningIcon } from "@phosphor-icons/react";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
interface LogModalContent {
|
||||
status: "WARN" | "ERROR" | "RESET" | "SUCCESS";
|
||||
@@ -15,40 +16,24 @@ export const LogModal = ({
|
||||
onClose,
|
||||
status,
|
||||
}: LogModalContent) => {
|
||||
return status === "RESET" || !isOpen ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<div className="modal modal-open modal-middle bg-base-100/20 backdrop-blur-md z-100">
|
||||
<div className="modal-box bg-transparent border-none shadow-none relative">
|
||||
<div
|
||||
className={`alert ${status === "WARN" ? "alert-warning" : "alert-error"} flex flex-col items-center text-center gap-6 py-4`}
|
||||
>
|
||||
{status === "WARN" && (
|
||||
<WarningIcon className="text-warning" size={16} weight="bold" />
|
||||
)}
|
||||
{status === "ERROR" && (
|
||||
<XCircleIcon className="text-error" size={16} weight="bold" />
|
||||
)}
|
||||
{message}
|
||||
<div className="divider text-primary-content text-xs uppercase tracking-widest">
|
||||
Error Stack
|
||||
</div>
|
||||
<div className="mockup-code bg-base-100 text-error w-full">
|
||||
<pre>
|
||||
<code>{String(log)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<form method="dialog">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn btn-sm btn-circle btn-ghost absolute right-6 top-6"
|
||||
>
|
||||
<XIcon size={6} weight="bold" />
|
||||
</button>
|
||||
</form>
|
||||
return (
|
||||
<Modal isOpen={isOpen && status !== "RESET"} onClose={onClose}>
|
||||
<div
|
||||
className={`alert ${status === "WARN" ? "alert-warning" : "alert-error"} flex flex-col items-center text-center gap-6 py-4`}
|
||||
>
|
||||
{status === "WARN" && (
|
||||
<WarningIcon className="text-warning" size={16} weight="duotone" />
|
||||
)}
|
||||
{message}
|
||||
<div className="divider text-primary-content text-xs uppercase tracking-widest">
|
||||
Error Stack
|
||||
</div>
|
||||
<div className="mockup-code bg-base-100 text-error w-full">
|
||||
<pre>
|
||||
<code>{String(log)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { XCircleIcon } from "@phosphor-icons/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal modal-open modal-middle backdrop-blur-md before:absolute before:top-0 before:left-0 before:w-full before:h-full before:content-[''] before:opacity-[0.03] before:z-10 before:pointer-events-none before:bg-[url('assets/noise.gif')]">
|
||||
<div className="modal-box relative bg-base-100/60 flex flex-col items-center text-center gap-6">
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2 z-20"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<XCircleIcon size={18} weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user