1420 lines
49 KiB
TypeScript
1420 lines
49 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { AuthenticatedLayout } from '../layout/AuthenticatedLayout';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { Input } from '../ui/input';
|
|
import { Label } from '../ui/label';
|
|
import { Textarea } from '../ui/textarea';
|
|
import { Badge } from '../ui/badge';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '../ui/table';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '../ui/select';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '../ui/sheet';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '../ui/alert-dialog';
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from '../ui/tabs';
|
|
import { Checkbox } from '../ui/checkbox';
|
|
import { Separator } from '../ui/separator';
|
|
import { Switch } from '../ui/switch';
|
|
import { toast } from "sonner@2.0.3";
|
|
import {
|
|
Search,
|
|
Plus,
|
|
Download,
|
|
MoreHorizontal,
|
|
Edit,
|
|
Eye,
|
|
Send,
|
|
Archive,
|
|
RotateCcw,
|
|
History,
|
|
Globe,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
ExternalLink,
|
|
X,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Monitor,
|
|
Tablet,
|
|
Smartphone,
|
|
Calendar,
|
|
Clock,
|
|
FileText,
|
|
Search as SearchIcon,
|
|
Link,
|
|
Image as ImageIcon
|
|
} from 'lucide-react';
|
|
|
|
interface LandingPagesProps {
|
|
onNavigate: (route: string) => void;
|
|
onLogout: () => void;
|
|
user: any;
|
|
}
|
|
|
|
interface LandingPage {
|
|
id: number;
|
|
pageTitle: string;
|
|
template: string;
|
|
linkedProgramme?: string;
|
|
urlSlug: string;
|
|
status: 'Draft' | 'Published' | 'Archived';
|
|
updated: string;
|
|
owner: string;
|
|
}
|
|
|
|
interface Programme {
|
|
id: number;
|
|
name: string;
|
|
status: 'Published' | 'Draft';
|
|
}
|
|
|
|
interface PageVersion {
|
|
id: number;
|
|
version: string;
|
|
status: 'Draft' | 'Published' | 'Archived';
|
|
updated: string;
|
|
actor: string;
|
|
}
|
|
|
|
const statusOptions = [
|
|
{ value: "all", label: "All" },
|
|
{ value: "draft", label: "Draft" },
|
|
{ value: "published", label: "Published" },
|
|
{ value: "archived", label: "Archived" }
|
|
];
|
|
|
|
const templateOptions = [
|
|
"Executive Programme",
|
|
"Course Landing",
|
|
"Webinar Registration",
|
|
"Generic"
|
|
];
|
|
|
|
const mockProgrammes: Programme[] = [
|
|
{ id: 1, name: "Executive Leadership Programme", status: "Published" },
|
|
{ id: 2, name: "Digital Transformation Course", status: "Published" },
|
|
{ id: 3, name: "Strategic Planning Workshop", status: "Draft" }
|
|
];
|
|
|
|
const mockVersions: PageVersion[] = [
|
|
{
|
|
id: 1,
|
|
version: "v1.0",
|
|
status: "Published",
|
|
updated: "2024-01-10",
|
|
actor: "Admin User"
|
|
},
|
|
{
|
|
id: 2,
|
|
version: "v1.1",
|
|
status: "Draft",
|
|
updated: "2024-01-15",
|
|
actor: "Admin User"
|
|
}
|
|
];
|
|
|
|
const mockOwners = [
|
|
"Admin User",
|
|
"Marketing Team",
|
|
"Content Team"
|
|
];
|
|
|
|
export function LandingPages({ onNavigate, onLogout, user }: LandingPagesProps) {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
const [templateFilter, setTemplateFilter] = useState("all");
|
|
const [ownerFilter, setOwnerFilter] = useState("all");
|
|
const [dateRangeFrom, setDateRangeFrom] = useState("");
|
|
const [dateRangeTo, setDateRangeTo] = useState("");
|
|
const [selectedPages, setSelectedPages] = useState<number[]>([]);
|
|
|
|
// Sheet/Modal state
|
|
const [isCreateEditSheetOpen, setIsCreateEditSheetOpen] = useState(false);
|
|
const [isPreviewDrawerOpen, setIsPreviewDrawerOpen] = useState(false);
|
|
const [isSEODrawerOpen, setIsSEODrawerOpen] = useState(false);
|
|
const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false);
|
|
const [isAuditSheetOpen, setIsAuditSheetOpen] = useState(false);
|
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
|
|
|
// Form state
|
|
const [editingPage, setEditingPage] = useState<LandingPage | null>(null);
|
|
const [pageForm, setPageForm] = useState({
|
|
pageTitle: "",
|
|
template: "",
|
|
linkedProgramme: "none",
|
|
urlSlug: "",
|
|
heroTitle: "",
|
|
heroDescription: "",
|
|
ctaText: "",
|
|
bodyContent: ""
|
|
});
|
|
|
|
// SEO state
|
|
const [seoForm, setSeoForm] = useState({
|
|
metaTitle: "",
|
|
metaDescription: "",
|
|
canonicalUrl: "",
|
|
ogTitle: "",
|
|
ogDescription: "",
|
|
ogImage: "",
|
|
robotsIndex: true,
|
|
robotsFollow: true
|
|
});
|
|
|
|
// Preview state
|
|
const [previewDevice, setPreviewDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
|
|
|
|
// Publish state
|
|
const [schedulePublish, setSchedulePublish] = useState(false);
|
|
const [publishDate, setPublishDate] = useState("");
|
|
const [publishTime, setPublishTime] = useState("");
|
|
|
|
// Confirmation state
|
|
const [confirmAction, setConfirmAction] = useState<{ type: string; page?: LandingPage; version?: PageVersion } | null>(null);
|
|
|
|
const breadcrumbs = [
|
|
{ label: "Admin", href: "/dashboard" },
|
|
{ label: "Landing-Pages" }
|
|
];
|
|
|
|
// Mock landing pages data (minimal for placeholder purposes)
|
|
const mockPages: LandingPage[] = [];
|
|
|
|
const filteredPages = mockPages.filter(page => {
|
|
const matchesSearch = page.pageTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
page.urlSlug.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesStatus = statusFilter === "all" || page.status.toLowerCase() === statusFilter;
|
|
const matchesTemplate = templateFilter === "all" || page.template === templateFilter;
|
|
const matchesOwner = ownerFilter === "all" || page.owner === ownerFilter;
|
|
return matchesSearch && matchesStatus && matchesTemplate && matchesOwner;
|
|
});
|
|
|
|
const getStatusBadgeVariant = (status: string) => {
|
|
switch (status) {
|
|
case "Published":
|
|
return "default";
|
|
case "Draft":
|
|
return "secondary";
|
|
case "Archived":
|
|
return "outline";
|
|
default:
|
|
return "secondary";
|
|
}
|
|
};
|
|
|
|
const generateUrlSlug = (title: string) => {
|
|
return title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.trim();
|
|
};
|
|
|
|
const handleRowSelection = (pageId: number, checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedPages([...selectedPages, pageId]);
|
|
} else {
|
|
setSelectedPages(selectedPages.filter(id => id !== pageId));
|
|
}
|
|
};
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
if (checked) {
|
|
const allIds = filteredPages.map(page => page.id);
|
|
setSelectedPages(allIds);
|
|
} else {
|
|
setSelectedPages([]);
|
|
}
|
|
};
|
|
|
|
const openCreatePage = () => {
|
|
setEditingPage(null);
|
|
setPageForm({
|
|
pageTitle: "",
|
|
template: "",
|
|
linkedProgramme: "none",
|
|
urlSlug: "",
|
|
heroTitle: "",
|
|
heroDescription: "",
|
|
ctaText: "",
|
|
bodyContent: ""
|
|
});
|
|
setIsCreateEditSheetOpen(true);
|
|
};
|
|
|
|
const openEditPage = (page: LandingPage) => {
|
|
setEditingPage(page);
|
|
setPageForm({
|
|
pageTitle: page.pageTitle,
|
|
template: page.template,
|
|
linkedProgramme: page.linkedProgramme || "none",
|
|
urlSlug: page.urlSlug,
|
|
heroTitle: "",
|
|
heroDescription: "",
|
|
ctaText: "",
|
|
bodyContent: ""
|
|
});
|
|
setIsCreateEditSheetOpen(true);
|
|
};
|
|
|
|
const handleSavePage = () => {
|
|
if (!pageForm.pageTitle.trim()) {
|
|
toast.error("Page title is required");
|
|
return;
|
|
}
|
|
|
|
if (!pageForm.template) {
|
|
toast.error("Template is required");
|
|
return;
|
|
}
|
|
|
|
if (!pageForm.urlSlug.trim()) {
|
|
toast.error("URL slug is required");
|
|
return;
|
|
}
|
|
|
|
toast.success("Landing page saved.");
|
|
setIsCreateEditSheetOpen(false);
|
|
};
|
|
|
|
const openPreview = (page: LandingPage) => {
|
|
setPreviewDevice('desktop');
|
|
setIsPreviewDrawerOpen(true);
|
|
};
|
|
|
|
const handlePublish = (page: LandingPage) => {
|
|
if (schedulePublish && (!publishDate || !publishTime)) {
|
|
toast.error("Please set a publish date and time");
|
|
return;
|
|
}
|
|
|
|
if (schedulePublish) {
|
|
toast.success(`Landing page scheduled for publish on ${publishDate} at ${publishTime}.`);
|
|
} else {
|
|
toast.success("Published.");
|
|
}
|
|
|
|
setConfirmAction(null);
|
|
setIsConfirmDialogOpen(false);
|
|
};
|
|
|
|
const handleUnpublish = (page: LandingPage) => {
|
|
toast.success("Unpublished.");
|
|
setConfirmAction(null);
|
|
setIsConfirmDialogOpen(false);
|
|
};
|
|
|
|
const handleArchiveRestore = (page: LandingPage) => {
|
|
if (page.status === "Archived") {
|
|
toast.success("Landing page restored.");
|
|
} else {
|
|
setConfirmAction({ type: "archive", page });
|
|
setIsConfirmDialogOpen(true);
|
|
}
|
|
};
|
|
|
|
const confirmArchive = () => {
|
|
toast.success("Landing page archived.");
|
|
setIsConfirmDialogOpen(false);
|
|
setConfirmAction(null);
|
|
};
|
|
|
|
const openSEOEditor = (page: LandingPage) => {
|
|
setSeoForm({
|
|
metaTitle: "",
|
|
metaDescription: "",
|
|
canonicalUrl: "",
|
|
ogTitle: "",
|
|
ogDescription: "",
|
|
ogImage: "",
|
|
robotsIndex: true,
|
|
robotsFollow: true
|
|
});
|
|
setIsSEODrawerOpen(true);
|
|
};
|
|
|
|
const handleSaveSEO = () => {
|
|
toast.success("SEO updated.");
|
|
setIsSEODrawerOpen(false);
|
|
};
|
|
|
|
const openVersionHistory = (page: LandingPage) => {
|
|
setIsVersionHistoryOpen(true);
|
|
};
|
|
|
|
const handleRollback = (version: PageVersion) => {
|
|
setConfirmAction({ type: "rollback", version });
|
|
setIsConfirmDialogOpen(true);
|
|
};
|
|
|
|
const confirmRollback = () => {
|
|
if (confirmAction?.version) {
|
|
const version = confirmAction.version;
|
|
toast.success(`Rolled back to ${version.version}.`);
|
|
}
|
|
setIsConfirmDialogOpen(false);
|
|
setConfirmAction(null);
|
|
};
|
|
|
|
const openAuditTrail = (page: LandingPage) => {
|
|
setIsAuditSheetOpen(true);
|
|
};
|
|
|
|
const handleBulkAction = (action: string) => {
|
|
const count = selectedPages.length;
|
|
switch (action) {
|
|
case "publish":
|
|
toast.success(`${count} pages published.`);
|
|
setSelectedPages([]);
|
|
break;
|
|
case "unpublish":
|
|
toast.success(`${count} pages unpublished.`);
|
|
setSelectedPages([]);
|
|
break;
|
|
case "archive":
|
|
setConfirmAction({ type: "bulk-archive" });
|
|
setIsConfirmDialogOpen(true);
|
|
break;
|
|
case "restore":
|
|
toast.success(`${count} pages restored.`);
|
|
setSelectedPages([]);
|
|
break;
|
|
case "export":
|
|
toast.success("Export started.");
|
|
break;
|
|
}
|
|
};
|
|
|
|
const confirmBulkArchive = () => {
|
|
const count = selectedPages.length;
|
|
toast.success(`${count} pages archived.`);
|
|
setSelectedPages([]);
|
|
setIsConfirmDialogOpen(false);
|
|
setConfirmAction(null);
|
|
};
|
|
|
|
const renderHeader = () => (
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1>Landing-Pages</h1>
|
|
<p className="text-muted-foreground">
|
|
Create and manage programmatic landing pages
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
className="min-h-[44px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Export
|
|
</Button>
|
|
<Button
|
|
onClick={openCreatePage}
|
|
className="min-h-[44px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Create Landing Page
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderToolbar = () => (
|
|
<div className="flex flex-wrap items-center gap-4 mb-6">
|
|
{/* Search */}
|
|
<div className="flex-1 min-w-[300px]">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search by page title or URL"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 min-h-[44px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Filter Chips */}
|
|
<div className="flex items-center gap-1">
|
|
{statusOptions.map((option) => (
|
|
<Button
|
|
key={option.value}
|
|
variant={statusFilter === option.value ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setStatusFilter(option.value)}
|
|
className="min-h-[44px]"
|
|
>
|
|
{option.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Template Filter */}
|
|
<Select value={templateFilter} onValueChange={setTemplateFilter}>
|
|
<SelectTrigger className="w-[160px] min-h-[44px]">
|
|
<SelectValue placeholder="Type/Template" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Templates</SelectItem>
|
|
{templateOptions.map(template => (
|
|
<SelectItem key={template} value={template}>{template}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* Owner Filter */}
|
|
<Select value={ownerFilter} onValueChange={setOwnerFilter}>
|
|
<SelectTrigger className="w-[140px] min-h-[44px]">
|
|
<SelectValue placeholder="Owner" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Owners</SelectItem>
|
|
{mockOwners.map(owner => (
|
|
<SelectItem key={owner} value={owner}>{owner}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* Date Range */}
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
type="date"
|
|
value={dateRangeFrom}
|
|
onChange={(e) => setDateRangeFrom(e.target.value)}
|
|
className="w-[140px] min-h-[44px]"
|
|
placeholder="From"
|
|
/>
|
|
<span className="text-muted-foreground">to</span>
|
|
<Input
|
|
type="date"
|
|
value={dateRangeTo}
|
|
onChange={(e) => setDateRangeTo(e.target.value)}
|
|
className="w-[140px] min-h-[44px]"
|
|
placeholder="To"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderBulkActions = () => {
|
|
if (selectedPages.length === 0) return null;
|
|
|
|
return (
|
|
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-card border rounded-lg shadow-lg p-4 z-50">
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-sm font-medium">{selectedPages.length} selected</span>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleBulkAction("publish")}
|
|
className="min-h-[44px]"
|
|
>
|
|
Publish
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleBulkAction("unpublish")}
|
|
className="min-h-[44px]"
|
|
>
|
|
Unpublish
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleBulkAction("archive")}
|
|
className="min-h-[44px]"
|
|
>
|
|
<Archive className="h-4 w-4 mr-2" />
|
|
Archive
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleBulkAction("restore")}
|
|
className="min-h-[44px]"
|
|
>
|
|
<RotateCcw className="h-4 w-4 mr-2" />
|
|
Restore
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleBulkAction("export")}
|
|
className="min-h-[44px]"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Export
|
|
</Button>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSelectedPages([])}
|
|
className="min-h-[44px] w-[44px] p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
<span className="sr-only">Clear selection</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderTable = () => {
|
|
if (filteredPages.length === 0) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<FileText className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
|
<h3 className="text-lg font-medium mb-2">No landing pages yet — create one.</h3>
|
|
<Button
|
|
onClick={openCreatePage}
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Create Landing Page
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader className="bg-muted/50 sticky top-0">
|
|
<TableRow>
|
|
<TableHead className="w-12">
|
|
<Checkbox
|
|
checked={selectedPages.length === filteredPages.length && filteredPages.length > 0}
|
|
onCheckedChange={handleSelectAll}
|
|
aria-label="Select all pages"
|
|
/>
|
|
</TableHead>
|
|
<TableHead className="min-w-[200px]">Page Title</TableHead>
|
|
<TableHead>Type / Template</TableHead>
|
|
<TableHead>Linked Programme</TableHead>
|
|
<TableHead className="min-w-[200px]">URL / Slug</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Updated</TableHead>
|
|
<TableHead>Owner</TableHead>
|
|
<TableHead className="w-12">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredPages.map((page) => (
|
|
<TableRow key={page.id} className="min-h-[44px]">
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedPages.includes(page.id)}
|
|
onCheckedChange={(checked) => handleRowSelection(page.id, checked as boolean)}
|
|
aria-label={`Select ${page.pageTitle}`}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<button
|
|
onClick={() => openEditPage(page)}
|
|
className="font-medium text-left hover:text-[var(--color-brand-primary)] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50 rounded"
|
|
>
|
|
{page.pageTitle}
|
|
</button>
|
|
</TableCell>
|
|
<TableCell className="text-sm">{page.template}</TableCell>
|
|
<TableCell className="text-sm">{page.linkedProgramme || "—"}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-mono truncate max-w-[150px]" title={`/pages/${page.urlSlug}`}>
|
|
/pages/{page.urlSlug}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0"
|
|
title="Open in new tab"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={getStatusBadgeVariant(page.status)}>
|
|
{page.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
{page.updated}
|
|
</TableCell>
|
|
<TableCell className="text-sm">{page.owner}</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 p-0 focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
<span className="sr-only">Open menu</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => openEditPage(page)}>
|
|
<Edit className="h-4 w-4 mr-2" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => openPreview(page)}>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
Preview
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => {
|
|
setConfirmAction({ type: page.status === 'Published' ? 'unpublish' : 'publish', page });
|
|
setIsConfirmDialogOpen(true);
|
|
}}>
|
|
{page.status === "Published" ? (
|
|
<>
|
|
<Archive className="h-4 w-4 mr-2" />
|
|
Unpublish
|
|
</>
|
|
) : (
|
|
<>
|
|
<Send className="h-4 w-4 mr-2" />
|
|
Publish
|
|
</>
|
|
)}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => openVersionHistory(page)}>
|
|
<History className="h-4 w-4 mr-2" />
|
|
Rollback
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleArchiveRestore(page)}>
|
|
{page.status === "Archived" ? (
|
|
<>
|
|
<RotateCcw className="h-4 w-4 mr-2" />
|
|
Restore
|
|
</>
|
|
) : (
|
|
<>
|
|
<Archive className="h-4 w-4 mr-2" />
|
|
Archive
|
|
</>
|
|
)}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => openSEOEditor(page)}>
|
|
<SearchIcon className="h-4 w-4 mr-2" />
|
|
SEO & Snippets
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => openAuditTrail(page)}>
|
|
<History className="h-4 w-4 mr-2" />
|
|
Audit
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
{/* Pagination Footer */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-t bg-muted/25">
|
|
<div className="text-sm text-muted-foreground">
|
|
Showing {filteredPages.length} of {mockPages.length} pages
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled
|
|
className="min-h-[44px] w-[44px] p-0"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
<span className="sr-only">Previous page</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled
|
|
className="min-h-[44px] w-[44px] p-0"
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
<span className="sr-only">Next page</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderCreateEditSheet = () => (
|
|
<Sheet open={isCreateEditSheetOpen} onOpenChange={setIsCreateEditSheetOpen}>
|
|
<SheetContent className="w-[480px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50">
|
|
<SheetHeader>
|
|
<SheetTitle>{editingPage ? "Edit Landing Page" : "Create Landing Page"}</SheetTitle>
|
|
<SheetDescription>
|
|
{editingPage ? "Update landing page content and settings" : "Create a new landing page"}
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className="mt-6 space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="page-title">
|
|
Page Title <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="page-title"
|
|
value={pageForm.pageTitle}
|
|
onChange={(e) => {
|
|
const title = e.target.value;
|
|
setPageForm({
|
|
...pageForm,
|
|
pageTitle: title,
|
|
urlSlug: generateUrlSlug(title)
|
|
});
|
|
}}
|
|
placeholder="Enter page title"
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="template">
|
|
Template / Type <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select value={pageForm.template} onValueChange={(value) => setPageForm({...pageForm, template: value})}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select template" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{templateOptions.map(template => (
|
|
<SelectItem key={template} value={template}>{template}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="linked-programme">Linked Programme</Label>
|
|
<Select value={pageForm.linkedProgramme} onValueChange={(value) => setPageForm({...pageForm, linkedProgramme: value})}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select programme (optional)" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">None</SelectItem>
|
|
{mockProgrammes
|
|
.filter(prog => prog.status === 'Published')
|
|
.map(programme => (
|
|
<SelectItem key={programme.id} value={programme.name}>{programme.name}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="url-slug">
|
|
URL Slug <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="url-slug"
|
|
value={pageForm.urlSlug}
|
|
onChange={(e) => setPageForm({...pageForm, urlSlug: e.target.value})}
|
|
placeholder="url-friendly-slug"
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
required
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Full URL: https://klc.edu/pages/{pageForm.urlSlug || "url-slug"}
|
|
</p>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-4">
|
|
<h3 className="font-medium">Content Blocks</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="hero-title">Hero Title</Label>
|
|
<Input
|
|
id="hero-title"
|
|
value={pageForm.heroTitle}
|
|
onChange={(e) => setPageForm({...pageForm, heroTitle: e.target.value})}
|
|
placeholder="Main headline"
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="hero-description">Hero Description</Label>
|
|
<Textarea
|
|
id="hero-description"
|
|
value={pageForm.heroDescription}
|
|
onChange={(e) => setPageForm({...pageForm, heroDescription: e.target.value})}
|
|
placeholder="Supporting description text"
|
|
rows={3}
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cta-text">CTA Text</Label>
|
|
<Input
|
|
id="cta-text"
|
|
value={pageForm.ctaText}
|
|
onChange={(e) => setPageForm({...pageForm, ctaText: e.target.value})}
|
|
placeholder="e.g., Register Now, Learn More"
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="body-content">Body Content</Label>
|
|
<Textarea
|
|
id="body-content"
|
|
value={pageForm.bodyContent}
|
|
onChange={(e) => setPageForm({...pageForm, bodyContent: e.target.value})}
|
|
placeholder="Main page content"
|
|
rows={6}
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 mt-8">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsCreateEditSheetOpen(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSavePage}
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
>
|
|
Save Draft
|
|
</Button>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
|
|
const renderPreviewDrawer = () => (
|
|
<Sheet open={isPreviewDrawerOpen} onOpenChange={setIsPreviewDrawerOpen}>
|
|
<SheetContent className="w-[480px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50">
|
|
<SheetHeader>
|
|
<SheetTitle>Preview Landing Page</SheetTitle>
|
|
<SheetDescription>
|
|
Read-only preview of the current page design
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className="mt-6 space-y-4">
|
|
{/* Device Toggle */}
|
|
<div className="flex items-center gap-2 p-1 bg-muted rounded-lg">
|
|
<Button
|
|
variant={previewDevice === 'desktop' ? 'default' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setPreviewDevice('desktop')}
|
|
className="flex-1"
|
|
>
|
|
<Monitor className="h-4 w-4 mr-2" />
|
|
Desktop
|
|
</Button>
|
|
<Button
|
|
variant={previewDevice === 'tablet' ? 'default' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setPreviewDevice('tablet')}
|
|
className="flex-1"
|
|
>
|
|
<Tablet className="h-4 w-4 mr-2" />
|
|
Tablet
|
|
</Button>
|
|
<Button
|
|
variant={previewDevice === 'mobile' ? 'default' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setPreviewDevice('mobile')}
|
|
className="flex-1"
|
|
>
|
|
<Smartphone className="h-4 w-4 mr-2" />
|
|
Mobile
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Preview Frame */}
|
|
<div className={`border rounded-lg bg-background ${
|
|
previewDevice === 'desktop' ? 'w-full aspect-[16/9]' :
|
|
previewDevice === 'tablet' ? 'w-3/4 mx-auto aspect-[4/3]' :
|
|
'w-1/2 mx-auto aspect-[9/16]'
|
|
}`}>
|
|
<div className="p-4 h-full overflow-y-auto">
|
|
<div className="space-y-4">
|
|
<div className="text-center">
|
|
<h1 className="text-xl font-semibold mb-2">{pageForm.heroTitle || "Hero Title"}</h1>
|
|
<p className="text-muted-foreground mb-4">{pageForm.heroDescription || "Hero description goes here..."}</p>
|
|
<Button
|
|
size="sm"
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
>
|
|
{pageForm.ctaText || "Call to Action"}
|
|
</Button>
|
|
</div>
|
|
<Separator />
|
|
<div className="prose prose-sm max-w-none">
|
|
<p className="text-sm">{pageForm.bodyContent || "Body content will appear here..."}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs text-muted-foreground text-center">
|
|
Preview shows current draft content with basic styling
|
|
</div>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
|
|
const renderSEODrawer = () => (
|
|
<Sheet open={isSEODrawerOpen} onOpenChange={setIsSEODrawerOpen}>
|
|
<SheetContent className="w-[480px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50">
|
|
<SheetHeader>
|
|
<SheetTitle>SEO & Snippets</SheetTitle>
|
|
<SheetDescription>
|
|
Update SEO metadata and social sharing snippets
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className="mt-6 space-y-6">
|
|
<div className="space-y-4">
|
|
<h3 className="font-medium">Basic SEO</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="meta-title">Meta Title</Label>
|
|
<Input
|
|
id="meta-title"
|
|
value={seoForm.metaTitle}
|
|
onChange={(e) => setSeoForm({...seoForm, metaTitle: e.target.value})}
|
|
placeholder="Page title for search engines"
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{seoForm.metaTitle.length}/60 characters
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="meta-description">Meta Description</Label>
|
|
<Textarea
|
|
id="meta-description"
|
|
value={seoForm.metaDescription}
|
|
onChange={(e) => setSeoForm({...seoForm, metaDescription: e.target.value})}
|
|
placeholder="Brief description for search results"
|
|
rows={3}
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{seoForm.metaDescription.length}/160 characters
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="canonical-url">Canonical URL</Label>
|
|
<Input
|
|
id="canonical-url"
|
|
value={seoForm.canonicalUrl}
|
|
onChange={(e) => setSeoForm({...seoForm, canonicalUrl: e.target.value})}
|
|
placeholder="https://klc.edu/pages/..."
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-4">
|
|
<h3 className="font-medium">Social Sharing (Open Graph)</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="og-title">Social Title</Label>
|
|
<Input
|
|
id="og-title"
|
|
value={seoForm.ogTitle}
|
|
onChange={(e) => setSeoForm({...seoForm, ogTitle: e.target.value})}
|
|
placeholder="Title for social media sharing"
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="og-description">Social Description</Label>
|
|
<Textarea
|
|
id="og-description"
|
|
value={seoForm.ogDescription}
|
|
onChange={(e) => setSeoForm({...seoForm, ogDescription: e.target.value})}
|
|
placeholder="Description for social media sharing"
|
|
rows={3}
|
|
className="focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="og-image">Social Image URL</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
id="og-image"
|
|
value={seoForm.ogImage}
|
|
onChange={(e) => setSeoForm({...seoForm, ogImage: e.target.value})}
|
|
placeholder="https://..."
|
|
className="flex-1 focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
/>
|
|
<Button variant="outline" size="sm">
|
|
<ImageIcon className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-4">
|
|
<h3 className="font-medium">Robots</h3>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="robots-index">Index</Label>
|
|
<Switch
|
|
id="robots-index"
|
|
checked={seoForm.robotsIndex}
|
|
onCheckedChange={(checked) => setSeoForm({...seoForm, robotsIndex: checked})}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="robots-follow">Follow</Label>
|
|
<Switch
|
|
id="robots-follow"
|
|
checked={seoForm.robotsFollow}
|
|
onCheckedChange={(checked) => setSeoForm({...seoForm, robotsFollow: checked})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 mt-8">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsSEODrawerOpen(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSaveSEO}
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
|
|
const renderVersionHistorySheet = () => (
|
|
<Sheet open={isVersionHistoryOpen} onOpenChange={setIsVersionHistoryOpen}>
|
|
<SheetContent className="w-[480px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50">
|
|
<SheetHeader>
|
|
<SheetTitle>Version History & Rollback</SheetTitle>
|
|
<SheetDescription>
|
|
View page versions and rollback to a previous version
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className="mt-6 space-y-4">
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Version</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Updated</TableHead>
|
|
<TableHead>Actor</TableHead>
|
|
<TableHead className="w-20">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{mockVersions.map((version) => (
|
|
<TableRow key={version.id}>
|
|
<TableCell className="font-medium">{version.version}</TableCell>
|
|
<TableCell>
|
|
<Badge variant={version.status === 'Published' ? 'default' : 'secondary'}>
|
|
{version.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">{version.updated}</TableCell>
|
|
<TableCell className="text-sm">{version.actor}</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
Preview this version
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleRollback(version)}>
|
|
<RotateCcw className="h-4 w-4 mr-2" />
|
|
Rollback
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<div className="flex items-start gap-2">
|
|
<AlertCircle className="h-4 w-4 text-blue-600 mt-0.5" />
|
|
<div className="text-sm text-blue-700">
|
|
<strong>Note:</strong> Rollback creates a new version with the selected content
|
|
and does not overwrite the version history.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
|
|
const renderAuditSheet = () => (
|
|
<Sheet open={isAuditSheetOpen} onOpenChange={setIsAuditSheetOpen}>
|
|
<SheetContent className="w-[480px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50">
|
|
<SheetHeader>
|
|
<SheetTitle>Audit Trail</SheetTitle>
|
|
<SheetDescription>
|
|
Complete history of landing page changes
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className="mt-6 space-y-4">
|
|
{/* Filters */}
|
|
<div className="flex gap-2">
|
|
<Select defaultValue="all">
|
|
<SelectTrigger className="flex-1">
|
|
<SelectValue placeholder="Action" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Actions</SelectItem>
|
|
<SelectItem value="create">Create</SelectItem>
|
|
<SelectItem value="edit">Edit</SelectItem>
|
|
<SelectItem value="preview">Preview</SelectItem>
|
|
<SelectItem value="publish">Publish</SelectItem>
|
|
<SelectItem value="unpublish">Unpublish</SelectItem>
|
|
<SelectItem value="rollback">Rollback</SelectItem>
|
|
<SelectItem value="archive">Archive</SelectItem>
|
|
<SelectItem value="restore">Restore</SelectItem>
|
|
<SelectItem value="seo-update">SEO Update</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Select defaultValue="all">
|
|
<SelectTrigger className="flex-1">
|
|
<SelectValue placeholder="Actor" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Users</SelectItem>
|
|
<SelectItem value="admin">Admin User</SelectItem>
|
|
<SelectItem value="marketing">Marketing Team</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Audit Entries */}
|
|
<div className="space-y-3">
|
|
<div className="border-l-2 border-muted pl-4 pb-4">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Badge variant="outline">Created</Badge>
|
|
<span className="text-sm text-muted-foreground">by {user.name}</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mb-1">
|
|
{new Date().toLocaleString()}
|
|
</div>
|
|
<p className="text-sm">Landing page created with basic content structure</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
|
|
const renderConfirmationDialog = () => (
|
|
<AlertDialog open={isConfirmDialogOpen} onOpenChange={setIsConfirmDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
{confirmAction?.type === 'publish' ? 'Publish Landing Page' :
|
|
confirmAction?.type === 'unpublish' ? 'Unpublish Landing Page' :
|
|
confirmAction?.type === 'archive' ? 'Archive Landing Page' :
|
|
confirmAction?.type === 'bulk-archive' ? 'Archive Multiple Pages' :
|
|
confirmAction?.type === 'rollback' ? 'Rollback to Version' :
|
|
'Confirm Action'}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{confirmAction?.type === 'publish' ? (
|
|
<div className="space-y-4">
|
|
<p>Are you sure you want to publish this landing page? It will be accessible to the public.</p>
|
|
{schedulePublish && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="schedule-publish"
|
|
checked={schedulePublish}
|
|
onCheckedChange={(checked) => setSchedulePublish(checked as boolean)}
|
|
/>
|
|
<Label htmlFor="schedule-publish">Schedule publish</Label>
|
|
</div>
|
|
{schedulePublish && (
|
|
<div className="grid grid-cols-2 gap-2 ml-6">
|
|
<Input
|
|
type="date"
|
|
value={publishDate}
|
|
onChange={(e) => setPublishDate(e.target.value)}
|
|
placeholder="Date"
|
|
/>
|
|
<Input
|
|
type="time"
|
|
value={publishTime}
|
|
onChange={(e) => setPublishTime(e.target.value)}
|
|
placeholder="Time"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : confirmAction?.type === 'unpublish' ?
|
|
'Are you sure you want to unpublish this landing page? It will no longer be accessible to the public.' :
|
|
confirmAction?.type === 'archive' ?
|
|
'Are you sure you want to archive this landing page? It will be hidden from public routing.' :
|
|
confirmAction?.type === 'bulk-archive' ?
|
|
`Are you sure you want to archive ${selectedPages.length} landing pages? They will be hidden from public routing.` :
|
|
confirmAction?.type === 'rollback' ?
|
|
`Are you sure you want to rollback to ${confirmAction.version?.version}? This will create a new version with the selected content.` :
|
|
'This action cannot be undone.'
|
|
}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={
|
|
confirmAction?.type === 'publish' ? () => handlePublish(confirmAction.page!) :
|
|
confirmAction?.type === 'unpublish' ? () => handleUnpublish(confirmAction.page!) :
|
|
confirmAction?.type === 'archive' ? confirmArchive :
|
|
confirmAction?.type === 'bulk-archive' ? confirmBulkArchive :
|
|
confirmAction?.type === 'rollback' ? confirmRollback :
|
|
() => setIsConfirmDialogOpen(false)
|
|
}
|
|
className={
|
|
confirmAction?.type === 'archive' || confirmAction?.type === 'bulk-archive'
|
|
? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
: ""
|
|
}
|
|
>
|
|
{confirmAction?.type === 'publish' ? (schedulePublish ? 'Schedule' : 'Publish') :
|
|
confirmAction?.type === 'unpublish' ? 'Unpublish' :
|
|
confirmAction?.type === 'archive' || confirmAction?.type === 'bulk-archive' ? 'Archive' :
|
|
confirmAction?.type === 'rollback' ? 'Rollback' :
|
|
'Confirm'}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
|
|
return (
|
|
<AuthenticatedLayout
|
|
currentRoute="/landing-pages"
|
|
onNavigate={onNavigate}
|
|
onLogout={onLogout}
|
|
user={user}
|
|
breadcrumbs={breadcrumbs}
|
|
>
|
|
<div className="p-6 space-y-6 max-w-[1440px] mx-auto">
|
|
{/* Header */}
|
|
{renderHeader()}
|
|
|
|
{/* Toolbar */}
|
|
{renderToolbar()}
|
|
|
|
{/* Table Card */}
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
{renderTable()}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bulk Actions */}
|
|
{renderBulkActions()}
|
|
|
|
{/* Sheets and Modals */}
|
|
{renderCreateEditSheet()}
|
|
{renderPreviewDrawer()}
|
|
{renderSEODrawer()}
|
|
{renderVersionHistorySheet()}
|
|
{renderAuditSheet()}
|
|
{renderConfirmationDialog()}
|
|
|
|
{/* Toast area for system messages */}
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-label="System status messages"
|
|
className="sr-only"
|
|
/>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
} |