263 lines
8.0 KiB
TypeScript
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>
|
|
);
|
|
} |