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
2025-09-26 19:45:02 +05:30

553 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
interface AboutUsEditorProps {
onNavigate: (route: string) => void;
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>
);
}