From edbcc272e7cfa8fa307bc23e7cf8fed17d565448 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 19 Sep 2025 18:58:46 +0100 Subject: [PATCH] feat(frontend): implement formatter import/export functionality with refactored ImportModal component --- .../frontend/src/components/menu/filters.tsx | 121 +--------------- .../src/components/menu/formatter.tsx | 75 +++++++++- .../src/components/shared/import-modal.tsx | 129 ++++++++++++++++++ 3 files changed, 204 insertions(+), 121 deletions(-) create mode 100644 packages/frontend/src/components/shared/import-modal.tsx diff --git a/packages/frontend/src/components/menu/filters.tsx b/packages/frontend/src/components/menu/filters.tsx index 7ed87f82..785de92c 100644 --- a/packages/frontend/src/components/menu/filters.tsx +++ b/packages/frontend/src/components/menu/filters.tsx @@ -4,6 +4,7 @@ import { PageWrapper } from '../shared/page-wrapper'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { cn } from '../ui/core/styling'; import { SettingsNavCard } from '../shared/settings-card'; +import { ImportModal } from '../shared/import-modal'; import { useUserData } from '@/context/userData'; import { FaBolt, @@ -3041,126 +3042,6 @@ function TwoTextInputs({ ); } -function ImportModal({ - open, - onOpenChange, - onImport, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - onImport: (data: any) => void; -}) { - const [isLoading, setIsLoading] = useState(false); - const fileInputRef = useRef(null); - const urlModalDisclosure = useDisclosure(false); - const [url, setUrl] = useState(''); - - const handleFileImport = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (e) => { - try { - const content = e.target?.result as string; - const data = JSON.parse(content); - onImport(data); - onOpenChange(false); - } catch (error) { - console.error('Error importing file:', error); - toast.error('Failed to parse JSON file'); - } - }; - reader.readAsText(file); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }; - - const handleUrlImport = async () => { - if (!url) { - toast.error('Please enter a URL'); - return; - } - - setIsLoading(true); - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error('Failed to fetch URL'); - } - const data = await response.json(); - onImport(data); - urlModalDisclosure.close(); - onOpenChange(false); - } catch (error) { - console.error('Error importing from URL:', error); - toast.error('Failed to import from URL'); - } finally { - setIsLoading(false); - } - }; - - return ( - <> - -
-
- - - -
-
-
- - -
- -
- - -
-
-
- - ); -} - interface SizeRangeSliderProps { label: string; help?: string; diff --git a/packages/frontend/src/components/menu/formatter.tsx b/packages/frontend/src/components/menu/formatter.tsx index 05912792..2a409cc6 100644 --- a/packages/frontend/src/components/menu/formatter.tsx +++ b/packages/frontend/src/components/menu/formatter.tsx @@ -20,6 +20,10 @@ import { CopyIcon } from 'lucide-react'; import { toast } from 'sonner'; import { NumberInput } from '../ui/number-input'; import { PageControls } from '../shared/page-controls'; +import { Tooltip } from '../ui/tooltip'; +import { FaFileImport, FaFileExport } from 'react-icons/fa'; +import { IconButton } from '../ui/button'; +import { ImportModal } from '../shared/import-modal'; const formatterChoices = Object.values(constants.FORMATTER_DETAILS); // Remove the throttle utility and replace with FormatQueue @@ -95,6 +99,7 @@ function FormatterPreviewBox({ function Content() { const { userData, setUserData } = useUserData(); + const importModalDisclosure = useDisclosure(false); const [formattedStream, setFormattedStream] = useState<{ name: string; @@ -102,6 +107,38 @@ function Content() { } | null>(null); const [isFormatting, setIsFormatting] = useState(false); + const handleImport = (data: any) => { + if (typeof data.name === 'string' && typeof data.description === 'string') { + handleFormatterChange( + constants.CUSTOM_FORMATTER, + data.name, + data.description + ); + toast.success('Formatter imported successfully'); + } else { + toast.error('Invalid formatter format'); + } + }; + + const handleExport = () => { + const data = { + name: userData.formatter.definition?.name || '', + description: userData.formatter.definition?.description || '', + }; + const blob = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'custom-formatter.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success('Formatter exported successfully'); + }; + // Create format queue ref to persist between renders const formatQueueRef = React.useRef(new FormatQueue(200)); @@ -345,7 +382,37 @@ function Content() { placeholder="Enter a template for the stream description" /> - +
+ +
+ } + onClick={importModalDisclosure.open} + /> + } + > + Import + + } + onClick={handleExport} + /> + } + > + Export + +
+
)} @@ -502,6 +569,12 @@ function Content() { + + ); } diff --git a/packages/frontend/src/components/shared/import-modal.tsx b/packages/frontend/src/components/shared/import-modal.tsx new file mode 100644 index 00000000..bdb68fe8 --- /dev/null +++ b/packages/frontend/src/components/shared/import-modal.tsx @@ -0,0 +1,129 @@ +'use client'; +import { useRef, useState } from 'react'; +import { Modal } from '../ui/modal'; +import { Button } from '../ui/button'; +import { TextInput } from '../ui/text-input'; +import { useDisclosure } from '@/hooks/disclosure'; +import { toast } from 'sonner'; + +export interface ImportModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onImport: (data: any) => void; +} + +export function ImportModal({ + open, + onOpenChange, + onImport, +}: ImportModalProps) { + const [isLoading, setIsLoading] = useState(false); + const fileInputRef = useRef(null); + const urlModalDisclosure = useDisclosure(false); + const [url, setUrl] = useState(''); + + const handleFileImport = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target?.result as string; + const data = JSON.parse(content); + onImport(data); + onOpenChange(false); + } catch (error) { + console.error('Error importing file:', error); + toast.error('Failed to parse JSON file'); + } + }; + reader.readAsText(file); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleUrlImport = async () => { + if (!url) { + toast.error('Please enter a URL'); + return; + } + + setIsLoading(true); + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error('Failed to fetch URL'); + } + const data = await response.json(); + onImport(data); + urlModalDisclosure.close(); + onOpenChange(false); + } catch (error) { + console.error('Error importing from URL:', error); + toast.error('Failed to import from URL'); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + +
+
+ + + +
+
+
+ + +
+ +
+ + +
+
+
+ + ); +}