inital working version

This commit is contained in:
Stefan Hardegger
2025-07-22 21:49:40 +02:00
parent bebb799784
commit 59d29dceaf
98 changed files with 8027 additions and 856 deletions

View File

@@ -0,0 +1,371 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import AppLayout from '../../../../components/layout/AppLayout';
import { Input, Textarea } from '../../../../components/ui/Input';
import Button from '../../../../components/ui/Button';
import TagInput from '../../../../components/stories/TagInput';
import RichTextEditor from '../../../../components/stories/RichTextEditor';
import ImageUpload from '../../../../components/ui/ImageUpload';
import LoadingSpinner from '../../../../components/ui/LoadingSpinner';
import { storyApi } from '../../../../lib/api';
import { Story } from '../../../../types/api';
export default function EditStoryPage() {
const params = useParams();
const router = useRouter();
const storyId = params.id as string;
const [story, setStory] = useState<Story | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [formData, setFormData] = useState({
title: '',
summary: '',
authorName: '',
contentHtml: '',
sourceUrl: '',
tags: [] as string[],
seriesName: '',
volume: '',
});
const [coverImage, setCoverImage] = useState<File | null>(null);
useEffect(() => {
const loadStory = async () => {
try {
setLoading(true);
const storyData = await storyApi.getStory(storyId);
setStory(storyData);
// Initialize form with story data
setFormData({
title: storyData.title,
summary: storyData.summary || '',
authorName: storyData.authorName,
contentHtml: storyData.contentHtml,
sourceUrl: storyData.sourceUrl || '',
tags: storyData.tags?.map(tag => tag.name) || [],
seriesName: storyData.seriesName || '',
volume: storyData.volume?.toString() || '',
});
} catch (error) {
console.error('Failed to load story:', error);
router.push('/library');
} finally {
setLoading(false);
}
};
if (storyId) {
loadStory();
}
}, [storyId, router]);
const handleInputChange = (field: string) => (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const handleContentChange = (html: string) => {
setFormData(prev => ({ ...prev, contentHtml: html }));
if (errors.contentHtml) {
setErrors(prev => ({ ...prev, contentHtml: '' }));
}
};
const handleTagsChange = (tags: string[]) => {
setFormData(prev => ({ ...prev, tags }));
};
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.title.trim()) {
newErrors.title = 'Title is required';
}
if (!formData.authorName.trim()) {
newErrors.authorName = 'Author name is required';
}
if (!formData.contentHtml.trim()) {
newErrors.contentHtml = 'Story content is required';
}
if (formData.seriesName && !formData.volume) {
newErrors.volume = 'Volume number is required when series is specified';
}
if (formData.volume && !formData.seriesName.trim()) {
newErrors.seriesName = 'Series name is required when volume is specified';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm() || !story) {
return;
}
setSaving(true);
try {
// Update the story with JSON data
const updateData = {
title: formData.title,
summary: formData.summary || undefined,
contentHtml: formData.contentHtml,
sourceUrl: formData.sourceUrl || undefined,
volume: formData.seriesName ? parseInt(formData.volume) : undefined,
authorId: story.authorId, // Keep existing author ID
seriesId: story.seriesId, // Keep existing series ID for now
tagNames: formData.tags,
};
const updatedStory = await storyApi.updateStory(storyId, updateData);
// If there's a new cover image, upload it separately
if (coverImage) {
await storyApi.uploadCover(storyId, coverImage);
}
router.push(`/stories/${storyId}`);
} catch (error: any) {
console.error('Failed to update story:', error);
const errorMessage = error.response?.data?.message || 'Failed to update story';
setErrors({ submit: errorMessage });
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!story || !confirm('Are you sure you want to delete this story? This action cannot be undone.')) {
return;
}
try {
setSaving(true);
await storyApi.deleteStory(storyId);
router.push('/library');
} catch (error) {
console.error('Failed to delete story:', error);
setErrors({ submit: 'Failed to delete story' });
} finally {
setSaving(false);
}
};
if (loading) {
return (
<AppLayout>
<div className="flex items-center justify-center py-20">
<LoadingSpinner size="lg" />
</div>
</AppLayout>
);
}
if (!story) {
return (
<AppLayout>
<div className="text-center py-20">
<h1 className="text-2xl font-bold theme-header mb-4">Story Not Found</h1>
<Button href="/library">Back to Library</Button>
</div>
</AppLayout>
);
}
return (
<AppLayout>
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold theme-header">Edit Story</h1>
<p className="theme-text mt-2">
Make changes to &quot;{story.title}&quot;
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Title */}
<Input
label="Title *"
value={formData.title}
onChange={handleInputChange('title')}
placeholder="Enter the story title"
error={errors.title}
required
/>
{/* Author - Display only, not editable in edit mode for simplicity */}
<Input
label="Author *"
value={formData.authorName}
onChange={handleInputChange('authorName')}
placeholder="Enter the author's name"
error={errors.authorName}
disabled
/>
<p className="text-sm theme-text mt-1">
Author changes should be done through Author management
</p>
{/* Summary */}
<div>
<label className="block text-sm font-medium theme-header mb-2">
Summary
</label>
<Textarea
value={formData.summary}
onChange={handleInputChange('summary')}
placeholder="Brief summary or description of the story..."
rows={3}
/>
<p className="text-sm theme-text mt-1">
Optional summary that will be displayed on the story detail page
</p>
</div>
{/* Cover Image Upload */}
<div>
<label className="block text-sm font-medium theme-header mb-2">
Cover Image
</label>
<ImageUpload
onImageSelect={setCoverImage}
accept="image/jpeg,image/png,image/webp"
maxSizeMB={5}
aspectRatio="3:4"
placeholder="Drop a new cover image here or click to select"
/>
{story.coverPath && !coverImage && (
<p className="text-sm theme-text mt-2">
Current cover will be kept unless you upload a new one.
</p>
)}
</div>
{/* Content */}
<div>
<label className="block text-sm font-medium theme-header mb-2">
Story Content *
</label>
<RichTextEditor
value={formData.contentHtml}
onChange={handleContentChange}
placeholder="Edit your story content here..."
error={errors.contentHtml}
/>
</div>
{/* Tags */}
<div>
<label className="block text-sm font-medium theme-header mb-2">
Tags
</label>
<TagInput
tags={formData.tags}
onChange={handleTagsChange}
placeholder="Edit tags to categorize your story..."
/>
</div>
{/* Series and Volume */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Input
label="Series (optional)"
value={formData.seriesName}
onChange={handleInputChange('seriesName')}
placeholder="Enter series name if part of a series"
error={errors.seriesName}
disabled
/>
<p className="text-sm theme-text mt-1">
Series changes not yet supported in edit mode
</p>
</div>
<Input
label="Volume/Part (optional)"
type="number"
min="1"
value={formData.volume}
onChange={handleInputChange('volume')}
placeholder="Enter volume/part number"
error={errors.volume}
disabled={!formData.seriesName}
/>
</div>
{/* Source URL */}
<Input
label="Source URL (optional)"
type="url"
value={formData.sourceUrl}
onChange={handleInputChange('sourceUrl')}
placeholder="https://example.com/original-story-url"
/>
{/* Submit Error */}
{errors.submit && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-800 dark:text-red-200">{errors.submit}</p>
</div>
)}
{/* Actions */}
<div className="flex justify-between pt-6">
<Button
type="button"
variant="ghost"
onClick={handleDelete}
disabled={saving}
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
Delete Story
</Button>
<div className="flex gap-4">
<Button
type="button"
variant="ghost"
onClick={() => router.push(`/stories/${storyId}`)}
disabled={saving}
>
Cancel
</Button>
<Button
type="submit"
loading={saving}
disabled={!formData.title || !formData.authorName || !formData.contentHtml}
>
Save Changes
</Button>
</div>
</div>
</form>
</div>
</AppLayout>
);
}