This repository has been archived on 2026-04-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
KLC-Admin-Panel-Frontend-Fi…/src/components/pages/LandingPages.tsx
2025-09-26 19:45:02 +05:30

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>
);
}