diff --git a/src/App.tsx b/src/App.tsx index afd4c76..ea31621 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,23 +3,33 @@ import Index from "./pages/Index"; import Privacy from "./pages/Privacy"; import NotFound from "./pages/NotFound"; import Waitlist from "./pages/Waitlist"; +import RequestAccountDeletion from "./pages/RequestAccountDeletion"; +import FinalizeAccountDeletion from "./pages/FinalizeAccountDeletion"; import BlogList from "./pages/BlogList"; import BlogPost from "./pages/BlogPost"; import CookieConsent from "./components/ui/CookieConsent"; +import { ToastProvider, ToastViewport } from "./components/ui/toast"; +import Toaster from "./components/ui/Toaster"; const App = () => ( <> - - + + + } /> } /> } /> + } /> + } /> } /> } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> - - + + + + + > ); diff --git a/src/components/ui/Toaster.tsx b/src/components/ui/Toaster.tsx new file mode 100644 index 0000000..c98146a --- /dev/null +++ b/src/components/ui/Toaster.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import { useToast } from "@/components/ui/use-toast"; +import { Toast, ToastTitle, ToastDescription, ToastClose } from "@/components/ui/toast"; + +const Toaster: React.FC = () => { + const { toasts } = useToast(); + + return ( + <> + {toasts.map((t) => ( + t.onOpenChange?.(open)} + variant={(t as any).variant} + > + + {t.title && {t.title}} + {t.description && {t.description}} + + + {t.action} + + + + ))} + > + ); +}; + +export default Toaster; diff --git a/src/index.css b/src/index.css index 0125945..5d68a7b 100644 --- a/src/index.css +++ b/src/index.css @@ -22,6 +22,11 @@ --color-brand: var(--color-accent-secondary); --color-brand-light: var(--color-accent-primary); --color-brand-dark: #86efac; /* Fallback or specific shade */ + + /* destructive / danger color used by some components */ + --color-destructive: #dc2626; /* red-600 */ + --color-destructive-foreground: #ffffff; + --color-red-300: #fca5a5; } @layer base { @@ -65,3 +70,10 @@ .text-secondary-foreground { color: var(--color-text-secondary); } .glass { background-color: rgba(255,255,255,0.05); } + +/* destructive / danger utilities (used by some components) */ +.bg-destructive { background-color: var(--color-destructive); } +.border-destructive { border-color: var(--color-destructive); } +.text-destructive-foreground { color: var(--color-destructive-foreground); } +.group\.\[\.destructive\]\:text-red-300 { color: var(--color-red-300); } + diff --git a/src/pages/FinalizeAccountDeletion.tsx b/src/pages/FinalizeAccountDeletion.tsx new file mode 100644 index 0000000..48632ea --- /dev/null +++ b/src/pages/FinalizeAccountDeletion.tsx @@ -0,0 +1,94 @@ +import * as React from "react"; +import { useLocation } from "react-router-dom"; +import { toast } from "@/components/ui/use-toast"; + +const FinalizeAccountDeletion: React.FC = () => { + const location = useLocation(); + const [userId, setUserId] = React.useState(null); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + const qp = new URLSearchParams(location.search); + const id = qp.get("user_id") || qp.get("userId"); + if (id) setUserId(id); + }, [location.search]); + + const onDelete = async () => { + if (!userId) { + toast({ title: "Missing user id in the URL." }); + return; + } + + setLoading(true); + try { + const envBase = (import.meta as any).env?.VITE_API_URL as string | undefined; + const base = envBase ? envBase.replace(/\/$/, "") : "http://localhost:8080"; + const url = `${base}/account/me?user_id=${encodeURIComponent(userId)}`; + + const res = await fetch(url, { method: "DELETE" }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `Status ${res.status}`); + } + + toast({ title: "Account deleted", description: "Your account has been removed." }); + setTimeout(() => { + window.location.href = "/"; + }, 700); + } catch (err: any) { + toast({ title: "Error", description: err?.message ?? String(err) }); + } finally { + setLoading(false); + } + }; + + return ( + + + + + Finalize account deletion + + + + + + + + + TailTrails + This action will permanently delete your account + + + + + + + {userId ? ( + + + + {loading ? "Deleting..." : "Delete my account"} + + + + ) : ( + + Invalid or expired deletion link. + + )} + + + + + + + + ); +}; + +export default FinalizeAccountDeletion; diff --git a/src/pages/RequestAccountDeletion.tsx b/src/pages/RequestAccountDeletion.tsx new file mode 100644 index 0000000..0b1f399 --- /dev/null +++ b/src/pages/RequestAccountDeletion.tsx @@ -0,0 +1,96 @@ +import * as React from "react"; +import { toast } from "@/components/ui/use-toast"; + +const RequestAccountDeletion: React.FC = () => { + const [email, setEmail] = React.useState(""); + const [loading, setLoading] = React.useState(false); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!email.trim()) { + toast({ title: "Please provide your email." }); + return; + } + + setLoading(true); + try { + const envBase = (import.meta as any).env?.VITE_API_URL as string | undefined; + const base = envBase ? envBase.replace(/\/$/, "") : "http://localhost:8080"; + const url = `${base}/account/request-deletion`; + + const res = await fetch(url, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `Status ${res.status}`); + } + + toast({ title: "Success", description: "Check your email for deletion instructions. Check your inbox and spam folder." }); + setEmail(""); + } catch (err: any) { + toast({ title: "Error", description: err?.message ?? String(err) }); + } finally { + setLoading(false); + } + }; + + return ( + + + + + Request account deletion + + + + + + + + + TailTrails + Request account deletion via email + + + + + + + + + Email + setEmail(e.target.value)} + placeholder="Email *" + required + /> + + + + + {loading ? "Sending..." : "Request deletion"} + + + + + + + + + + + ); +}; + +export default RequestAccountDeletion;
Invalid or expired deletion link.