Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
b4c8c8ee91
commit
95e84ad842
@@ -0,0 +1,163 @@
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Loader2, Plus, ExternalLink, Pencil, Trash2, FileText } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/settings/public-pages/")({
|
||||
component: PublicPagesSettingsPage,
|
||||
});
|
||||
|
||||
type PageRow = {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
status: string;
|
||||
show_in_nav: boolean;
|
||||
sort_order: number;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
function PublicPagesSettingsPage() {
|
||||
const { isAdmin } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pages, setPages] = useState<PageRow[]>([]);
|
||||
const [newOpen, setNewOpen] = useState(false);
|
||||
const [newTitle, setNewTitle] = useState("");
|
||||
const [newSlug, setNewSlug] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
const { data } = await supabase
|
||||
.from("public_pages")
|
||||
.select("id,slug,title,status,show_in_nav,sort_order,updated_at")
|
||||
.order("sort_order", { ascending: true })
|
||||
.order("title", { ascending: true });
|
||||
setPages((data ?? []) as PageRow[]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const slugify = (s: string) =>
|
||||
s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
|
||||
const create = async () => {
|
||||
if (!newTitle.trim()) { toast.error("Title is required"); return; }
|
||||
const slug = (newSlug.trim() || slugify(newTitle)).toLowerCase();
|
||||
if (!slug) { toast.error("Invalid slug"); return; }
|
||||
setCreating(true);
|
||||
const { data, error } = await supabase
|
||||
.from("public_pages")
|
||||
.insert({ title: newTitle.trim(), slug, status: "draft" })
|
||||
.select("id")
|
||||
.single();
|
||||
setCreating(false);
|
||||
if (error) { toast.error("Could not create", { description: error.message }); return; }
|
||||
toast.success("Page created");
|
||||
setNewOpen(false);
|
||||
setNewTitle(""); setNewSlug("");
|
||||
navigate({ to: "/settings/public-pages/$pageId", params: { pageId: data.id } });
|
||||
};
|
||||
|
||||
const remove = async (id: string) => {
|
||||
if (!confirm("Delete this page and all its sections?")) return;
|
||||
const { error } = await supabase.from("public_pages").delete().eq("id", id);
|
||||
if (error) { toast.error("Could not delete", { description: error.message }); return; }
|
||||
toast.success("Deleted");
|
||||
load();
|
||||
};
|
||||
|
||||
if (!isAdmin) return <p className="text-sm text-muted-foreground">Admins only.</p>;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Public pages</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage marketing pages rendered at <code>/p/{slug}</code>.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setNewOpen(true)}><Plus className="h-4 w-4 mr-2" />New page</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Pages</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-10"><Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /></div>
|
||||
) : pages.length === 0 ? (
|
||||
<div className="text-center py-10 text-sm text-muted-foreground">
|
||||
<FileText className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
No pages yet. Create your first one.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{pages.map((p) => (
|
||||
<div key={p.id} className="flex items-center justify-between py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium truncate">{p.title}</span>
|
||||
<Badge variant={p.status === "published" ? "default" : "secondary"}>{p.status}</Badge>
|
||||
{!p.show_in_nav && <Badge variant="outline">hidden in nav</Badge>}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">/p/{p.slug}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{p.status === "published" && (
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<a href={`/p/${p.slug}`} target="_blank" rel="noreferrer"><ExternalLink className="h-4 w-4" /></a>
|
||||
</Button>
|
||||
)}
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link to="/settings/public-pages/$pageId" params={{ pageId: p.id }}><Pencil className="h-4 w-4" /></Link>
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => remove(p.id)}>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={newOpen} onOpenChange={setNewOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle>New public page</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>Title</Label>
|
||||
<Input value={newTitle} onChange={(e) => {
|
||||
setNewTitle(e.target.value);
|
||||
if (!newSlug) setNewSlug(slugify(e.target.value));
|
||||
}} placeholder="About Us" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Slug</Label>
|
||||
<Input value={newSlug} onChange={(e) => setNewSlug(slugify(e.target.value))} placeholder="about-us" />
|
||||
<p className="text-xs text-muted-foreground mt-1">URL: /p/{newSlug || "your-slug"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setNewOpen(false)}>Cancel</Button>
|
||||
<Button onClick={create} disabled={creating}>{creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}Create</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user