mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 00:11:35 +00:00
Enable users to create and modify literary works with metadata and content
Implements the WorkEditor component with form using React Hook Form and Zod. Replit-Commit-Author: Agent Replit-Commit-Session-Id: bddfbb2b-6d6b-457b-b18c-05792cdaa035 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/e62d5cbe-5096-47f5-a9d2-160416058752.jpg
This commit is contained in:
parent
5fc570e3f2
commit
47743e62cc
@ -27,7 +27,7 @@ This document tracks the implementation status of all components for the Tercul
|
||||
| Component | Status | File Path | Notes |
|
||||
|-----------|--------|-----------|-------|
|
||||
| Work Preview | ✅ Implemented | `client/src/components/work/work-preview.tsx` | Complete with multiple display variants |
|
||||
| Work Editor | ⬜️ Planned | `client/src/components/work/work-editor.tsx` | |
|
||||
| Work Editor | ✅ Implemented | `client/src/components/work/work-editor.tsx` | Complete form with metadata, content editor, and preview |
|
||||
| Work Header | ✅ Implemented | `client/src/components/work/work-header.tsx` | Complete with metadata, actions, and stats display |
|
||||
| Comparison View | ⬜️ Planned | `client/src/components/work/comparison-view.tsx` | |
|
||||
|
||||
|
||||
@ -0,0 +1,722 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useForm } 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 { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import TagInput, { TagItem } from "@/components/ui/tag-input";
|
||||
import { FileUploader } from "@/components/ui/file-uploader";
|
||||
import { RichTextEditor } from "@/components/ui/rich-text-editor";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { AlertCircle, BookOpen, BookText, Trash2 } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Author } from "./work-header";
|
||||
|
||||
/**
|
||||
* Work Editor component for creating and editing literary works
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <WorkEditor
|
||||
* work={existingWork}
|
||||
* authors={authors}
|
||||
* languages={languages}
|
||||
* genres={genres}
|
||||
* onSave={handleSave}
|
||||
* onCancel={handleCancel}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Define the schema for the work form
|
||||
const workFormSchema = z.object({
|
||||
title: z.string().min(2, "Title must be at least 2 characters").max(200, "Title must be less than 200 characters"),
|
||||
subtitle: z.string().optional(),
|
||||
authorId: z.string().min(1, "Author is required"),
|
||||
description: z.string().optional(),
|
||||
publicationYear: z.string()
|
||||
.refine(val => !val || /^\d{4}$/.test(val), {
|
||||
message: "Year must be a 4-digit number",
|
||||
})
|
||||
.transform(val => val ? parseInt(val) : undefined)
|
||||
.optional(),
|
||||
language: z.string().min(1, "Language is required"),
|
||||
content: z.string().min(10, "Content is required").optional(),
|
||||
status: z.enum(["draft", "review", "published", "archived"]).default("draft"),
|
||||
});
|
||||
|
||||
// Infer the form values type from the schema
|
||||
type WorkFormValues = z.infer<typeof workFormSchema>;
|
||||
|
||||
export interface Work {
|
||||
id?: number | string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
authorId: number | string;
|
||||
description?: string;
|
||||
publicationYear?: number;
|
||||
language: string;
|
||||
content?: string;
|
||||
status?: 'draft' | 'review' | 'published' | 'archived';
|
||||
genres?: string[];
|
||||
coverImage?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export interface WorkEditorProps {
|
||||
/**
|
||||
* Work data for editing (optional, if creating new)
|
||||
*/
|
||||
work?: Partial<Work>;
|
||||
/**
|
||||
* Available authors
|
||||
*/
|
||||
authors: Author[];
|
||||
/**
|
||||
* Available languages
|
||||
*/
|
||||
languages: { code: string; name: string }[];
|
||||
/**
|
||||
* Available genres for selection
|
||||
*/
|
||||
genres?: { id: string | number; name: string }[];
|
||||
/**
|
||||
* Whether the form is in loading state
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
/**
|
||||
* Whether the form is in read-only mode
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* Whether to show translation fields
|
||||
*/
|
||||
showTranslationFields?: boolean;
|
||||
/**
|
||||
* Whether to show a preview tab
|
||||
*/
|
||||
showPreview?: boolean;
|
||||
/**
|
||||
* Callback when saving the work
|
||||
*/
|
||||
onSave?: (data: Work) => void;
|
||||
/**
|
||||
* Callback when publication status changes
|
||||
*/
|
||||
onStatusChange?: (status: string) => void;
|
||||
/**
|
||||
* Callback when canceling
|
||||
*/
|
||||
onCancel?: () => void;
|
||||
/**
|
||||
* Callback for deleting a work
|
||||
*/
|
||||
onDelete?: (id: number | string) => void;
|
||||
/**
|
||||
* CSS class for the container
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WorkEditor({
|
||||
work,
|
||||
authors = [],
|
||||
languages = [],
|
||||
genres = [],
|
||||
isLoading = false,
|
||||
readOnly = false,
|
||||
showTranslationFields = false,
|
||||
showPreview = true,
|
||||
onSave,
|
||||
onStatusChange,
|
||||
onCancel,
|
||||
onDelete,
|
||||
className,
|
||||
}: WorkEditorProps) {
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
const [coverImage, setCoverImage] = useState<File | null>(null);
|
||||
const [coverPreview, setCoverPreview] = useState<string | undefined>(work?.coverImage);
|
||||
const [selectedGenres, setSelectedGenres] = useState<TagItem[]>(
|
||||
work?.genres?.map(genre => ({ value: genre, label: genre })) || []
|
||||
);
|
||||
const [previewMode, setPreviewMode] = useState(false);
|
||||
const [workContent, setWorkContent] = useState(work?.content || "");
|
||||
|
||||
// Initialize form with default values
|
||||
const form = useForm({
|
||||
resolver: zodResolver(workFormSchema),
|
||||
defaultValues: {
|
||||
title: work?.title || "",
|
||||
subtitle: work?.subtitle || "",
|
||||
authorId: work?.authorId?.toString() || "",
|
||||
description: work?.description || "",
|
||||
publicationYear: work?.publicationYear?.toString() || "",
|
||||
language: work?.language || "",
|
||||
content: work?.content || "",
|
||||
status: work?.status || "draft",
|
||||
},
|
||||
mode: "onChange",
|
||||
}) as any;
|
||||
|
||||
// Watch values for preview
|
||||
const watchedValues = form.watch();
|
||||
|
||||
// Handle form submission
|
||||
const onSubmit = (formData: any) => {
|
||||
if (onSave) {
|
||||
// Convert to our strongly typed form values
|
||||
const data = formData as WorkFormValues;
|
||||
|
||||
// Prepare the work data
|
||||
const workData: Work = {
|
||||
title: data.title,
|
||||
language: data.language,
|
||||
authorId: data.authorId,
|
||||
subtitle: data.subtitle,
|
||||
description: data.description,
|
||||
publicationYear: data.publicationYear,
|
||||
content: data.content,
|
||||
status: data.status,
|
||||
genres: selectedGenres.map(tag => tag.value),
|
||||
};
|
||||
|
||||
// If there's a new cover image, we'd typically upload it here
|
||||
// and add the URL to workData
|
||||
if (coverImage) {
|
||||
// In a real app, you'd upload the file and get a URL
|
||||
// workData.coverImage = uploadedUrl;
|
||||
} else if (coverPreview) {
|
||||
workData.coverImage = coverPreview;
|
||||
}
|
||||
|
||||
// Add content if we have it
|
||||
if (workContent) {
|
||||
workData.content = workContent;
|
||||
}
|
||||
|
||||
// If editing, include the ID
|
||||
if (work?.id) {
|
||||
workData.id = work.id;
|
||||
}
|
||||
|
||||
onSave(workData);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle status change
|
||||
const handleStatusChange = (status: string) => {
|
||||
form.setValue("status", status as any);
|
||||
onStatusChange?.(status);
|
||||
};
|
||||
|
||||
// Handle file selection for cover image
|
||||
const handleCoverImageSelect = (files: File[]) => {
|
||||
if (files.length > 0) {
|
||||
setCoverImage(files[0]);
|
||||
setCoverPreview(URL.createObjectURL(files[0]));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-6", className)}>
|
||||
{/* Form title */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{work?.id ? "Edit Work" : "Create New Work"}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{work?.id
|
||||
? "Update the details of your literary work"
|
||||
: "Enter the details of your new literary work"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status selection for existing works */}
|
||||
{work?.id && (
|
||||
<Select
|
||||
value={form.getValues("status")}
|
||||
onValueChange={handleStatusChange}
|
||||
disabled={isLoading || readOnly}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
<SelectItem value="review">Under Review</SelectItem>
|
||||
<SelectItem value="published">Published</SelectItem>
|
||||
<SelectItem value="archived">Archived</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main form */}
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Tabs
|
||||
defaultValue="basic"
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid grid-cols-3">
|
||||
<TabsTrigger value="basic">Basic Information</TabsTrigger>
|
||||
<TabsTrigger value="content">
|
||||
Content
|
||||
</TabsTrigger>
|
||||
{showPreview && (
|
||||
<TabsTrigger value="preview">
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
{/* Basic Information Tab */}
|
||||
<TabsContent value="basic" className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Left column - Metadata */}
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Work Details</CardTitle>
|
||||
<CardDescription>Basic information about this literary work</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Title */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter the title of your work"
|
||||
{...field}
|
||||
disabled={isLoading || readOnly}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Subtitle */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subtitle"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Subtitle (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a subtitle if applicable"
|
||||
{...field}
|
||||
disabled={isLoading || readOnly}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Author */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authorId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Author</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
disabled={isLoading || readOnly}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an author" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{authors.map((author) => (
|
||||
<SelectItem
|
||||
key={author.id.toString()}
|
||||
value={author.id.toString()}
|
||||
>
|
||||
{author.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Language */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Language</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
disabled={isLoading || readOnly}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{languages.map((language) => (
|
||||
<SelectItem
|
||||
key={language.code}
|
||||
value={language.code}
|
||||
>
|
||||
{language.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Publication Year */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="publicationYear"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Publication Year</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="e.g. 1866"
|
||||
{...field}
|
||||
disabled={isLoading || readOnly}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Enter a brief description or synopsis"
|
||||
className="min-h-[120px]"
|
||||
{...field}
|
||||
disabled={isLoading || readOnly}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Genres */}
|
||||
<FormItem>
|
||||
<FormLabel>Genres</FormLabel>
|
||||
<TagInput
|
||||
placeholder="Add genres"
|
||||
tags={selectedGenres}
|
||||
onTagsChange={setSelectedGenres}
|
||||
suggestions={genres.map(genre => ({
|
||||
value: genre.name,
|
||||
label: genre.name
|
||||
}))}
|
||||
allowNew={true}
|
||||
readOnly={readOnly}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<FormDescription>
|
||||
Select from existing genres or create new ones
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Translation Fields (if applicable) */}
|
||||
{showTranslationFields && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Translation Information</CardTitle>
|
||||
<CardDescription>Details about this translation</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="translator">Translator</Label>
|
||||
<Input
|
||||
id="translator"
|
||||
placeholder="Translator's name"
|
||||
disabled={isLoading || readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="translation-year">Translation Year</Label>
|
||||
<Input
|
||||
id="translation-year"
|
||||
placeholder="e.g. 2020"
|
||||
disabled={isLoading || readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="translation-notes">Translation Notes</Label>
|
||||
<Textarea
|
||||
id="translation-notes"
|
||||
placeholder="Enter any notes about this translation"
|
||||
className="min-h-[100px]"
|
||||
disabled={isLoading || readOnly}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right column - Cover Image */}
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cover Image</CardTitle>
|
||||
<CardDescription>Upload a cover image for your work</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Image preview */}
|
||||
{coverPreview ? (
|
||||
<div className="relative aspect-[2/3] rounded-md overflow-hidden border">
|
||||
<img
|
||||
src={coverPreview}
|
||||
alt="Cover preview"
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 h-8 w-8 rounded-full"
|
||||
onClick={() => {
|
||||
setCoverImage(null);
|
||||
setCoverPreview(undefined);
|
||||
}}
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="sr-only">Remove cover image</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="aspect-[2/3] rounded-md border border-dashed flex items-center justify-center bg-muted/50">
|
||||
<div className="text-center p-4">
|
||||
<BookText className="h-8 w-8 mx-auto text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
No cover image
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File uploader */}
|
||||
{!readOnly && (
|
||||
<FileUploader
|
||||
onFilesSelected={handleCoverImageSelect}
|
||||
accept=".jpg, .jpeg, .png"
|
||||
maxFiles={1}
|
||||
maxSize={5 * 1024 * 1024} // 5MB
|
||||
buttonText="Select cover image"
|
||||
showSelectedFiles={false}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Content Tab */}
|
||||
<TabsContent value="content" className="py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Work Content</CardTitle>
|
||||
<CardDescription>
|
||||
Enter the content of your literary work
|
||||
</CardDescription>
|
||||
{!form.getValues("content") && (
|
||||
<Alert className="bg-amber-50 text-amber-800 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400 dark:border-amber-700/30">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>No content yet</AlertTitle>
|
||||
<AlertDescription>
|
||||
Enter the complete text of your work in this section.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="min-h-[500px]">
|
||||
<RichTextEditor
|
||||
value={workContent}
|
||||
onChange={setWorkContent}
|
||||
placeholder="Enter your work content here..."
|
||||
readOnly={readOnly || isLoading}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Preview Tab */}
|
||||
{showPreview && (
|
||||
<TabsContent value="preview" className="py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Preview</CardTitle>
|
||||
<CardDescription>
|
||||
Preview how your work will appear to readers
|
||||
</CardDescription>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={previewMode ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setPreviewMode(false)}
|
||||
>
|
||||
<BookOpen className="h-4 w-4 mr-2" />
|
||||
Work View
|
||||
</Button>
|
||||
{/* Additional preview modes could be added here */}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border rounded-lg p-6 min-h-[500px]">
|
||||
{/* Preview title */}
|
||||
<h1 className="text-3xl font-bold font-serif mb-2">
|
||||
{watchedValues.title || "Untitled Work"}
|
||||
</h1>
|
||||
|
||||
{/* Preview subtitle */}
|
||||
{watchedValues.subtitle && (
|
||||
<h2 className="text-xl text-muted-foreground font-serif mb-4">
|
||||
{watchedValues.subtitle}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{/* Author and metadata */}
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
by {authors.find(a => a.id.toString() === watchedValues.authorId)?.name || "Unknown Author"}
|
||||
</span>
|
||||
{watchedValues.publicationYear && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
• {watchedValues.publicationYear}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* Content preview */}
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
{workContent ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: workContent }} />
|
||||
) : (
|
||||
<p className="text-muted-foreground italic">
|
||||
No content to preview. Add content in the "Content" tab.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
|
||||
{/* Form actions */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
{work?.id && onDelete && !readOnly && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => onDelete(work.id!)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Delete Work
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || !form.formState.isValid}
|
||||
>
|
||||
{isLoading ? "Saving..." : (work?.id ? "Update Work" : "Create Work")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkEditor;
|
||||
Loading…
Reference in New Issue
Block a user