Files
placebo.mk/frontend/src/components/admin/live-blog/UpdatePublisher.tsx
T
2026-01-29 04:01:55 +01:00

263 lines
8.0 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { useCreateLiveBlogUpdate, useUpdateLiveBlogUpdate, useLiveBlogUpdates } from '@/queries/live-blogs';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import type { LiveBlogUpdate, CreateLiveBlogUpdateDto } from '@/lib/api';
import { cn } from '@/lib/utils';
import { Send, Pin, Calendar, Edit } from 'lucide-react';
interface UpdatePublisherProps {
liveBlogId: string;
authorId?: string;
className?: string;
}
export function UpdatePublisher({ liveBlogId, authorId, className }: UpdatePublisherProps) {
const [content, setContent] = useState('');
const [isPinned, setIsPinned] = useState(false);
const [scheduledAt, setScheduledAt] = useState('');
const [isScheduleMode, setIsScheduleMode] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const createMutation = useCreateLiveBlogUpdate();
const updateMutation = useUpdateLiveBlogUpdate();
const [editingUpdate, setEditingUpdate] = useState<LiveBlogUpdate | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim()) return;
const updateData: CreateLiveBlogUpdateDto = {
content: content.trim(),
isPinned,
authorId,
...(isScheduleMode && scheduledAt ? { scheduledAt } : {}),
};
try {
if (editingUpdate) {
// Update existing update
await updateMutation.mutateAsync({
liveBlogId,
updateId: editingUpdate.id,
dto: {
content: content.trim(),
isPinned,
},
});
setEditingUpdate(null);
} else {
// Create new update
await createMutation.mutateAsync({
liveBlogId,
dto: updateData,
});
}
// Reset form
setContent('');
setIsPinned(false);
setScheduledAt('');
setIsScheduleMode(false);
textareaRef.current?.focus();
} catch (error) {
console.error('Failed to save update:', error);
}
};
const handleEdit = (update: LiveBlogUpdate) => {
setContent(update.content);
setIsPinned(update.isPinned);
setEditingUpdate(update);
setIsScheduleMode(false);
textareaRef.current?.focus();
};
const handleCancelEdit = () => {
setEditingUpdate(null);
setContent('');
setIsPinned(false);
setIsScheduleMode(false);
};
const isLoading = createMutation.isPending || updateMutation.isPending;
// Get minimum date for scheduling (now + 5 minutes)
const minScheduleDate = new Date().toISOString().slice(0, 16);
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{editingUpdate ? 'Edit Update' : 'Publish Update'}
{editingUpdate && (
<span className="text-xs bg-muted px-2 py-1 rounded-full">
ID: {editingUpdate.id.slice(0, 8)}
</span>
)}
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Content textarea */}
<div className="space-y-2">
<Label htmlFor="content">
{editingUpdate ? 'Edit content' : 'Update content'}
</Label>
<Textarea
id="content"
ref={textareaRef}
placeholder="What's the latest update?"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={4}
className="resize-none"
disabled={isLoading}
/>
</div>
{/* Options */}
<div className="flex flex-wrap gap-4">
{/* Pin toggle */}
<div className="flex items-center space-x-2">
<Switch
id="pin"
checked={isPinned}
onCheckedChange={setIsPinned}
disabled={isLoading}
/>
<Label htmlFor="pin" className="flex items-center gap-2">
<Pin className="w-4 h-4" />
Pin to top
</Label>
</div>
{/* Schedule toggle */}
<div className="flex items-center space-x-2">
<Switch
id="schedule"
checked={isScheduleMode}
onCheckedChange={setIsScheduleMode}
disabled={isLoading}
/>
<Label htmlFor="schedule" className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
Schedule
</Label>
</div>
</div>
{/* Schedule date/time picker */}
{isScheduleMode && (
<div className="space-y-2">
<Label htmlFor="scheduleAt">Schedule for (minimum 5 minutes from now)</Label>
<input
id="scheduleAt"
type="datetime-local"
min={minScheduleDate}
value={scheduledAt}
onChange={(e) => setScheduledAt(e.target.value)}
className="w-full p-2 border rounded-md"
disabled={isLoading}
/>
</div>
)}
{/* Action buttons */}
<div className="flex gap-2">
<Button
type="submit"
disabled={!content.trim() || isLoading}
className="flex-1"
>
{isLoading ? (
'Saving...'
) : editingUpdate ? (
'Update'
) : (
<>
<Send className="w-4 h-4 mr-2" />
{isScheduleMode ? 'Schedule' : 'Publish'}
</>
)}
</Button>
{editingUpdate && (
<Button
type="button"
variant="outline"
onClick={handleCancelEdit}
disabled={isLoading}
>
Cancel
</Button>
)}
</div>
</form>
{/* Recent updates for editing */}
{editingUpdate === null && (
<div className="mt-6 pt-6 border-t">
<h4 className="text-sm font-medium mb-3">Recent Updates (Click to edit)</h4>
<RecentUpdates
liveBlogId={liveBlogId}
onEdit={handleEdit}
isLoading={isLoading}
/>
</div>
)}
</CardContent>
</Card>
);
}
interface RecentUpdatesProps {
liveBlogId: string;
onEdit: (update: LiveBlogUpdate) => void;
isLoading: boolean;
}
function RecentUpdates({ liveBlogId, onEdit, isLoading }: RecentUpdatesProps) {
const { data: updatesData } = useLiveBlogUpdates(liveBlogId, 1, 5);
const updates = updatesData?.data || [];
if (updates.length === 0) {
return (
<p className="text-sm text-muted-foreground">No updates yet. Start by publishing your first update!</p>
);
}
return (
<div className="space-y-2">
{updates.map((update) => (
<div
key={update.id}
className={cn(
'p-3 rounded border cursor-pointer hover:bg-muted/50 transition-colors',
isLoading && 'opacity-50 pointer-events-none'
)}
onClick={() => onEdit(update)}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
{update.isPinned && <Pin className="w-3 h-3 text-primary" />}
<span className="text-xs text-muted-foreground">
{new Date(update.createdAt).toLocaleString()}
</span>
</div>
<Edit className="w-3 h-3 text-muted-foreground" />
</div>
<p className="text-sm line-clamp-2">{update.content}</p>
</div>
))}
</div>
);
}