0% found this document useful (0 votes)
31 views17 pages

React Code

Uploaded by

luckychawlaup
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
31 views17 pages

React Code

Uploaded by

luckychawlaup
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 17

'use client';

import React, { useState, useEffect, FormEvent, Suspense } from 'react';


import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from
'@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from
'@/components/ui/select';
import { useToast } from '@/hooks/use-toast';
import {
getItemFromSection,
updateItemInSection,
ItemDocument,
ItemData,
DynamicFieldConfig,
ItemFAQ,
} from '@/lib/firebase/sectionsService';
import { RefreshCw, PlusCircle, Trash2, Save, Settings2, MessageSquareMore, Image
as ImageIcon, Edit3, Check, X } from 'lucide-react';
import Image from 'next/image';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from
'@/components/ui/accordion';
import { usePageLoader } from '@/hooks/use-page-loader';
import { Skeleton } from '@/components/ui/skeleton';

// Zod schema for a single dynamic field


const dynamicFieldSchema = z.object({
id: z.string(),
label: z.string().min(1, 'Label is required.'),
placeholder: z.string().optional(),
type: z.enum(['text', 'number', 'email', 'alphabetical']),
required: z.boolean().optional(),
maxLength: z.number().optional(),
});

// Zod schema for a single FAQ item


const faqSchema = z.object({
id: z.string(),
question: z.string().min(1, 'Question is required.'),
answer: z.string().min(1, 'Answer is required.'),
imageUrl: z.string().url('Must be a valid URL.').optional().or(z.literal('')),
});

// Main form schema


const itemFormSchema = z.object({
title: z.string().min(1, 'Item Title is required.'),
category: z.string().min(1, 'Category is required.'),
imageSrc: z.string().url('A valid Image URL is required.'),
imageAlt: z.string().optional(),
aiHint: z.string().optional(),
order: z.coerce.number(),
dynamicFieldsConfig: z.array(dynamicFieldSchema).optional(),
faqs: z.array(faqSchema).optional(),
});

type ItemFormValues = z.infer<typeof itemFormSchema>;

function EditItemContent() {
const router = useRouter();
const params = useParams();
const searchParams = useSearchParams();
const { toast } = useToast();
const { startLoading, stopLoading } = usePageLoader();

const itemId = params.itemId as string;


const sectionId = searchParams.get('sectionId');

const [isFetching, setIsFetching] = useState(true);


const [isSaving, setIsSaving] = useState(false);
const [item, setItem] = useState<ItemDocument | null>(null);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

// States for managing dynamic field inputs


const [currentDynamicFieldLabel, setCurrentDynamicFieldLabel] = useState('');
const [currentDynamicFieldType, setCurrentDynamicFieldType] =
useState<DynamicFieldConfig['type']>('text');
const [currentDynamicFieldPlaceholder, setCurrentDynamicFieldPlaceholder] =
useState('');
const [currentDynamicFieldRequired, setCurrentDynamicFieldRequired] =
useState(false);
const [currentDynamicFieldMaxLength, setCurrentDynamicFieldMaxLength] =
useState('');

// States for managing FAQ inputs


const [newFaqQuestion, setNewFaqQuestion] = useState('');
const [newFaqAnswer, setNewFaqAnswer] = useState('');
const [newFaqImageUrl, setNewFaqImageUrl] = useState('');

// States for editing existing items


const [editingDynamicField, setEditingDynamicField] = useState<string |
null>(null);
const [editingFaq, setEditingFaq] = useState<string | null>(null);

const form = useForm<ItemFormValues>({


resolver: zodResolver(itemFormSchema),
defaultValues: {
title: '',
category: '',
imageSrc: '',
imageAlt: '',
aiHint: '',
order: 0,
dynamicFieldsConfig: [],
faqs: [],
},
mode: 'onChange',
});

const { reset, watch, setValue, handleSubmit, formState, trigger, getValues } =


form;
const { isDirty, errors } = formState;
const dynamicFields = watch('dynamicFieldsConfig') || [];
const faqs = watch('faqs') || [];
const imageSrcPreview = watch('imageSrc');

// Track changes manually for better control


useEffect(() => {
const subscription = watch((value, { name, type }) => {
if (type === 'change') {
setHasUnsavedChanges(true);
}
});
return () => subscription.unsubscribe();
}, [watch]);

useEffect(() => {
if (!sectionId || !itemId) {
toast({ variant: 'destructive', title: 'Error', description: 'Missing
section or item ID.' });
setIsFetching(false);
return;
}

async function fetchItem() {


setIsFetching(true);
try {
const fetchedItem = await getItemFromSection(sectionId!, itemId);
if (fetchedItem) {
setItem(fetchedItem);
const formData = {
title: fetchedItem.title,
category: fetchedItem.category,
imageSrc: fetchedItem.imageSrc,
imageAlt: fetchedItem.imageAlt || '',
aiHint: fetchedItem.aiHint || '',
order: fetchedItem.order,
dynamicFieldsConfig: fetchedItem.dynamicFieldsConfig || [],
faqs: fetchedItem.faqs || [],
};
reset(formData);
setHasUnsavedChanges(false);
} else {
toast({ variant: 'destructive', title: 'Not Found', description: 'The
requested item could not be found.' });
}
} catch (error) {
toast({ variant: 'destructive', title: 'Error fetching item', description:
(error as Error).message });
} finally {
setIsFetching(false);
}
}
fetchItem();
}, [sectionId, itemId, reset, toast]);

const onSubmit = async (data: ItemFormValues) => {


if (!sectionId || !itemId) return;
setIsSaving(true);
startLoading();
try {
const dataToUpdate: Partial<ItemData> = { ...data };
await updateItemInSection(sectionId, itemId, dataToUpdate);
toast({ title: 'Item Updated', description: `${data.title} has been
successfully updated.` });

// Reset form state and mark as clean


reset(data);
setHasUnsavedChanges(false);
setEditingDynamicField(null);
setEditingFaq(null);
} catch (error) {
toast({ variant: 'destructive', title: 'Error saving item', description:
(error as Error).message });
} finally {
setIsSaving(false);
stopLoading();
}
};

const handleAddDynamicField = async () => {


if (!currentDynamicFieldLabel.trim()) {
toast({ variant: 'destructive', title: 'Missing Field Label' });
return;
}

const maxLengthValue = currentDynamicFieldMaxLength ?


parseInt(currentDynamicFieldMaxLength, 10) : undefined;

const newField: DynamicFieldConfig = {


id: `field_${Date.now()}`,
label: currentDynamicFieldLabel.trim(),
placeholder: currentDynamicFieldPlaceholder.trim() || undefined,
type: currentDynamicFieldType,
required: currentDynamicFieldRequired,
...(maxLengthValue && maxLengthValue > 0 && { maxLength: maxLengthValue })
};

const currentFields = getValues('dynamicFieldsConfig') || [];


const updatedFields = [...currentFields, newField];

setValue('dynamicFieldsConfig', updatedFields, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});

// Clear input fields


setCurrentDynamicFieldLabel('');
setCurrentDynamicFieldPlaceholder('');
setCurrentDynamicFieldType('text');
setCurrentDynamicFieldRequired(false);
setCurrentDynamicFieldMaxLength('');

setHasUnsavedChanges(true);
await trigger('dynamicFieldsConfig');
};
const handleRemoveDynamicField = async (id: string) => {
const currentFields = getValues('dynamicFieldsConfig') || [];
const updatedFields = currentFields.filter(f => f.id !== id);

setValue('dynamicFieldsConfig', updatedFields, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});

setHasUnsavedChanges(true);
await trigger('dynamicFieldsConfig');
};

const handleUpdateDynamicField = async (id: string, updates:


Partial<DynamicFieldConfig>) => {
const currentFields = getValues('dynamicFieldsConfig') || [];
const updatedFields = currentFields.map(field =>
field.id === id ? { ...field, ...updates } : field
);

setValue('dynamicFieldsConfig', updatedFields, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});

setHasUnsavedChanges(true);
await trigger('dynamicFieldsConfig');
};

const handleAddFaq = async () => {


if (!newFaqQuestion.trim() || !newFaqAnswer.trim()) {
toast({ variant: 'destructive', title: 'Missing FAQ Content', description:
'Question and Answer are required.' });
return;
}

// Validate image URL if provided


if (newFaqImageUrl.trim() && !isValidUrl(newFaqImageUrl.trim())) {
toast({ variant: 'destructive', title: 'Invalid Image URL', description:
'Please enter a valid URL for the image.' });
return;
}

const newFaq: ItemFAQ = {


id: `faq_${Date.now()}`,
question: newFaqQuestion.trim(),
answer: newFaqAnswer.trim(),
imageUrl: newFaqImageUrl.trim() || undefined,
};

const currentFaqs = getValues('faqs') || [];


const updatedFaqs = [...currentFaqs, newFaq];

setValue('faqs', updatedFaqs, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});

// Clear input fields


setNewFaqQuestion('');
setNewFaqAnswer('');
setNewFaqImageUrl('');

setHasUnsavedChanges(true);
await trigger('faqs');
};

const handleRemoveFaq = async (id: string) => {


const currentFaqs = getValues('faqs') || [];
const updatedFaqs = currentFaqs.filter(f => f.id !== id);

setValue('faqs', updatedFaqs, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});

setHasUnsavedChanges(true);
await trigger('faqs');
};

const handleUpdateFaq = async (id: string, updates: Partial<ItemFAQ>) => {


const currentFaqs = getValues('faqs') || [];
const updatedFaqs = currentFaqs.map(faq =>
faq.id === id ? { ...faq, ...updates } : faq
);

setValue('faqs', updatedFaqs, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});

setHasUnsavedChanges(true);
await trigger('faqs');
};

// Helper function to validate URL


const isValidUrl = (string: string) => {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
};

const isMaxLengthApplicable = ['text', 'email', 'alphabetical',


'number'].includes(currentDynamicFieldType);

// Check if save button should be enabled


const canSave = (isDirty || hasUnsavedChanges) && !isSaving &&
Object.keys(errors).length === 0;
if (isFetching) {
return (
<div className="space-y-6">
<Skeleton className="h-40 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-64 w-full" />
</div>
);
}

return (
<div className="space-y-6 md:space-y-8 pb-4">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 md:space-y-8
animate-slideInUp">
<Card className="shadow-lg">
<CardHeader><CardTitle className="text-xl md:text-2xl flex items-
center"><ImageIcon className="mr-3 h-5 w-5" /> Item
Details</CardTitle></CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-
4">
<div className="space-y-4">
<div>
<Label htmlFor="title">Item Title</Label>
<Input id="title" {...form.register('title')} placeholder="e.g.,
Valorant" disabled={isSaving} />
{errors.title && <p className="text-xs text-destructive mt-
1">{errors.title.message}</p>}
</div>
<div>
<Label htmlFor="category">Category</Label>
<Input id="category" {...form.register('category')}
placeholder="e.g., Tactical Shooter" disabled={isSaving} />
{errors.category && <p className="text-xs text-destructive mt-
1">{errors.category.message}</p>}
</div>
<div>
<Label htmlFor="order">Display Order</Label>
<Input id="order" type="number" {...form.register('order')}
placeholder="0" disabled={isSaving} />
<p className="text-xs text-muted-foreground pt-1">Lower numbers
appear first within the section.</p>
{errors.order && <p className="text-xs text-destructive mt-
1">{errors.order.message}</p>}
</div>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="imageSrc">Image URL</Label>
<Input id="imageSrc" type="url" {...form.register('imageSrc')}
placeholder="https://example.com/image.png" disabled={isSaving} />
{errors.imageSrc && <p className="text-xs text-destructive mt-
1">{errors.imageSrc.message}</p>}
</div>
{imageSrcPreview && (
<div className="mt-2">
<Label>Image Preview:</Label>
<div className="relative aspect-square w-full max-w-xs mt-1
border rounded-md overflow-hidden bg-muted">
<Image
src={imageSrcPreview}
alt={form.getValues('imageAlt') || 'Preview'}
fill
style={{objectFit: 'cover'}}
onError={() => toast({ variant: 'destructive', title:
'Invalid image URL' })}
/>
</div>
</div>
)}
</div>
<div className="md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-x-6
gap-y-4">
<div>
<Label htmlFor="imageAlt">Image Alt Text (Optional)</Label>
<Input id="imageAlt" {...form.register('imageAlt')}
placeholder="Descriptive alt text for accessibility" disabled={isSaving} />
</div>
<div>
<Label htmlFor="aiHint">AI Hint (Optional)</Label>
<Input id="aiHint" {...form.register('aiHint')}
placeholder="Keywords for AI image search" disabled={isSaving} />
<p className="text-xs text-muted-foreground pt-1">Max 2 keywords.
If blank, uses title.</p>
</div>
</div>
</CardContent>
</Card>

<Accordion type="single" collapsible className="w-full space-y-6 md:space-


y-8" defaultValue="item-1">
<AccordionItem value="item-1" className="border-none">
<Card className="shadow-lg">
<AccordionTrigger className="hover:no-underline">
<CardHeader className="flex-1 text-left p-4 sm:p-6">
<CardTitle className="text-xl md:text-2xl flex items-
center"><Settings2 className="mr-3 h-5 w-5"/> Custom Input Fields</CardTitle>
<CardDescription className="pt-1">Configure fields for
users to fill on the product page.</CardDescription>
</CardHeader>
</AccordionTrigger>
<AccordionContent>
<CardContent className="space-y-4 -mt-4">
<div className="space-y-4 p-4 border rounded-md bg-
muted/50">
<h4 className="font-medium">Add New Field</h4>
<div className="grid grid-cols-1 md:grid-cols-2
gap-4">
<div>
<Label htmlFor="dynamicFieldLabel">Field
Label</Label>
<Input
id="dynamicFieldLabel"
value={currentDynamicFieldLabel}
onChange={e =>
setCurrentDynamicFieldLabel(e.target.value)}
placeholder="e.g., Player ID"
disabled={isSaving}
/>
</div>
<div>
<Label htmlFor="dynamicFieldType">Field
Type</Label>
<Select
value={currentDynamicFieldType}
onValueChange={(value) =>
setCurrentDynamicFieldType(value as DynamicFieldConfig['type'])}
disabled={isSaving}
>
<SelectTrigger
id="dynamicFieldType"><SelectValue placeholder="Select type" /></SelectTrigger>
<SelectContent>
<SelectItem
value="text">Text</SelectItem>
<SelectItem
value="email">Email</SelectItem>
<SelectItem
value="number">Number</SelectItem>
<SelectItem
value="alphabetical">Alphabetical</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label
htmlFor="dynamicFieldPlaceholder">Placeholder (Optional)</Label>
<Input
id="dynamicFieldPlaceholder"
value={currentDynamicFieldPlaceholder}
onChange={e =>
setCurrentDynamicFieldPlaceholder(e.target.value)}
placeholder="e.g., Enter your username"
disabled={isSaving}
/>
</div>
{isMaxLengthApplicable && (
<div>
<Label htmlFor="dynamicFieldMaxLength">Max
Length (Optional)</Label>
<Input
id="dynamicFieldMaxLength"
type="number"
value={currentDynamicFieldMaxLength}
onChange={e =>
setCurrentDynamicFieldMaxLength(e.target.value)}
placeholder="e.g., 50"
disabled={isSaving}
min="1"
/>
</div>
)}
<div className="flex items-center space-x-2">
<Switch
id="dynamicFieldRequired"
checked={currentDynamicFieldRequired}

onCheckedChange={setCurrentDynamicFieldRequired}
disabled={isSaving}
/>
<Label htmlFor="dynamicFieldRequired">Required
Field</Label>
</div>
<Button
type="button"
size="sm"
onClick={handleAddDynamicField}
disabled={isSaving || !
currentDynamicFieldLabel.trim()}
>
<PlusCircle className="mr-2 h-4 w-4"/> Add
Field
</Button>
</div>
{dynamicFields.length > 0 && (
<div>
<h4 className="font-medium mb-2">Configured
Fields:</h4>
<div className="space-y-2">
{dynamicFields.map((field) => (
<DynamicFieldItem
key={field.id}
field={field}
isEditing={editingDynamicField ===
field.id}
onEdit={() =>
setEditingDynamicField(field.id)}
onCancel={() =>
setEditingDynamicField(null)}
onSave={(updates) => {
handleUpdateDynamicField(field.id,
updates);
setEditingDynamicField(null);
}}
onRemove={() =>
handleRemoveDynamicField(field.id)}
disabled={isSaving}
/>
))}
</div>
</div>
)}
</CardContent>
</AccordionContent>
</Card>
</AccordionItem>
<AccordionItem value="item-2" className="border-none">
<Card className="shadow-lg">
<AccordionTrigger className="hover:no-underline">
<CardHeader className="flex-1 text-left p-4 sm:p-6">
<CardTitle className="text-xl md:text-2xl flex items-
center"><MessageSquareMore className="mr-3 h-5 w-5"/> Item-Specific
FAQs</CardTitle>
<CardDescription className="pt-1">Add FAQs that will
appear only on this item's product page.</CardDescription>
</CardHeader>
</AccordionTrigger>
<AccordionContent>
<CardContent className="space-y-4 -mt-4">
<div className="space-y-4 p-4 border rounded-md bg-
muted/50">
<h4 className="font-medium">Add New FAQ</h4>
<div>
<Label
htmlFor="newFaqQuestion">Question</Label>
<Input
id="newFaqQuestion"
value={newFaqQuestion}
onChange={e =>
setNewFaqQuestion(e.target.value)}
placeholder="e.g., How to claim bonus?"
disabled={isSaving}
/>
</div>
<div>
<Label htmlFor="newFaqAnswer">Answer</Label>
<Textarea
id="newFaqAnswer"
value={newFaqAnswer}
onChange={e =>
setNewFaqAnswer(e.target.value)}
placeholder="Detailed answer..."
rows={3}
disabled={isSaving}
/>
</div>
<div>
<Label htmlFor="newFaqImageUrl">Image URL
(Optional)</Label>
<Input
id="newFaqImageUrl"
type="url"
value={newFaqImageUrl}
onChange={e =>
setNewFaqImageUrl(e.target.value)}
placeholder="https://example.com/faq-
image.png"
disabled={isSaving}
/>
</div>
<Button
type="button"
size="sm"
onClick={handleAddFaq}
disabled={isSaving || !newFaqQuestion.trim() || !
newFaqAnswer.trim()}
>
<PlusCircle className="mr-2 h-4 w-4"/> Add FAQ
</Button>
</div>
{faqs.length > 0 && (
<div>
<h4 className="font-medium mb-2">Configured
FAQs:</h4>
<div className="space-y-2">
{faqs.map((faq) => (
<FaqItem
key={faq.id}
faq={faq}
isEditing={editingFaq === faq.id}
onEdit={() => setEditingFaq(faq.id)}
onCancel={() => setEditingFaq(null)}
onSave={(updates) => {
handleUpdateFaq(faq.id, updates);
setEditingFaq(null);
}}
onRemove={() =>
handleRemoveFaq(faq.id)}
disabled={isSaving}
/>
))}
</div>
</div>
)}
</CardContent>
</AccordionContent>
</Card>
</AccordionItem>
</Accordion>

<Button
type="submit"
disabled={!canSave}
size="lg"
className="w-full"
>
{isSaving ? <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> : <Save
className="mr-2 h-4 w-4" />}
{isSaving ? 'Saving Changes...' : 'Save All Changes'}
</Button>

{(isDirty || hasUnsavedChanges) && !isSaving && (


<p className="text-xs text-amber-600 text-center">You have unsaved
changes</p>
)}
</form>
</div>
);
}

// Component for editing dynamic fields


function DynamicFieldItem({ field, isEditing, onEdit, onCancel, onSave, onRemove,
disabled }: {
field: DynamicFieldConfig;
isEditing: boolean;
onEdit: () => void;
onCancel: () => void;
onSave: (updates: Partial<DynamicFieldConfig>) => void;
onRemove: () => void;
disabled: boolean;
}) {
const [editLabel, setEditLabel] = useState(field.label);
const [editPlaceholder, setEditPlaceholder] = useState(field.placeholder || '');
const [editType, setEditType] = useState(field.type);
const [editRequired, setEditRequired] = useState(field.required || false);
const [editMaxLength, setEditMaxLength] = useState(field.maxLength?.toString() ||
'');

const handleSave = () => {


if (!editLabel.trim()) return;

const maxLengthValue = editMaxLength ? parseInt(editMaxLength, 10) : undefined;

onSave({
label: editLabel.trim(),
placeholder: editPlaceholder.trim() || undefined,
type: editType,
required: editRequired,
...(maxLengthValue && maxLengthValue > 0 && { maxLength: maxLengthValue })
});
};

const isMaxLengthApplicable = ['text', 'email', 'alphabetical',


'number'].includes(editType);

if (isEditing) {
return (
<div className="p-3 border rounded-md bg-card shadow-sm space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<Label>Field Label</Label>
<Input
value={editLabel}
onChange={e => setEditLabel(e.target.value)}
disabled={disabled}
size={undefined}
/>
</div>
<div>
<Label>Field Type</Label>
<Select value={editType} onValueChange={(value) => setEditType(value as
DynamicFieldConfig['type'])} disabled={disabled}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="text">Text</SelectItem>
<SelectItem value="email">Email</SelectItem>
<SelectItem value="number">Number</SelectItem>
<SelectItem value="alphabetical">Alphabetical</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label>Placeholder</Label>
<Input
value={editPlaceholder}
onChange={e => setEditPlaceholder(e.target.value)}
disabled={disabled}
size={undefined}
/>
</div>
{isMaxLengthApplicable && (
<div>
<Label>Max Length</Label>
<Input
type="number"
value={editMaxLength}
onChange={e => setEditMaxLength(e.target.value)}
disabled={disabled}
min="1"
size={undefined}
/>
</div>
)}
<div className="flex items-center space-x-2">
<Switch
checked={editRequired}
onCheckedChange={setEditRequired}
disabled={disabled}
/>
<Label>Required Field</Label>
</div>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" size="sm" onClick={onCancel}
disabled={disabled}>
<X className="mr-1 h-3 w-3"/> Cancel
</Button>
<Button type="button" size="sm" onClick={handleSave} disabled={disabled
|| !editLabel.trim()}>
<Check className="mr-1 h-3 w-3"/> Save
</Button>
</div>
</div>
);
}

return (
<div className="flex items-center justify-between p-3 border rounded-md bg-card
shadow-sm text-sm">
<div>
<p><span className="font-semibold">{field.label}</span> ({field.type})</p>
<p className="text-xs text-muted-foreground">
Required: {field.required ? 'Yes' : 'No'} | Max Length: {field.maxLength
|| 'N/A'}
{field.placeholder && ` | Placeholder: "${field.placeholder}"`}
</p>
</div>
<div className="flex space-x-1">
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={onEdit}
disabled={disabled}
>
<Edit3 className="h-4 w-4"/>
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={onRemove}
disabled={disabled}
>
<Trash2 className="h-4 w-4 text-destructive"/>
</Button>
</div>
</div>
);
}

// Component for editing FAQs


function FaqItem({ faq, isEditing, onEdit, onCancel, onSave, onRemove, disabled }:
{
faq: ItemFAQ;
isEditing: boolean;
onEdit: () => void;
onCancel: () => void;
onSave: (updates: Partial<ItemFAQ>) => void;
onRemove: () => void;
disabled: boolean;
}) {
const [editQuestion, setEditQuestion] = useState(faq.question);
const [editAnswer, setEditAnswer] = useState(faq.answer);
const [editImageUrl, setEditImageUrl] = useState(faq.imageUrl || '');

const handleSave = () => {


if (!editQuestion.trim() || !editAnswer.trim()) return;

// Validate image URL if provided


if (editImageUrl.trim() && !isValidUrl(editImageUrl.trim())) {
return;
}

onSave({
question: editQuestion.trim(),
answer: editAnswer.trim(),
imageUrl: editImageUrl.trim() || undefined,
});
};

const isValidUrl = (string: string) => {


try {
new URL(string);
return true;
} catch (_) {
return false;
}
};

if (isEditing) {
return (
<div className="bg-card border rounded-md p-4 shadow-sm space-y-3">
<div>
<Label>Question</Label>
<Input
value={editQuestion}
onChange={e => setEditQuestion(e.target.value)}
disabled={disabled}
size={undefined}
/>
</div>
<div>
<Label>Answer</Label>
<Textarea
value={editAnswer}
onChange={e => setEditAnswer(e.target.value)}
rows={3}
disabled={disabled}
/>
</div>
<div>
<Label>Image URL (Optional)</Label>
<Input
type="url"
value={editImageUrl}
onChange={e => setEditImageUrl(e.target.value)}
disabled={disabled}
size={undefined}
/>
</div>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" size="sm" onClick={onCancel}
disabled={disabled}>
<X className="mr-1 h-3 w-3"/> Cancel
</Button>
<Button
type="button"
size="sm"
onClick={handleSave}
disabled={disabled || !editQuestion.trim() || !editAnswer.trim()}
>
<Check className="mr-1 h-3 w-3"/> Save
</Button>
</div>
</div>
);
}

return (
<AccordionItem value={faq.id} className="bg-card border rounded-md px-1 shadow-
sm">
<div className="flex items-center justify-between group p-1">
<AccordionTrigger className="text-left font-semibold hover:no-underline px-
3 py-2 text-sm flex-1">
{faq.question}
</AccordionTrigger>
<div className="flex space-x-1 opacity-50 group-hover:opacity-100">
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
disabled={disabled}
className="h-8 w-8"
>
<Edit3 className="h-4 w-4"/>
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
disabled={disabled}
className="h-8 w-8"
>
<Trash2 className="h-4 w-4 text-destructive"/>
</Button>
</div>
</div>
<AccordionContent className="px-4 pb-3 pt-0 text-sm text-muted-foreground">
<p className="whitespace-pre-line">{faq.answer}</p>
{faq.imageUrl && <p className="mt-1 text-xs">Image: {faq.imageUrl}</p>}
</AccordionContent>
</AccordionItem>
);
}

export default function EditItemPage() {


return (
<Suspense fallback={<p>Loading editor...</p>}>
<EditItemContent />
</Suspense>
)
}

You might also like