This commit is contained in:
Tomas Dvorak
2025-10-21 15:02:05 +02:00
parent 68e69e00cc
commit 63700eedb2
103 changed files with 12442 additions and 446 deletions
+103 -15
View File
@@ -57,6 +57,9 @@ import { MapCoordinates } from '../../utils/mapUrlParser';
import ContactMap from '../../components/home/ContactMap';
import RichTextEditor from '../../components/common/RichTextEditor';
import { getCachedYouTube, YouTubeVideo } from '../../services/youtube';
import SaveStatusIndicator from '../../components/common/SaveStatusIndicator';
import DraftRecoveryModal from '../../components/common/DraftRecoveryModal';
import { useAutoSave, loadDraft, getDraftMetadata } from '../../hooks/useAutoSave';
import { FiVideo, FiYoutube, FiLink } from 'react-icons/fi';
import ThumbnailPreview from '../../components/common/ThumbnailPreview';
import { assetUrl } from '../../utils/url';
@@ -77,6 +80,8 @@ const AdminActivitiesPage: React.FC = () => {
const qc = useQueryClient();
const { isOpen, onOpen, onClose } = useDisclosure();
const [editing, setEditing] = useState<Partial<Event> | null>(null);
const [showDraftRecovery, setShowDraftRecovery] = useState(false);
const [draftKey, setDraftKey] = useState<string>('');
const [aiPrompt, setAiPrompt] = useState<string>('');
const [aiLoading, setAiLoading] = useState<boolean>(false);
const [aiTone, setAiTone] = useState<'informative'|'friendly'|'formal'>('friendly');
@@ -88,6 +93,31 @@ const AdminActivitiesPage: React.FC = () => {
const [clubVideos, setClubVideos] = useState<YouTubeVideo[]>([]);
const [youtubeTab, setYoutubeTab] = useState<'club' | 'custom'>('club');
// Auto-save hook - saves draft automatically
const { saveStatus, lastSaved, forceSave, clearDraft } = useAutoSave({
data: editing || {},
storageKey: draftKey,
onSave: async (data) => {
// If event has ID, update it
if (data.id) {
return await updateEvent(data.id, data);
}
// If no ID and has title, create as draft
if (data.title?.trim() && data.start_time) {
const created = await createEvent(data);
// Update editing state with new ID
if (created?.id) {
setEditing(prev => ({ ...prev, id: created.id } as any));
}
return created;
}
// Don't save if no title or start time
return {};
},
debounceMs: 2000,
enabled: isOpen && editing !== null,
});
const { data, isLoading } = useQuery({
queryKey: ['admin-events'],
queryFn: () => getEvents(),
@@ -115,14 +145,28 @@ const AdminActivitiesPage: React.FC = () => {
staleTime: 5 * 60_000,
});
const openCreate = () => {
setEditing({ title: '', description: '', type: 'other', is_public: true } as any);
setLocationLat(undefined);
setLocationLng(undefined);
onOpen();
const openCreate = () => {
// Check for existing draft
const key = 'draft-activity-new';
setDraftKey(key);
const metadata = getDraftMetadata(key);
if (metadata && metadata.age < 1440) {
// Show recovery modal
setShowDraftRecovery(true);
} else {
// No draft, start fresh
setEditing({ title: '', description: '', type: 'other', is_public: true } as any);
setLocationLat(undefined);
setLocationLng(undefined);
onOpen();
}
};
const openEdit = (ev: Event) => {
setEditing({ ...ev });
const openEdit = (ev: Event) => {
// Set unique draft key for this event
const key = `draft-activity-${ev.id}`;
setDraftKey(key);
setEditing({ ...ev });
// Initialize map coordinates from event
if ((ev as any).latitude && (ev as any).longitude) {
setLocationLat((ev as any).latitude);
@@ -131,13 +175,43 @@ const AdminActivitiesPage: React.FC = () => {
setLocationLat(undefined);
setLocationLng(undefined);
}
onOpen();
onOpen();
};
const closeModal = () => {
setEditing(null);
const closeModal = () => {
setEditing(null);
setLocationLat(undefined);
setLocationLng(undefined);
onClose();
onClose();
};
// Draft recovery handlers
const handleRecoverDraft = () => {
const draft = loadDraft<Partial<Event>>(draftKey);
if (draft) {
setEditing(draft);
// Restore location if present
if ((draft as any)?.latitude && (draft as any)?.longitude) {
setLocationLat((draft as any).latitude);
setLocationLng((draft as any).longitude);
}
onOpen();
}
setShowDraftRecovery(false);
};
const handleDiscardDraft = () => {
clearDraft();
setEditing({ title: '', description: '', type: 'other', is_public: true } as any);
setLocationLat(undefined);
setLocationLng(undefined);
setShowDraftRecovery(false);
onOpen();
};
const handleDeleteOnly = () => {
clearDraft();
setShowDraftRecovery(false);
// Don't open the modal - just delete and close
};
const createMut = useMutation({
@@ -433,10 +507,13 @@ const AdminActivitiesPage: React.FC = () => {
<ModalOverlay backdropFilter="blur(3px)" />
<ModalContent maxW={{ base: '96vw', md: '920px' }} maxH={{ base: '90vh', md: '86vh' }} borderRadius="2xl" overflow="hidden" boxShadow="2xl">
<ModalHeader>
<VStack align="start" spacing={1}>
<Heading size="md">{(editing as any)?.id ? 'Upravit aktivitu' : 'Nová aktivita'}</Heading>
<Text fontSize="sm" color="gray.500">Plánujte klubové akce, sdílejte s fanoušky a týmem.</Text>
</VStack>
<HStack justify="space-between" align="start" w="full" pr={8}>
<VStack align="start" spacing={1} flex={1}>
<Heading size="md">{(editing as any)?.id ? 'Upravit aktivitu' : 'Nová aktivita'}</Heading>
<Text fontSize="sm" color="gray.500">Plánujte klubové akce, sdílejte s fanoušky a týmem.</Text>
</VStack>
<SaveStatusIndicator status={saveStatus} lastSaved={lastSaved} compact />
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody overflowY="auto" maxH={{ base: '76vh', md: '70vh' }}>
@@ -946,6 +1023,17 @@ const AdminActivitiesPage: React.FC = () => {
</ModalFooter>
</ModalContent>
</Modal>
{/* Draft Recovery Modal */}
<DraftRecoveryModal
isOpen={showDraftRecovery}
onClose={() => setShowDraftRecovery(false)}
onRecover={handleRecoverDraft}
onDiscard={handleDiscardDraft}
onDeleteOnly={handleDeleteOnly}
draftAge={getDraftMetadata(draftKey)?.age || null}
entityType="aktivitu"
/>
</Box>
</AdminLayout>
);