feat(frontend): implement formatter import/export functionality with refactored ImportModal component

This commit is contained in:
Viren070
2025-09-19 18:58:46 +01:00
parent 7860546894
commit edbcc272e7
3 changed files with 204 additions and 121 deletions
@@ -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<HTMLInputElement>(null);
const urlModalDisclosure = useDisclosure(false);
const [url, setUrl] = useState('');
const handleFileImport = (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<>
<Modal open={open} onOpenChange={onOpenChange} title="Import">
<div className="space-y-4">
<div className="flex flex-col gap-4">
<input
type="file"
ref={fileInputRef}
onChange={handleFileImport}
accept=".json"
className="hidden"
/>
<Button
intent="primary"
onClick={() => fileInputRef.current?.click()}
className="w-full"
>
Import from File
</Button>
<Button
intent="primary"
onClick={urlModalDisclosure.open}
className="w-full"
>
Import from URL
</Button>
</div>
</div>
</Modal>
<Modal
open={urlModalDisclosure.isOpen}
onOpenChange={urlModalDisclosure.close}
title="Import from URL"
>
<div className="space-y-4">
<TextInput
label="URL"
value={url}
onValueChange={setUrl}
placeholder="Enter URL to JSON file"
/>
<div className="flex justify-end gap-2">
<Button intent="primary-outline" onClick={urlModalDisclosure.close}>
Cancel
</Button>
<Button
intent="primary"
onClick={handleUrlImport}
loading={isLoading}
>
Import
</Button>
</div>
</div>
</Modal>
</>
);
}
interface SizeRangeSliderProps {
label: string;
help?: string;
@@ -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<FormatQueue>(new FormatQueue(200));
@@ -345,7 +382,37 @@ function Content() {
placeholder="Enter a template for the stream description"
/>
</div>
<SnippetsButton />
<div className="flex gap-2 items-center">
<SnippetsButton />
<div className="ml-auto flex gap-2">
<Tooltip
trigger={
<IconButton
rounded
size="sm"
intent="primary-subtle"
icon={<FaFileImport />}
onClick={importModalDisclosure.open}
/>
}
>
Import
</Tooltip>
<Tooltip
trigger={
<IconButton
rounded
size="sm"
intent="primary-subtle"
icon={<FaFileExport />}
onClick={handleExport}
/>
}
>
Export
</Tooltip>
</div>
</div>
</div>
</SettingsCard>
)}
@@ -502,6 +569,12 @@ function Content() {
</div>
</div>
</SettingsCard>
<ImportModal
open={importModalDisclosure.isOpen}
onOpenChange={importModalDisclosure.toggle}
onImport={handleImport}
/>
</>
);
}
@@ -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<HTMLInputElement>(null);
const urlModalDisclosure = useDisclosure(false);
const [url, setUrl] = useState('');
const handleFileImport = (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<>
<Modal open={open} onOpenChange={onOpenChange} title="Import">
<div className="space-y-4">
<div className="flex flex-col gap-4">
<input
type="file"
ref={fileInputRef}
onChange={handleFileImport}
accept=".json"
className="hidden"
/>
<Button
intent="primary"
onClick={() => fileInputRef.current?.click()}
className="w-full"
>
Import from File
</Button>
<Button
intent="primary"
onClick={urlModalDisclosure.open}
className="w-full"
>
Import from URL
</Button>
</div>
</div>
</Modal>
<Modal
open={urlModalDisclosure.isOpen}
onOpenChange={urlModalDisclosure.close}
title="Import from URL"
>
<div className="space-y-4">
<TextInput
label="URL"
value={url}
onValueChange={setUrl}
placeholder="Enter URL to JSON file"
/>
<div className="flex justify-end gap-2">
<Button intent="primary-outline" onClick={urlModalDisclosure.close}>
Cancel
</Button>
<Button
intent="primary"
onClick={handleUrlImport}
loading={isLoading}
>
Import
</Button>
</div>
</div>
</Modal>
</>
);
}