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/AboutUsEditor.tsx

554 lines
16 KiB
TypeScript
Raw Normal View History

2025-09-26 19:45:02 +05:30
import React, { useState } from 'react';
import { AuthenticatedLayout } from '../layout/AuthenticatedLayout';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import { Badge } from '../ui/badge';
import { toast } from "sonner@2.0.3";
import {
Save,
Eye,
Send,
Check,
X,
History,
GripVertical,
Link
} from 'lucide-react';
import { InternalLinkPicker } from '../landing-pages/InternalLinkPicker';
import { MediaPicker } from '../landing-pages/MediaPicker';
import { PreviewModal } from '../landing-pages/PreviewModal';
import { VersionHistory } from '../landing-pages/VersionHistory';
import { AuditDrawer } from '../landing-pages/AuditDrawer';
2025-10-29 19:21:35 +05:30
import { Route } from '../../types/routes';
2025-09-26 19:45:02 +05:30
interface AboutUsEditorProps {
2025-10-29 19:21:35 +05:30
onNavigate: (route: Route) => void;
2025-09-26 19:45:02 +05:30
onLogout: () => void;
user: any;
}
interface HeroData {
imageUrl?: string;
imageAlt?: string;
headline: string;
subtext: string;
cta: {
label: string;
internalHref: string;
};
}
interface StatData {
value: number;
suffix: string;
label: string;
}
interface TeamMember {
title: string;
imageUrl?: string;
imageAlt?: string;
body: string;
}
export function AboutUsEditor({ onNavigate, onLogout, user }: AboutUsEditorProps) {
const [status, setStatus] = useState<'draft' | 'in_review' | 'changes_requested' | 'approved' | 'published'>('published');
const [isLinkPickerOpen, setIsLinkPickerOpen] = useState(false);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false);
const [isAuditOpen, setIsAuditOpen] = useState(false);
// Form data
const [hero, setHero] = useState<HeroData>({
headline: '',
subtext: '',
cta: { label: '', internalHref: '' }
});
const [stats, setStats] = useState<StatData[]>([
{ value: 27187, suffix: '+', label: 'LEADERS DEVELOPED' },
{ value: 15510, suffix: '+', label: 'CORPORATE CLIENTS' },
{ value: 1240, suffix: '+', label: 'COUNTRIES SERVED' }
]);
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([
{ title: '', body: '' },
{ title: '', body: '' },
{ title: '', body: '' },
{ title: '', body: '' }
]);
const breadcrumbs = [
{ label: "Admin", href: "/dashboard" },
{ label: "Landing Pages", href: "/landing-pages" },
{ label: "About Us" }
];
const handleSaveDraft = () => {
toast.success("Saved as draft.");
};
const handleSubmitForApproval = () => {
setStatus('in_review');
toast.success("Submitted for approval.");
};
const handleApprove = () => {
setStatus('approved');
toast.success("Approved.");
};
const handleRequestChanges = () => {
setStatus('changes_requested');
toast.success("Changes requested.");
};
const handlePublish = () => {
setStatus('published');
toast.success("Published.");
};
const handleUnpublish = () => {
setStatus('draft');
toast.success("Unpublished.");
};
const handleLinkSelect = (link: { href: string; title: string }) => {
setHero(prev => ({
...prev,
cta: { ...prev.cta, internalHref: link.href }
}));
};
const validateCTA = (cta: { label: string; internalHref: string }) => {
if (cta.label && !cta.internalHref) return "CTA requires both text and destination.";
if (!cta.label && cta.internalHref) return "CTA requires both text and destination.";
return null;
};
const getStatusBadgeVariant = () => {
switch (status) {
case 'published': return 'default';
case 'approved': return 'secondary';
case 'in_review': return 'outline';
case 'changes_requested': return 'destructive';
default: return 'secondary';
}
};
const getStatusLabel = () => {
switch (status) {
case 'draft': return 'Draft';
case 'in_review': return 'In Review';
case 'changes_requested': return 'Changes Requested';
case 'approved': return 'Approved';
case 'published': return 'Published';
default: return 'Draft';
}
};
const canEdit = status !== 'in_review';
const canApprove = user.role === 'Super Admin' && status === 'in_review';
const canPublish = user.role === 'Super Admin' && status === 'approved';
const renderHeader = () => (
<div className="sticky top-0 bg-background border-b p-6 z-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<h1>About Us Page</h1>
<Badge variant={getStatusBadgeVariant()}>
{getStatusLabel()}
</Badge>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={handleSaveDraft}
disabled={!canEdit}
className="min-h-[44px]"
>
<Save className="h-4 w-4 mr-2" />
Save Draft
</Button>
<Button
variant="outline"
onClick={() => setIsPreviewOpen(true)}
className="min-h-[44px]"
>
<Eye className="h-4 w-4 mr-2" />
Preview
</Button>
{status === 'draft' && (
<Button
onClick={handleSubmitForApproval}
className="min-h-[44px]"
style={{ backgroundColor: "var(--color-brand-primary)" }}
>
<Send className="h-4 w-4 mr-2" />
Submit for Approval
</Button>
)}
{canApprove && (
<>
<Button
onClick={handleApprove}
className="min-h-[44px]"
style={{ backgroundColor: "var(--color-brand-primary)" }}
>
<Check className="h-4 w-4 mr-2" />
Approve
</Button>
<Button
variant="outline"
onClick={handleRequestChanges}
className="min-h-[44px]"
>
<X className="h-4 w-4 mr-2" />
Request Changes
</Button>
</>
)}
{canPublish && (
<Button
onClick={handlePublish}
className="min-h-[44px]"
style={{ backgroundColor: "var(--color-brand-primary)" }}
>
Publish
</Button>
)}
{status === 'published' && (
<Button
variant="outline"
onClick={handleUnpublish}
className="min-h-[44px]"
>
Unpublish
</Button>
)}
<Button
variant="ghost"
onClick={() => setIsAuditOpen(true)}
className="min-h-[44px] w-[44px] p-0"
>
<History className="h-4 w-4" />
<span className="sr-only">Audit</span>
</Button>
</div>
</div>
</div>
);
const renderRightRail = () => (
<div className="w-80 border-l bg-muted/25 p-6 space-y-6">
<div>
<h3 className="font-medium mb-3">Page Information</h3>
<div className="space-y-2 text-sm">
<div>
<span className="text-muted-foreground">URL:</span>
<span className="ml-2 font-mono">/about-us</span>
</div>
<div>
<span className="text-muted-foreground">Last published:</span>
<span className="ml-2">2024-01-13 16:45</span>
</div>
<div>
<span className="text-muted-foreground">Last editor:</span>
<span className="ml-2">Marketing Team</span>
</div>
</div>
</div>
<div>
<h3 className="font-medium mb-3">Actions</h3>
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setIsVersionHistoryOpen(true)}
className="w-full justify-start min-h-[44px]"
>
<History className="h-4 w-4 mr-2" />
Version History
</Button>
</div>
</div>
</div>
);
const renderHeroSection = () => (
<Card>
<CardHeader>
<CardTitle>Hero Section</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Background Image */}
<div className="space-y-2">
<Label>Background Image</Label>
<MediaPicker
type="image"
onChange={() => {}}
recommendedSize="1440×600px"
required
/>
</div>
{/* Headline */}
<div className="space-y-2">
<Label htmlFor="hero-headline">
Headline <span className="text-destructive">*</span>
</Label>
<Input
id="hero-headline"
value={hero.headline}
onChange={(e) => setHero(prev => ({ ...prev, headline: e.target.value }))}
placeholder="Enter hero headline"
disabled={!canEdit}
required
/>
</div>
{/* Subtext */}
<div className="space-y-2">
<Label htmlFor="hero-subtext">Subtext</Label>
<Textarea
id="hero-subtext"
value={hero.subtext}
onChange={(e) => setHero(prev => ({ ...prev, subtext: e.target.value }))}
placeholder="Enter hero subtext"
disabled={!canEdit}
rows={3}
/>
</div>
{/* CTA */}
<div className="space-y-4">
<Label>Call to Action</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="hero-cta-text">CTA Text</Label>
<Input
id="hero-cta-text"
value={hero.cta.label}
onChange={(e) => setHero(prev => ({
...prev,
cta: { ...prev.cta, label: e.target.value }
}))}
placeholder="Enter CTA text"
disabled={!canEdit}
/>
</div>
<div className="space-y-2">
<Label>CTA Destination</Label>
<div className="flex gap-2">
<Input
value={hero.cta.internalHref}
placeholder="Select internal link"
readOnly
className="flex-1"
/>
<Button
variant="outline"
onClick={() => setIsLinkPickerOpen(true)}
disabled={!canEdit}
className="flex-shrink-0"
>
<Link className="h-4 w-4" />
<span className="sr-only">Select link</span>
</Button>
</div>
</div>
</div>
{validateCTA(hero.cta) && (
<p className="text-sm text-destructive">{validateCTA(hero.cta)}</p>
)}
</div>
</CardContent>
</Card>
);
const renderStatsSection = () => (
<Card>
<CardHeader>
<CardTitle>Stats Section</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{stats.map((stat, index) => (
<div key={index} className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor={`stat-${index}-value`}>Number</Label>
<Input
id={`stat-${index}-value`}
type="number"
value={stat.value}
onChange={(e) => {
const newStats = [...stats];
newStats[index].value = parseInt(e.target.value) || 0;
setStats(newStats);
}}
disabled={!canEdit}
min="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor={`stat-${index}-suffix`}>Suffix</Label>
<Input
id={`stat-${index}-suffix`}
value={stat.suffix}
onChange={(e) => {
const newStats = [...stats];
newStats[index].suffix = e.target.value;
setStats(newStats);
}}
disabled={!canEdit}
placeholder="+"
/>
</div>
<div className="space-y-2">
<Label htmlFor={`stat-${index}-label`}>Label</Label>
<Input
id={`stat-${index}-label`}
value={stat.label}
onChange={(e) => {
const newStats = [...stats];
newStats[index].label = e.target.value;
setStats(newStats);
}}
disabled={!canEdit}
placeholder="Label"
required
/>
</div>
</div>
))}
</CardContent>
</Card>
);
const renderTeamSection = () => (
<Card>
<CardHeader>
<CardTitle>Our Team</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{teamMembers.map((member, index) => (
<div key={index} className="space-y-4 p-4 border rounded-lg">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Team Member {index + 1}</span>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor={`team-${index}-title`}>Name / Role</Label>
<Input
id={`team-${index}-title`}
value={member.title}
onChange={(e) => {
const newMembers = [...teamMembers];
newMembers[index].title = e.target.value;
setTeamMembers(newMembers);
}}
disabled={!canEdit}
placeholder="Enter name and role"
/>
</div>
<div className="space-y-2">
<Label>Photo</Label>
<MediaPicker
type="image"
acceptedTypes={['.jpg', '.jpeg', '.png', '.svg']}
onChange={() => {}}
recommendedSize="400×400px"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor={`team-${index}-body`}>Bio / Description</Label>
<Textarea
id={`team-${index}-body`}
value={member.body}
onChange={(e) => {
const newMembers = [...teamMembers];
newMembers[index].body = e.target.value;
setTeamMembers(newMembers);
}}
disabled={!canEdit}
placeholder="Enter bio or role description"
rows={4}
/>
</div>
</div>
</div>
))}
</CardContent>
</Card>
);
return (
<AuthenticatedLayout
user={user}
onLogout={onLogout}
breadcrumbs={breadcrumbs}
>
<div className="min-h-screen flex flex-col">
{renderHeader()}
<div className="flex-1 flex">
{/* Main Content */}
<div className="flex-1 p-6 space-y-6 overflow-y-auto">
{renderHeroSection()}
{renderStatsSection()}
{renderTeamSection()}
</div>
{/* Right Rail */}
{renderRightRail()}
</div>
</div>
{/* Modals */}
<InternalLinkPicker
isOpen={isLinkPickerOpen}
onClose={() => setIsLinkPickerOpen(false)}
onSelect={handleLinkSelect}
currentLink={{ href: hero.cta.internalHref, title: '' }}
/>
<PreviewModal
isOpen={isPreviewOpen}
onClose={() => setIsPreviewOpen(false)}
pageTitle="About Us"
pageData={{ hero, stats, teamMembers }}
/>
<VersionHistory
isOpen={isVersionHistoryOpen}
onClose={() => setIsVersionHistoryOpen(false)}
pageTitle="About Us"
/>
<AuditDrawer
isOpen={isAuditOpen}
onClose={() => setIsAuditOpen(false)}
pageTitle="About Us"
/>
</AuthenticatedLayout>
);
}