commit d30fd8d30b0676cdf0312d8b99876a5b6a424e27 Author: José Henrique Date: Fri Jul 18 22:07:22 2025 -0300 init diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100755 index 0000000..8911865 --- /dev/null +++ b/App.tsx @@ -0,0 +1,410 @@ +import React, { useState, useEffect } from 'react'; +import WebsiteTile from './components/WebsiteTile'; +import ConfigurationModal from './components/ConfigurationModal'; +import Clock from './components/Clock'; +import ServerWidget from './components/ServerWidget'; +import { DEFAULT_CATEGORIES } from './constants'; +import { Category, Website, Wallpaper } from './types'; +import Dropdown from './components/Dropdown'; +import WebsiteEditModal from './components/WebsiteEditModal'; +import CategoryEditModal from './components/CategoryEditModal'; +import { PlusCircle, Pencil } from 'lucide-react'; +import { baseWallpapers } from './components/utils/baseWallpapers'; + + +const defaultConfig = { + title: 'Vision Start', + subtitle: 'Your personal portal to the web.', + backgroundUrl: '/waves.jpg', + wallpaperBlur: 0, + wallpaperBrightness: 100, + wallpaperOpacity: 100, + titleSize: 'medium', + subtitleSize: 'medium', + alignment: 'middle', + clock: { + enabled: true, + size: 'medium', + font: 'Helvetica', + format: 'h:mm A', + }, + serverWidget: { + enabled: false, + pingFrequency: 15, + servers: [], + }, +}; + +const App: React.FC = () => { + const [categories, setCategories] = useState(() => { + try { + const storedCategories = localStorage.getItem('categories'); + if (storedCategories) { + return JSON.parse(storedCategories); + } + } catch (error) { + console.error('Error parsing categories from localStorage', error); + } + return DEFAULT_CATEGORIES; + }); + const [isEditing, setIsEditing] = useState(false); + const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); + const [editingWebsite, setEditingWebsite] = useState(null); + const [addingWebsite, setAddingWebsite] = useState(null); + const [editingCategory, setEditingCategory] = useState(null); + const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false); + const [config, setConfig] = useState(() => { + try { + const storedConfig = localStorage.getItem('config'); + if (storedConfig) { + return { ...defaultConfig, ...JSON.parse(storedConfig) }; + } + } catch (error) { + console.error('Error parsing config from localStorage', error); + } + return { ...defaultConfig }; + }); + const [userWallpapers, setUserWallpapers] = useState(() => { + const storedUserWallpapers = localStorage.getItem('userWallpapers'); + return storedUserWallpapers ? JSON.parse(storedUserWallpapers) : []; + }); + + const allWallpapers = [...baseWallpapers, ...userWallpapers]; + const selectedWallpaper = allWallpapers.find(w => w.url === config.backgroundUrl || w.base64 === config.backgroundUrl); + + useEffect(() => { + localStorage.setItem('categories', JSON.stringify(categories)); + localStorage.setItem('config', JSON.stringify(config)); + }, [categories, config]); + + const handleSaveConfig = (newConfig: any) => { + setConfig(newConfig); + setIsConfigModalOpen(false); + }; + + const handleSaveWebsite = (website: Partial) => { + if (editingWebsite) { + const newCategories = categories.map(category => ({ + ...category, + websites: category.websites.map(w => + w.id === website.id ? { ...w, ...website } : w + ), + })); + setCategories(newCategories); + setEditingWebsite(null); + } else if (addingWebsite) { + const newWebsite: Website = { + id: Date.now().toString(), + name: website.name || '', + url: website.url || '', + icon: website.icon || '', + categoryId: addingWebsite.id, + }; + const newCategories = categories.map(category => + category.id === addingWebsite.id + ? { ...category, websites: [...category.websites, newWebsite] } + : category + ); + setCategories(newCategories); + setAddingWebsite(null); + } + }; + + const handleSaveCategory = (name: string) => { + if (editingCategory) { + const newCategories = categories.map(category => + category.id === editingCategory.id ? { ...category, name } : category + ); + setCategories(newCategories); + } else { + const newCategory: Category = { + id: Date.now().toString(), + name, + websites: [], + }; + setCategories([...categories, newCategory]); + } + setEditingCategory(null); + setIsCategoryModalOpen(false); + }; + + const handleDeleteWebsite = () => { + if (!editingWebsite) return; + + const newCategories = categories.map(category => ({ + ...category, + websites: category.websites.filter(w => w.id !== editingWebsite.id), + })); + setCategories(newCategories); + setEditingWebsite(null); + }; + + const handleDeleteCategory = () => { + if (!editingCategory) return; + + const newCategories = categories.filter(c => c.id !== editingCategory.id); + setCategories(newCategories); + setEditingCategory(null); + setIsCategoryModalOpen(false); + }; + + const handleMoveWebsite = (website: Website, direction: 'left' | 'right') => { + const categoryIndex = categories.findIndex(c => c.id === website.categoryId); + if (categoryIndex === -1) return; + + const category = categories[categoryIndex]; + const websiteIndex = category.websites.findIndex(w => w.id === website.id); + if (websiteIndex === -1) return; + + const newCategories = [...categories]; + const newWebsites = [...category.websites]; + const [movedWebsite] = newWebsites.splice(websiteIndex, 1); + + if (direction === 'left') { + const newCategoryIndex = (categoryIndex - 1 + categories.length) % categories.length; + newCategories[categoryIndex] = { ...category, websites: newWebsites }; + const destCategory = newCategories[newCategoryIndex]; + const destWebsites = [...destCategory.websites, { ...movedWebsite, categoryId: destCategory.id }]; + newCategories[newCategoryIndex] = { ...destCategory, websites: destWebsites }; + } else { + const newCategoryIndex = (categoryIndex + 1) % categories.length; + newCategories[categoryIndex] = { ...category, websites: newWebsites }; + const destCategory = newCategories[newCategoryIndex]; + const destWebsites = [...destCategory.websites, { ...movedWebsite, categoryId: destCategory.id }]; + newCategories[newCategoryIndex] = { ...destCategory, websites: destWebsites }; + } + + setCategories(newCategories); + }; + + const getAlignmentClass = (alignment: string) => { + switch (alignment) { + case 'top': + return 'justify-start'; + case 'middle': + return 'justify-center'; + case 'bottom': + return 'justify-end'; + default: + return 'justify-center'; + } + }; + + const getClockSizeClass = (size: string) => { + switch (size) { + case 'tiny': + return 'text-3xl'; + case 'small': + return 'text-4xl'; + case 'medium': + return 'text-5xl'; + case 'large': + return 'text-6xl'; + default: + return 'text-5xl'; + } + }; + + const getTitleSizeClass = (size: string) => { + switch (size) { + case 'tiny': + return 'text-4xl'; + case 'small': + return 'text-5xl'; + case 'medium': + return 'text-6xl'; + case 'large': + return 'text-7xl'; + default: + return 'text-6xl'; + } + }; + + const getSubtitleSizeClass = (size: string) => { + switch (size) { + case 'tiny': + return 'text-lg'; + case 'small': + return 'text-xl'; + case 'medium': + return 'text-2xl'; + case 'large': + return 'text-3xl'; + default: + return 'text-2xl'; + } + }; + + const getTileSizeClass = (size: string) => { + switch (size) { + case 'small': + return 'w-28 h-28'; + case 'medium': + return 'w-32 h-32'; + case 'large': + return 'w-36 h-36'; + default: + return 'w-32 h-32'; + } + }; + + return ( + +
+
+
+ +
+
+ +
+ + {/* Absolute top-center Clock */} + {config.clock.enabled && ( +
+ +
+ )} + +
+ {config.title || config.subtitle && + ( +
+

+ {config.title} +

+

+ {config.subtitle} +

+
+ )} +
+ +
+ {categories.map((category) => ( +
+
+

{category.name}

+ {isEditing && ( + + )} +
+
+ {category.websites.map((website) => ( + + ))} + {isEditing && ( + + )} +
+
+ ))} + {isEditing && ( +
+ +
+ )} +
+ + {config.serverWidget.enabled && ( +
+ +
+ )} + + {(editingWebsite || addingWebsite) && ( + { + setEditingWebsite(null); + setAddingWebsite(null); + }} + onSave={handleSaveWebsite} + onDelete={handleDeleteWebsite} + /> + )} + + {isCategoryModalOpen && ( + { + setEditingCategory(null); + setIsCategoryModalOpen(false); + }} + onSave={handleSaveCategory} + onDelete={handleDeleteCategory} + /> + )} + + {isConfigModalOpen && ( + setIsConfigModalOpen(false)} + onSave={handleSaveConfig} + /> + )} +
+ ); +} + +export default App; diff --git a/README.md b/README.md new file mode 100755 index 0000000..bafbc1c --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Vision Start +#### Small startpage + +## Predefined themes + +1. Abstract +2. Aurora (Vista vibes) +3. Mountain + +## Run Locally + +**Prerequisites:** Node.js + +1. Install dependencies: + `npm install` +2. Run the app: + `npm run dev` diff --git a/components/CategoryEditModal.tsx b/components/CategoryEditModal.tsx new file mode 100644 index 0000000..eb90b69 --- /dev/null +++ b/components/CategoryEditModal.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { Category } from '../types'; + +interface CategoryEditModalProps { + category?: Category; + edit: boolean; + onClose: () => void; + onSave: (name: string) => void; + onDelete: () => void; +} + +const CategoryEditModal: React.FC = ({ category, edit, onClose, onSave, onDelete }) => { + const [name, setName] = useState(category ? category.name : ''); + + const handleSave = () => { + onSave(name); + }; + + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + return ( +
+
+

{edit ? 'Edit Category' : 'Add Category'}

+
+ setName(e.target.value)} + className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> +
+
+
+ {edit && ( + + )} +
+
+ + +
+
+
+
+ ); +}; + +export default CategoryEditModal; diff --git a/components/Clock.tsx b/components/Clock.tsx new file mode 100644 index 0000000..339bdeb --- /dev/null +++ b/components/Clock.tsx @@ -0,0 +1,53 @@ +import React, { useState, useEffect } from 'react'; + +interface ClockProps { + config: { + clock: { + enabled: boolean; + size: string; + font: string; + format: string; + }; + }; + getClockSizeClass: (size: string) => string; +} + +const Clock: React.FC = ({ config, getClockSizeClass }) => { + const [time, setTime] = useState(new Date()); + + useEffect(() => { + const timerId = setInterval(() => setTime(new Date()), 1000); + return () => clearInterval(timerId); + }, []); + + if (!config.clock.enabled) { + return null; + } + + const formatTime = (date: Date) => { + const hours = date.getHours(); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + if (config.clock.format === 'HH:mm') { + return `${hours.toString().padStart(2, '0')}:${minutes}`; + } + + const ampm = hours >= 12 ? 'PM' : 'AM'; + const formattedHours = (hours % 12 || 12).toString(); + return `${formattedHours}:${minutes} ${ampm}`; + }; + + return ( +
+ {formatTime(time)} +
+ ); +}; + +export default Clock; diff --git a/components/ConfigurationModal.tsx b/components/ConfigurationModal.tsx new file mode 100644 index 0000000..97a9a83 --- /dev/null +++ b/components/ConfigurationModal.tsx @@ -0,0 +1,611 @@ + +import React, { useState, useRef, useEffect } from 'react'; +import ToggleSwitch from './ToggleSwitch'; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; +import { Server, Wallpaper } from '../types'; +import { Trash } from 'lucide-react'; +import Dropdown from './Dropdown'; +import { baseWallpapers } from './utils/baseWallpapers'; + +interface ConfigurationModalProps { + onClose: () => void; + onSave: (config: any) => void; + currentConfig: any; +} + +const ConfigurationModal: React.FC = ({ onClose, onSave, currentConfig }) => { + const [config, setConfig] = useState({ + ...currentConfig, + titleSize: currentConfig.titleSize || 'medium', + subtitleSize: currentConfig.subtitleSize || 'medium', + alignment: currentConfig.alignment || 'middle', + tileSize: currentConfig.tileSize || 'medium', + wallpaperBlur: currentConfig.wallpaperBlur || 0, + wallpaperBrightness: currentConfig.wallpaperBrightness || 100, + wallpaperOpacity: currentConfig.wallpaperOpacity || 100, + serverWidget: { + enabled: false, + pingFrequency: 15, + servers: [], + ...currentConfig.serverWidget, + }, + clock: { + enabled: true, + size: 'medium', + font: 'Helvetica', + format: 'h:mm A', + ...currentConfig.clock, + }, + }); + const [activeTab, setActiveTab] = useState('general'); + const [newServerName, setNewServerName] = useState(''); + const [newServerAddress, setNewServerAddress] = useState(''); + const [newWallpaperName, setNewWallpaperName] = useState(''); + const [newWallpaperUrl, setNewWallpaperUrl] = useState(''); + const [userWallpapers, setUserWallpapers] = useState([]); + const menuRef = useRef(null); + const fileInputRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const storedUserWallpapers = localStorage.getItem('userWallpapers'); + if (storedUserWallpapers) { + setUserWallpapers(JSON.parse(storedUserWallpapers)); + } + }, []); + + useEffect(() => { + // A small timeout to allow the component to mount before starting the transition + const timer = setTimeout(() => { + setIsVisible(true); + }, 10); + return () => clearTimeout(timer); + }, []); + + const handleClose = () => { + setIsVisible(false); + setTimeout(() => { + onClose(); + }, 300); // This duration should match the transition duration + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + if (name.startsWith('serverWidget.')) { + const field = name.split('.')[1]; + setConfig({ + ...config, + serverWidget: { ...config.serverWidget, [field]: value }, + }); + } else if (name.startsWith('clock.')) { + const field = name.split('.')[1]; + setConfig({ + ...config, + clock: { ...config.clock, [field]: value }, + }); + } else { + setConfig({ ...config, [name]: value }); + } + }; + + const handleClockToggleChange = (checked: boolean) => { + setConfig({ ...config, clock: { ...config.clock, enabled: checked } }); + }; + + const handleServerWidgetToggleChange = (checked: boolean) => { + setConfig({ + ...config, + serverWidget: { ...config.serverWidget, enabled: checked }, + }); + }; + + const handleAddServer = () => { + if (newServerName.trim() === '' || newServerAddress.trim() === '') return; + + const newServer: Server = { + id: Date.now().toString(), + name: newServerName, + address: newServerAddress, + }; + + setConfig({ + ...config, + serverWidget: { + ...config.serverWidget, + servers: [...config.serverWidget.servers, newServer], + }, + }); + + setNewServerName(''); + setNewServerAddress(''); + }; + + const handleRemoveServer = (id: string) => { + setConfig({ + ...config, + serverWidget: { + ...config.serverWidget, + servers: config.serverWidget.servers.filter((server: Server) => server.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); + + setConfig({ + ...config, + serverWidget: { + ...config.serverWidget, + servers: items, + }, + }); + }; + + const handleAddWallpaper = () => { + if (newWallpaperName.trim() === '' || newWallpaperUrl.trim() === '') return; + + const newWallpaper: Wallpaper = { + name: newWallpaperName, + url: newWallpaperUrl, + }; + + const updatedUserWallpapers = [...userWallpapers, newWallpaper]; + setUserWallpapers(updatedUserWallpapers); + localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers)); + setConfig({ ...config, backgroundUrl: newWallpaperUrl }); + + setNewWallpaperName(''); + setNewWallpaperUrl(''); + }; + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (file.size > 4 * 1024 * 1024) { + alert('File size exceeds 4MB. Please choose a smaller file.'); + return; + } + + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result as string; + if (base64.length > 4.5 * 1024 * 1024) { + alert('The uploaded image is too large. Please choose a smaller file.'); + return; + } + + const updatedUserWallpapers = userWallpapers.filter(w => !w.base64); + const newWallpaper: Wallpaper = { + name: file.name, + base64, + }; + setUserWallpapers([...updatedUserWallpapers, newWallpaper]); + localStorage.setItem('userWallpapers', JSON.stringify([...updatedUserWallpapers, newWallpaper])); + setConfig({ ...config, backgroundUrl: base64 }); + }; + reader.readAsDataURL(file); + } + }; + + const handleDeleteWallpaper = (wallpaper: Wallpaper) => { + const updatedUserWallpapers = userWallpapers.filter(w => w.name !== wallpaper.name); + setUserWallpapers(updatedUserWallpapers); + localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers)); + + if (config.backgroundUrl === (wallpaper.url || wallpaper.base64)) { + const nextWallpaper = baseWallpapers[0] || updatedUserWallpapers[0]; + if (nextWallpaper) { + setConfig({ ...config, backgroundUrl: nextWallpaper.url || nextWallpaper.base64 }); + } + } + }; + + const allWallpapers = [...baseWallpapers, ...userWallpapers]; + + return ( +
+
+ +
+
+

Configuration

+ +
+ + + + +
+ + {activeTab === 'general' && ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ )} + + {activeTab === 'theme' && ( +
+
+ + ({ + value: w.url || w.base64 || '', + label: ( +
+ {w.name} + {!baseWallpapers.includes(w) && ( + + )} +
+ ) + }))} + /> +
+
+ +
+ + {config.wallpaperBlur}px +
+
+
+ +
+ + {config.wallpaperBrightness}% +
+
+
+ +
+ + {config.wallpaperOpacity}% +
+
+
+

Add New Wallpaper

+
+ setNewWallpaperName(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> +
+ setNewWallpaperUrl(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + +
+
+ +
+
+
+
+ )} + + {activeTab === 'clock' && ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ )} + + {activeTab === 'serverWidget' && ( +
+
+ + +
+ {config.serverWidget.enabled && ( + <> +
+ +
+ + {config.serverWidget.pingFrequency}s +
+
+
+

Servers

+ + + {(provided) => ( +
+ {config.serverWidget.servers.map((server: Server, index: number) => ( + + {(provided) => ( +
+
+

{server.name}

+

{server.address}

+
+ +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+
+ setNewServerName(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + setNewServerAddress(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + +
+
+ + )} +
+ )} +
+
+
+ + +
+
+
+
+ ); +}; + +export default ConfigurationModal; diff --git a/components/Dropdown.tsx b/components/Dropdown.tsx new file mode 100644 index 0000000..5342f8f --- /dev/null +++ b/components/Dropdown.tsx @@ -0,0 +1,94 @@ +import React, { useState, useRef, useEffect } from 'react'; + +interface DropdownProps { + options: { value: string; label: string }[]; + value: string; + onChange: (e: { target: { name: string; value: string } }) => void; + name?: string; +} + +const Dropdown: React.FC = ({ options, value, onChange, name, ...rest }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const selectedOptionLabel = options.find(option => option.value === value)?.label || ''; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const handleOptionClick = (optionValue: string) => { + const syntheticEvent = { + target: { + name: name || '', + value: optionValue, + }, + }; + onChange(syntheticEvent); + setIsOpen(false); + }; + + return ( +
+ + + {isOpen && ( +
    + {options.map((option) => ( +
  • handleOptionClick(option.value)} + className={`h-10 px-3 text-white cursor-pointer transition-all duration-150 ease-in-out flex items-center + ${option.value === value + ? 'bg-cyan-500/20 text-cyan-300' + : 'hover:bg-white/20 hover:text-white hover:shadow-lg' + }`} + role="option" + aria-selected={option.value === value} + > + {option.label} +
  • + ))} +
+ )} + + {/* Hidden input to mimic native select behavior for forms */} + {name && } +
+ ); +}; + +export default Dropdown; diff --git a/components/EditModal.tsx b/components/EditModal.tsx new file mode 100644 index 0000000..c736186 --- /dev/null +++ b/components/EditModal.tsx @@ -0,0 +1,255 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'; +import { getWebsiteIcon } from './utils/iconService'; +import { Category, Website } from '../types'; +import IconPicker from './IconPicker'; +import { icons } from 'lucide-react'; + +interface EditModalProps { + categories: Category[]; + onClose: () => void; + onSave: (categories: Category[]) => void; +} + +const EditModal: React.FC = ({ categories, onClose, onSave }) => { + const [localCategories, setLocalCategories] = useState(categories); + const [selectedCategoryId, setSelectedCategoryId] = useState(categories[0]?.id || null); + const [newCategoryName, setNewCategoryName] = useState(''); + const [newWebsite, setNewWebsite] = useState({ name: '', url: '', icon: '' }); + const [showIconPicker, setShowIconPicker] = useState(false); + const modalRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + onClose(); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [onClose]); + + const handleOnDragEnd = (result: DropResult) => { + if (!result.destination) return; + + const { source, destination } = result; + + if (source.droppableId === destination.droppableId) { + const category = localCategories.find(cat => cat.id === source.droppableId); + if (category) { + const items = Array.from(category.websites); + const [reorderedItem] = items.splice(source.index, 1); + items.splice(destination.index, 0, reorderedItem); + const updatedCategories = localCategories.map(cat => + cat.id === category.id ? { ...cat, websites: items } : cat + ); + setLocalCategories(updatedCategories); + } + } else { + const sourceCategory = localCategories.find(cat => cat.id === source.droppableId); + const destCategory = localCategories.find(cat => cat.id === destination.droppableId); + if (sourceCategory && destCategory) { + const sourceItems = Array.from(sourceCategory.websites); + const [movedItem] = sourceItems.splice(source.index, 1); + const destItems = Array.from(destCategory.websites); + destItems.splice(destination.index, 0, { ...movedItem, categoryId: destCategory.id }); + + const updatedCategories = localCategories.map(cat => { + if (cat.id === sourceCategory.id) return { ...cat, websites: sourceItems }; + if (cat.id === destCategory.id) return { ...cat, websites: destItems }; + return cat; + }); + setLocalCategories(updatedCategories); + } + } + }; + + const handleAddCategory = () => { + if (newCategoryName.trim() === '') return; + const newCategory: Category = { + id: Date.now().toString(), + name: newCategoryName, + websites: [], + }; + setLocalCategories([...localCategories, newCategory]); + setNewCategoryName(''); + }; + + const handleRemoveCategory = (id: string) => { + const updatedCategories = localCategories.filter(cat => cat.id !== id); + setLocalCategories(updatedCategories); + if (selectedCategoryId === id) { + setSelectedCategoryId(updatedCategories[0]?.id || null); + } + }; + + const handleAddWebsite = async () => { + if (!selectedCategoryId || !newWebsite.name || !newWebsite.url) return; + + let icon = newWebsite.icon; + if (!icon || !Object.keys(icons).includes(icon)) { + icon = await getWebsiteIcon(newWebsite.url); + } + + const newWebsiteData: Website = { + id: Date.now().toString(), + name: newWebsite.name, + url: newWebsite.url, + icon, + categoryId: selectedCategoryId, + }; + + const updatedCategories = localCategories.map(cat => { + if (cat.id === selectedCategoryId) { + return { ...cat, websites: [...cat.websites, newWebsiteData] }; + } + return cat; + }); + + setLocalCategories(updatedCategories); + setNewWebsite({ name: '', url: '', icon: '' }); + }; + + const handleRemoveWebsite = (categoryId: string, websiteId: string) => { + const updatedCategories = localCategories.map(cat => { + if (cat.id === categoryId) { + return { ...cat, websites: cat.websites.filter(web => web.id !== websiteId) }; + } + return cat; + }); + setLocalCategories(updatedCategories); + }; + + const selectedCategory = localCategories.find(cat => cat.id === selectedCategoryId); + + return ( +
+
+

Edit Bookmarks

+
+
+

Categories

+
+ {localCategories.map(category => ( +
setSelectedCategoryId(category.id)} + > + {category.name} + +
+ ))} +
+
+ setNewCategoryName(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + +
+
+
+

Websites

+ {selectedCategory && ( + + + {(provided) => ( +
    + {selectedCategory.websites.map((website, index) => ( + + {(provided) => ( +
  • +
    + {Object.keys(icons).includes(website.icon) ? ( + React.createElement(icons[website.icon as keyof typeof icons], { className: "h-8 w-8 mr-4" }) + ) : ( + {website.name} + )} + {website.name} +
    + +
  • + )} +
    + ))} + {provided.placeholder} +
+ )} +
+
+ )} +
+

Add New Bookmark

+
+ setNewWebsite({ ...newWebsite, name: e.target.value })} + className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + setNewWebsite({ ...newWebsite, url: e.target.value })} + className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> +
+ setNewWebsite({ ...newWebsite, icon: e.target.value })} + className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 w-full" + /> + +
+ {showIconPicker && ( + { + setNewWebsite({ ...newWebsite, icon: iconName }); + setShowIconPicker(false); + }} + /> + )} + +
+
+
+
+
+ + +
+
+
+ ); +}; + +export default EditModal; diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx new file mode 100644 index 0000000..d8e2d63 --- /dev/null +++ b/components/IconPicker.tsx @@ -0,0 +1,48 @@ +import React, { useState, useMemo } from 'react'; +import { icons } from 'lucide-react'; + +interface IconPickerProps { + onSelect: (iconName: string) => void; +} + +const IconPicker: React.FC = ({ onSelect }) => { + const [search, setSearch] = useState(''); + + const filteredIcons = useMemo(() => { + if (!search) { + return Object.keys(icons).slice(0, 50); + } + return Object.keys(icons).filter(name => + name.toLowerCase().includes(search.toLowerCase()) + ); + }, [search]); + + return ( +
+ setSearch(e.target.value)} + className="w-full p-2 mb-4 bg-gray-700 rounded text-white" + /> +
+ {filteredIcons.map(iconName => { + const LucideIcon = icons[iconName as keyof typeof icons]; + return ( +
onSelect(iconName)} + className="cursor-pointer flex flex-col items-center justify-center p-2 rounded-lg hover:bg-gray-700" + > + + {iconName} +
+ ); + })} +
+
+ ); +}; + +export default IconPicker; diff --git a/components/ServerWidget.tsx b/components/ServerWidget.tsx new file mode 100644 index 0000000..101ae1c --- /dev/null +++ b/components/ServerWidget.tsx @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from 'react'; +import { Server } from '../types'; +import ping from './utils/jsping.js'; + +interface ServerWidgetProps { + config: { + serverWidget: { + enabled: boolean; + pingFrequency: number; + servers: Server[]; + }; + }; +} + +const ServerWidget: React.FC = ({ config }) => { + const [serverStatus, setServerStatus] = useState>({}); + + useEffect(() => { + const pingServers = () => { + config.serverWidget.servers.forEach((server) => { + setServerStatus((prevStatus) => ({ ...prevStatus, [server.id]: 'pending' })); + ping(server.address) + .then(() => { + setServerStatus((prevStatus) => ({ ...prevStatus, [server.id]: 'online' })); + }) + .catch(() => { + setServerStatus((prevStatus) => ({ ...prevStatus, [server.id]: 'offline' })); + }); + }); + }; + + if (config.serverWidget.enabled) { + pingServers(); + const interval = setInterval(pingServers, config.serverWidget.pingFrequency * 1000); + return () => clearInterval(interval); + } + }, [config.serverWidget.enabled, config.serverWidget.servers, config.serverWidget.pingFrequency]); + + if (!config.serverWidget.enabled) { + return null; + } + + const getStatusColor = (status: string) => { + switch (status) { + case 'online': + return 'bg-green-500'; + case 'offline': + return 'bg-red-500'; + default: + return 'bg-gray-500'; + } + }; + + return ( +
+
+ {config.serverWidget.servers.map((server) => ( +
+
+ + {server.name} + +
+ ))} +
+
+ ); +}; + +export default ServerWidget; \ No newline at end of file diff --git a/components/ToggleSwitch.tsx b/components/ToggleSwitch.tsx new file mode 100644 index 0000000..5901303 --- /dev/null +++ b/components/ToggleSwitch.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +interface ToggleSwitchProps { + checked: boolean; + onChange: (checked: boolean) => void; +} + +const ToggleSwitch: React.FC = ({ checked, onChange }) => { + const handleToggle = () => { + onChange(!checked); + }; + + return ( +
+
+
+ ); +}; + +export default ToggleSwitch; diff --git a/components/WebsiteEditModal.tsx b/components/WebsiteEditModal.tsx new file mode 100644 index 0000000..29e9538 --- /dev/null +++ b/components/WebsiteEditModal.tsx @@ -0,0 +1,112 @@ +import React, { useState, useEffect } from 'react'; +import { Website } from '../types'; +import IconPicker from './IconPicker'; +import { getWebsiteIcon } from './utils/iconService'; +import { icons } from 'lucide-react'; + +interface WebsiteEditModalProps { + website?: Website; + edit: boolean; + onClose: () => void; + onSave: (website: Partial) => void; + onDelete: () => void; +} + +const WebsiteEditModal: React.FC = ({ website, edit, onClose, onSave, onDelete }) => { + const [name, setName] = useState(website ? website.name : ''); + const [url, setUrl] = useState(website ? website.url : ''); + const [icon, setIcon] = useState(website ? website.icon : ''); + const [showIconPicker, setShowIconPicker] = useState(false); + + useEffect(() => { + const fetchIcon = async () => { + if (url) { + const fetchedIcon = await getWebsiteIcon(url); + setIcon(fetchedIcon); + } + }; + fetchIcon(); + }, [url]); + + const handleSave = () => { + onSave({ id: website?.id, name, url, icon }); + }; + + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const LucideIcon = icons[icon as keyof typeof icons]; + + return ( +
+
+

{edit ? 'Edit Website' : 'Add Website'}

+
+
+ {LucideIcon ? ( + + ) : ( + Website Icon + )} +
+ setName(e.target.value)} + className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + setUrl(e.target.value)} + className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> +
+ setIcon(e.target.value)} + className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 w-full" + /> + +
+ {showIconPicker && ( + { + setIcon(iconName); + setShowIconPicker(false); + }} + /> + )} +
+
+
+ {edit && ( + + )} +
+
+ + +
+
+
+
+ ); +}; + +export default WebsiteEditModal; diff --git a/components/WebsiteTile.tsx b/components/WebsiteTile.tsx new file mode 100755 index 0000000..82e9c27 --- /dev/null +++ b/components/WebsiteTile.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Website } from '../types'; +import { icons, ArrowLeft, ArrowRight, Pencil } from 'lucide-react'; + +interface WebsiteTileProps { + website: Website; + isEditing: boolean; + onEdit: (website: Website) => void; + onMove: (website: Website, direction: 'left' | 'right') => void; + className?: string; +} + +const WebsiteTile: React.FC = ({ website, isEditing, onEdit, onMove, className }) => { + const LucideIcon = icons[website.icon as keyof typeof icons]; + + return ( +
+ +
+ {LucideIcon ? ( + + ) : ( + {`${website.name} + )} +
+ + {website.name} + +
+ {isEditing && ( +
+ + + +
+ )} +
+ ); +}; + +export default WebsiteTile; diff --git a/components/utils/baseWallpapers.ts b/components/utils/baseWallpapers.ts new file mode 100644 index 0000000..2427ed6 --- /dev/null +++ b/components/utils/baseWallpapers.ts @@ -0,0 +1,25 @@ + +import { Wallpaper } from '../../types'; + +export const baseWallpapers: Wallpaper[] = [ + { + name: 'Abstract', + url: 'https://i.imgur.com/C6ynAtX.jpeg' + }, + { + name: 'Abstract Red', + url: 'https://i.imgur.com/L89cqyP.jpeg' + }, + { + name: 'Beach', + url: 'https://wallpapershome.com/images/pages/pic_h/615.jpg' + }, + { + name: 'Mountain', + url: 'https://i.imgur.com/yHfOZUd.jpeg' + }, + { + name: 'Waves', + url: 'waves.jpg', + }, +]; diff --git a/components/utils/iconService.ts b/components/utils/iconService.ts new file mode 100644 index 0000000..8c1ec0c --- /dev/null +++ b/components/utils/iconService.ts @@ -0,0 +1,31 @@ + +async function getWebsiteIcon(url: string): Promise { + try { + const response = await fetch(url); + const html = await response.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + + const appleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]'); + if (appleTouchIcon) { + const href = appleTouchIcon.getAttribute('href'); + if (href) { + return new URL(href, url).href; + } + } + + const iconLink = doc.querySelector('link[rel="icon"][type="image/png"]') || doc.querySelector('link[rel="icon"]'); + if (iconLink) { + const href = iconLink.getAttribute('href'); + if (href) { + return new URL(href, url).href; + } + } + + } catch (error) { + console.error('Error fetching and parsing HTML for icon:', error); + } + + return `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=128`; +} + +export { getWebsiteIcon }; diff --git a/components/utils/jsping.js b/components/utils/jsping.js new file mode 100644 index 0000000..791ef7a --- /dev/null +++ b/components/utils/jsping.js @@ -0,0 +1,24 @@ +function request_image(url) { + return new Promise(function(resolve, reject) { + var img = new Image(); + img.onload = function() { resolve(img); }; + img.onerror = function() { reject(url); }; + img.src = url + '?random-no-cache=' + Math.floor((1 + Math.random()) * 0x10000).toString(16); + }); +} + +function ping(url, multiplier) { + return new Promise(function(resolve, reject) { + var start = (new Date()).getTime(); + var response = function() { + var delta = ((new Date()).getTime() - start); + delta *= (multiplier || 1); + resolve(delta); + }; + request_image(url).then(response).catch(response); + + setTimeout(function() { reject(Error('Timeout')); }, 5000); + }); +} + +export default ping; \ No newline at end of file diff --git a/constants.tsx b/constants.tsx new file mode 100755 index 0000000..1d547c9 --- /dev/null +++ b/constants.tsx @@ -0,0 +1,18 @@ + +import { Category } from './types'; + +export const DEFAULT_CATEGORIES: Category[] = [ + { + id: '1', + name: 'Search', + websites: [ + { + id: '1', + name: 'Google', + url: 'https://www.google.com', + icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=128', + categoryId: '1', + }, + ], + }, +]; diff --git a/index.html b/index.html new file mode 100755 index 0000000..8876d05 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + Vision Start + + + + + + + + +
+ + + \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100755 index 0000000..aaa0c6e --- /dev/null +++ b/index.tsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1bad32b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1210 @@ +{ + "name": "vision-start", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vision-start", + "version": "0.0.0", + "dependencies": { + "@hello-pangea/dnd": "^18.0.1", + "lucide-react": "^0.525.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@types/react": "^19.1.8", + "typescript": "~5.7.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", + "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", + "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", + "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", + "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", + "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", + "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", + "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", + "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", + "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", + "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", + "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", + "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", + "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", + "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", + "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", + "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", + "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", + "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", + "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", + "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.3.tgz", + "integrity": "sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lucide-react": { + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", + "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.0", + "@rollup/rollup-android-arm64": "4.45.0", + "@rollup/rollup-darwin-arm64": "4.45.0", + "@rollup/rollup-darwin-x64": "4.45.0", + "@rollup/rollup-freebsd-arm64": "4.45.0", + "@rollup/rollup-freebsd-x64": "4.45.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", + "@rollup/rollup-linux-arm-musleabihf": "4.45.0", + "@rollup/rollup-linux-arm64-gnu": "4.45.0", + "@rollup/rollup-linux-arm64-musl": "4.45.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-musl": "4.45.0", + "@rollup/rollup-linux-s390x-gnu": "4.45.0", + "@rollup/rollup-linux-x64-gnu": "4.45.0", + "@rollup/rollup-linux-x64-musl": "4.45.0", + "@rollup/rollup-win32-arm64-msvc": "4.45.0", + "@rollup/rollup-win32-ia32-msvc": "4.45.0", + "@rollup/rollup-win32-x64-msvc": "4.45.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..88f4f62 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "vision-start", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@hello-pangea/dnd": "^18.0.1", + "lucide-react": "^0.525.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@types/react": "^19.1.8", + "typescript": "~5.7.2", + "vite": "^6.2.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..4d0fdee --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "allowJs": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "paths": { + "@/*" : ["./*"] + } + } +} diff --git a/types.ts b/types.ts new file mode 100755 index 0000000..8640da5 --- /dev/null +++ b/types.ts @@ -0,0 +1,26 @@ + +export interface Website { + id: string; + name: string; + url: string; + icon: string; + categoryId: string; +} + +export interface Server { + id: string; + name: string; + address: string; +} + +export interface Category { + id: string; + name: string; + websites: Website[]; +} + +export interface Wallpaper { + name: string; + url?: string; + base64?: string; +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100755 index 0000000..14322a5 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,14 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + define: { }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +}); diff --git a/waves.jpg b/waves.jpg new file mode 100644 index 0000000..129f7fd Binary files /dev/null and b/waves.jpg differ