refactoring configuration modal

This commit is contained in:
2026-03-21 12:58:21 -03:00
parent b1957f2c19
commit 7efdd17534
7 changed files with 843 additions and 710 deletions

View File

@@ -0,0 +1,69 @@
import React from 'react';
import Dropdown from '../Dropdown';
import ToggleSwitch from '../ToggleSwitch';
import { Config } from '../../types';
interface ClockTabProps {
config: Config;
onChange: (updates: Partial<Config>) => void;
}
const ClockTab: React.FC<ClockTabProps> = ({ config, onChange }) => {
const updateClock = (updates: Partial<Config['clock']>) => {
onChange({ clock: { ...config.clock, ...updates } });
};
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Enable Clock</label>
<ToggleSwitch
checked={config.clock.enabled}
onChange={(checked) => updateClock({ enabled: checked })}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Clock Size</label>
<Dropdown
name="clock.size"
value={config.clock.size}
onChange={(e) => updateClock({ size: e.target.value as string })}
options={[
{ value: 'tiny', label: 'Tiny' },
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
]}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Clock Font</label>
<Dropdown
name="clock.font"
value={config.clock.font}
onChange={(e) => updateClock({ font: e.target.value as string })}
options={[
{ value: 'Helvetica', label: 'Helvetica' },
{ value: `'Orbitron', sans-serif`, label: 'Orbitron' },
{ value: 'monospace', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
]}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Time Format</label>
<Dropdown
name="clock.format"
value={config.clock.format}
onChange={(e) => updateClock({ format: e.target.value as string })}
options={[
{ value: 'h:mm A', label: 'AM/PM' },
{ value: 'HH:mm', label: '24:00' },
]}
/>
</div>
</div>
);
};
export default ClockTab;

View File

@@ -0,0 +1,79 @@
import React from 'react';
import Dropdown from '../Dropdown';
import { Config } from '../../types';
interface GeneralTabProps {
config: Config;
onChange: (updates: Partial<Config>) => void;
}
const GeneralTab: React.FC<GeneralTabProps> = ({ config, onChange }) => {
return (
<div className="flex flex-col gap-6">
<div>
<label className="text-slate-300 text-sm font-semibold mb-2 block">Title</label>
<input
type="text"
value={config.title}
onChange={(e) => onChange({ title: e.target.value })}
className="bg-white/10 p-3 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Title Size</label>
<Dropdown
name="titleSize"
value={config.titleSize}
onChange={(e) => onChange({ titleSize: e.target.value as string })}
options={[
{ value: 'tiny', label: 'Tiny' },
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
]}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Vertical Alignment</label>
<Dropdown
name="alignment"
value={config.alignment}
onChange={(e) => onChange({ alignment: e.target.value as string })}
options={[
{ value: 'top', label: 'Top' },
{ value: 'middle', label: 'Middle' },
{ value: 'bottom', label: 'Bottom' },
]}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Tile Size</label>
<Dropdown
name="tileSize"
value={config.tileSize || 'medium'}
onChange={(e) => onChange({ tileSize: e.target.value as string })}
options={[
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
]}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Horizontal Alignment</label>
<Dropdown
name="horizontalAlignment"
value={config.horizontalAlignment}
onChange={(e) => onChange({ horizontalAlignment: e.target.value as string })}
options={[
{ value: 'left', label: 'Left' },
{ value: 'middle', label: 'Middle' },
{ value: 'right', label: 'Right' },
]}
/>
</div>
</div>
);
};
export default GeneralTab;

View File

@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
import ToggleSwitch from '../ToggleSwitch';
import { Config, Server } from '../../types';
interface ServerWidgetTabProps {
config: Config;
onChange: (updates: Partial<Config>) => void;
}
const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) => {
const [newServerName, setNewServerName] = useState('');
const [newServerAddress, setNewServerAddress] = useState('');
const updateServerWidget = (updates: Partial<Config['serverWidget']>) => {
onChange({ serverWidget: { ...config.serverWidget, ...updates } });
};
const handleAddServer = () => {
if (newServerName.trim() === '' || newServerAddress.trim() === '') return;
const newServer: Server = {
id: Date.now().toString(),
name: newServerName,
address: newServerAddress,
};
updateServerWidget({ servers: [...config.serverWidget.servers, newServer] });
setNewServerName('');
setNewServerAddress('');
};
const handleRemoveServer = (id: string) => {
updateServerWidget({
servers: config.serverWidget.servers.filter((s) => s.id !== id),
});
};
const onDragEnd = (result: any) => {
if (!result.destination) return;
const items = Array.from(config.serverWidget.servers);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
updateServerWidget({ servers: items });
};
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Enable Server Widget</label>
<ToggleSwitch
checked={config.serverWidget.enabled}
onChange={(checked) => updateServerWidget({ enabled: checked })}
/>
</div>
{config.serverWidget.enabled && (
<>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Ping Frequency</label>
<div className="flex items-center gap-4">
<input
type="range"
min="5"
max="60"
value={config.serverWidget.pingFrequency}
onChange={(e) => updateServerWidget({ pingFrequency: Number(e.target.value) })}
className="w-48"
/>
<span>{config.serverWidget.pingFrequency}s</span>
</div>
</div>
<div>
<h3 className="text-slate-300 text-sm font-semibold mb-2">Servers</h3>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="servers">
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className="flex flex-col gap-2"
>
{config.serverWidget.servers.map((server: Server, index: number) => (
<Draggable key={server.id} draggableId={server.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="flex items-center justify-between bg-white/10 p-2 rounded-lg"
>
<div>
<p className="font-semibold">{server.name}</p>
<p className="text-sm text-slate-400">{server.address}</p>
</div>
<button
onClick={() => handleRemoveServer(server.id)}
className="text-red-500 hover:text-red-400"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-trash"
viewBox="0 0 16 16"
>
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
<path
fillRule="evenodd"
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"
/>
</svg>
</button>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
<div className="flex gap-2 mt-2">
<input
type="text"
placeholder="Server Name"
value={newServerName}
onChange={(e) => setNewServerName(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
<input
type="text"
placeholder="HTTP Address"
value={newServerAddress}
onChange={(e) => setNewServerAddress(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
<button
onClick={handleAddServer}
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
>
Add
</button>
</div>
</div>
</>
)}
</div>
);
};
export default ServerWidgetTab;

View File

@@ -0,0 +1,228 @@
import React, { useRef, useState } from 'react';
import Dropdown from '../Dropdown';
import { Config, Wallpaper } from '../../types';
interface ThemeTabProps {
config: Config;
onChange: (updates: Partial<Config>) => void;
userWallpapers: Wallpaper[];
allWallpapers: Wallpaper[];
chromeStorageAvailable: boolean;
onAddWallpaper: (name: string, url: string) => Promise<void>;
onAddWallpaperFile: (file: File) => Promise<void>;
onDeleteWallpaper: (wallpaper: Wallpaper) => Promise<void>;
}
const ThemeTab: React.FC<ThemeTabProps> = ({
config,
onChange,
userWallpapers,
allWallpapers,
chromeStorageAvailable,
onAddWallpaper,
onAddWallpaperFile,
onDeleteWallpaper,
}) => {
const [newWallpaperName, setNewWallpaperName] = useState('');
const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
const handleAddWallpaper = async () => {
if (newWallpaperUrl.trim() === '') return;
try {
await onAddWallpaper(newWallpaperName, newWallpaperUrl);
setNewWallpaperName('');
setNewWallpaperUrl('');
} catch (error) {
alert('Error adding wallpaper. Please check the URL and try again.');
console.error(error);
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
await onAddWallpaperFile(file);
} catch (error: any) {
alert(error?.message || 'Error adding wallpaper. Please try again.');
console.error(error);
}
e.target.value = '';
};
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Background</label>
<Dropdown
name="currentWallpapers"
value={config.currentWallpapers}
onChange={(e) => onChange({ currentWallpapers: e.target.value as string[] })}
multiple
options={allWallpapers.map((w) => ({ value: w.name, label: w.name }))}
/>
</div>
{Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
<Dropdown
name="wallpaperFrequency"
value={config.wallpaperFrequency}
onChange={(e) => onChange({ wallpaperFrequency: e.target.value as string })}
options={[
{ value: '1h', label: '1 hour' },
{ value: '3h', label: '3 hours' },
{ value: '6h', label: '6 hours' },
{ value: '12h', label: '12 hours' },
{ value: '1d', label: '1 day' },
{ value: '2d', label: '2 days' },
]}
/>
</div>
)}
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Wallpaper Blur</label>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="50"
value={config.wallpaperBlur}
onChange={(e) => onChange({ wallpaperBlur: Number(e.target.value) })}
className="w-48"
/>
<span>{config.wallpaperBlur}px</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Wallpaper Brightness</label>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="200"
value={config.wallpaperBrightness}
onChange={(e) => onChange({ wallpaperBrightness: Number(e.target.value) })}
className="w-48"
/>
<span>{config.wallpaperBrightness}%</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-300 text-sm font-semibold">Wallpaper Opacity</label>
<div className="flex items-center gap-4">
<input
type="range"
min="1"
max="100"
value={config.wallpaperOpacity}
onChange={(e) => onChange({ wallpaperOpacity: Number(e.target.value) })}
className="w-48"
/>
<span>{config.wallpaperOpacity}%</span>
</div>
</div>
{chromeStorageAvailable && (
<>
<div>
<h3 className="text-slate-300 text-sm font-semibold mb-2">User Wallpapers</h3>
<div className="flex flex-col gap-2">
{userWallpapers.map((wallpaper) => (
<div
key={wallpaper.name}
className="flex items-center justify-between bg-white/10 p-2 rounded-lg"
>
<span className="truncate">{wallpaper.name}</span>
<button
onClick={() => onDeleteWallpaper(wallpaper)}
className="text-red-500 hover:text-red-400"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-trash"
viewBox="0 0 16 16"
>
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
<path
fillRule="evenodd"
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"
/>
</svg>
</button>
</div>
))}
</div>
</div>
<div>
<h3 className="text-slate-300 text-sm font-semibold mb-2">Add New Wallpaper</h3>
<div className="flex flex-col gap-2">
<input
type="text"
placeholder="Wallpaper Name (optional for URLs)"
value={newWallpaperName}
onChange={(e) => setNewWallpaperName(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
<div className="flex gap-2">
<input
type="text"
placeholder="Image URL"
value={newWallpaperUrl}
onChange={(e) => setNewWallpaperUrl(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
<button
onClick={handleAddWallpaper}
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
>
Add
</button>
</div>
<div className="flex items-center justify-center w-full">
<label
htmlFor="file-upload"
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-white/5 border-white/20 hover:bg-white/10"
>
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<svg
className="w-8 h-8 mb-4 text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 16"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
/>
</svg>
<p className="mb-2 text-sm text-gray-400">
<span className="font-semibold">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-gray-400">PNG, JPG, WEBP, etc.</p>
</div>
<input
id="file-upload"
type="file"
className="hidden"
onChange={handleFileUpload}
ref={fileInputRef}
/>
</label>
</div>
</div>
</div>
</>
)}
</div>
);
};
export default ThemeTab;