refactors!
This commit is contained in:
158
App.tsx
158
App.tsx
@@ -1,17 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import WebsiteTile from './components/WebsiteTile';
|
|
||||||
import ConfigurationModal from './components/ConfigurationModal';
|
import ConfigurationModal from './components/ConfigurationModal';
|
||||||
import Clock from './components/Clock';
|
|
||||||
import ServerWidget from './components/ServerWidget';
|
import ServerWidget from './components/ServerWidget';
|
||||||
import { DEFAULT_CATEGORIES } from './constants';
|
import { DEFAULT_CATEGORIES } from './constants';
|
||||||
import { Category, Website, Wallpaper } from './types';
|
import { Category, Website, Wallpaper, Config } from './types';
|
||||||
import Dropdown from './components/Dropdown';
|
|
||||||
import WebsiteEditModal from './components/WebsiteEditModal';
|
import WebsiteEditModal from './components/WebsiteEditModal';
|
||||||
import CategoryEditModal from './components/CategoryEditModal';
|
import CategoryEditModal from './components/CategoryEditModal';
|
||||||
|
import Header from './components/layout/Header';
|
||||||
|
import EditButton from './components/layout/EditButton';
|
||||||
|
import ConfigurationButton from './components/layout/ConfigurationButton';
|
||||||
|
import CategoryGroup from './components/layout/CategoryGroup';
|
||||||
|
|
||||||
import { baseWallpapers } from './components/utils/baseWallpapers';
|
import { baseWallpapers } from './components/utils/baseWallpapers';
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig: Config = {
|
||||||
title: 'Vision Start',
|
title: 'Vision Start',
|
||||||
subtitle: 'Your personal portal to the web.',
|
subtitle: 'Your personal portal to the web.',
|
||||||
backgroundUrls: ['https://i.imgur.com/C6ynAtX.jpeg'],
|
backgroundUrls: ['https://i.imgur.com/C6ynAtX.jpeg'],
|
||||||
@@ -54,7 +55,7 @@ const App: React.FC = () => {
|
|||||||
const [addingWebsite, setAddingWebsite] = useState<Category | null>(null);
|
const [addingWebsite, setAddingWebsite] = useState<Category | null>(null);
|
||||||
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
||||||
const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false);
|
const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false);
|
||||||
const [config, setConfig] = useState(() => {
|
const [config, setConfig] = useState<Config>(() => {
|
||||||
try {
|
try {
|
||||||
const storedConfig = localStorage.getItem('config');
|
const storedConfig = localStorage.getItem('config');
|
||||||
if (storedConfig) {
|
if (storedConfig) {
|
||||||
@@ -240,51 +241,6 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 getHorizontalAlignmentClass = (alignment: string) => {
|
const getHorizontalAlignmentClass = (alignment: string) => {
|
||||||
switch (alignment) {
|
switch (alignment) {
|
||||||
case 'left':
|
case 'left':
|
||||||
@@ -299,7 +255,6 @@ const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<main
|
<main
|
||||||
className={`min-h-screen w-full flex flex-col items-center ${getAlignmentClass(config.alignment)} p-4`}
|
className={`min-h-screen w-full flex flex-col items-center ${getAlignmentClass(config.alignment)} p-4`}
|
||||||
>
|
>
|
||||||
@@ -311,99 +266,26 @@ const App: React.FC = () => {
|
|||||||
opacity: `${config.wallpaperOpacity}%`,
|
opacity: `${config.wallpaperOpacity}%`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<div className="absolute top-4 left-4">
|
<EditButton isEditing={isEditing} onClick={() => setIsEditing(!isEditing)} />
|
||||||
<button
|
<ConfigurationButton onClick={() => setIsConfigModalOpen(true)} />
|
||||||
onClick={() => setIsEditing(!isEditing)}
|
|
||||||
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
|
||||||
style={{ fontSize: '12px' }}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
|
||||||
</svg>
|
|
||||||
{isEditing ? 'Done' : ''}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="absolute top-4 right-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsConfigModalOpen(true)}
|
|
||||||
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-gear-wide" viewBox="0 0 16 16">
|
|
||||||
<path d="M8.932.727c-.243-.97-1.62-.97-1.864 0l-.071.286a.96.96 0 0 1-1.622.434l-.205-.211c-.695-.719-1.888-.03-1.613.931l.08.284a.96.96 0 0 1-1.186 1.187l-.284-.081c-.96-.275-1.65.918-.931 1.613l.211.205a.96.96 0 0 1-.434 1.622l-.286.071c-.97.243-.97 1.62 0 1.864l.286.071a.96.96 0 0 1 .434 1.622l-.211.205c-.719.695-.03 1.888.931 1.613l.284-.08a.96.96 0 0 1 1.187 1.187l-.081.283c-.275.96.918 1.65 1.613.931l.205-.211a.96.96 0 0 1 1.622.434l.071.286c.243.97 1.62.97 1.864 0l.071-.286a.96.96 0 0 1 1.622-.434l.205.211c.695.719 1.888.03 1.613-.931l-.08-.284a.96.96 0 0 1 1.187-1.187l.283.081c.96.275 1.65-.918-.931-1.613l-.211-.205a.96.96 0 0 1 .434-1.622l.286-.071c.97-.243.97-1.62 0-1.864l-.286-.071a.96.96 0 0 1-.434-1.622l.211-.205c.719-.695.03-1.888-.931-1.613l-.284.08a.96.96 0 0 1-1.187-1.186l.081-.284c.275-.96-.918-1.65-1.613-.931l-.205.211a.96.96 0 0 1-1.622-.434zM8 12.997a4.998 4.998 0 1 1 0-9.995 4.998 4.998 0 0 1 0 9.996z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Absolute top-center Clock */}
|
<Header config={config} />
|
||||||
{config.clock.enabled && (
|
|
||||||
<div className="absolute top-5 left-1/2 -translate-x-1/2 z-10 flex justify-center w-auto p-2">
|
|
||||||
<Clock config={config} getClockSizeClass={getClockSizeClass} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={`flex flex-col ${config.alignment === 'bottom' ? 'mt-auto' : ''} items-center`}>
|
|
||||||
{(config.title || config.subtitle) && (
|
|
||||||
<div className="text-center">
|
|
||||||
<h1
|
|
||||||
className={`${getTitleSizeClass(config.titleSize)} font-extrabold text-white tracking-tighter mb-3 mt-4`}
|
|
||||||
style={{ textShadow: '0 2px 4px rgba(0,0,0,0.5)' }}
|
|
||||||
>
|
|
||||||
{config.title}
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
className={`${getSubtitleSizeClass(config.subtitleSize)} text-slate-300`}
|
|
||||||
style={{ textShadow: '0 1px 3px rgba(0,0,0,0.5)' }}
|
|
||||||
>
|
|
||||||
{config.subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-8 w-full mt-16">
|
<div className="flex flex-col gap-8 w-full mt-16">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div key={category.id} className="w-full">
|
<CategoryGroup
|
||||||
<div className={`flex ${getHorizontalAlignmentClass(config.horizontalAlignment)} items-center mb-4 w-full ${config.horizontalAlignment !== 'middle' ? 'px-8' : ''}`}>
|
key={category.id}
|
||||||
<h2 className={`text-2xl font-bold text-white ${config.horizontalAlignment === 'left' ? 'text-left' : config.horizontalAlignment === 'right' ? 'text-right' : 'text-center'} ${config.horizontalAlignment !== 'middle' ? 'w-full' : ''}`}>{category.name}</h2>
|
category={category}
|
||||||
{isEditing && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setEditingCategory(category);
|
|
||||||
setIsCategoryModalOpen(true);
|
|
||||||
}}
|
|
||||||
className={`ml-2 text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zM1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={`flex flex-wrap ${getHorizontalAlignmentClass(config.horizontalAlignment)} gap-6`}>
|
|
||||||
{category.websites.map((website) => (
|
|
||||||
<WebsiteTile
|
|
||||||
key={website.id}
|
|
||||||
website={website}
|
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onEdit={setEditingWebsite}
|
setEditingCategory={setEditingCategory}
|
||||||
onMove={handleMoveWebsite}
|
setIsCategoryModalOpen={setIsCategoryModalOpen}
|
||||||
tileSize={config.tileSize}
|
setAddingWebsite={setAddingWebsite}
|
||||||
|
setEditingWebsite={setEditingWebsite}
|
||||||
|
handleMoveWebsite={handleMoveWebsite}
|
||||||
|
getHorizontalAlignmentClass={getHorizontalAlignmentClass}
|
||||||
|
config={config}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isEditing && (
|
|
||||||
<button
|
|
||||||
onClick={() => setAddingWebsite(category)}
|
|
||||||
className={`text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className={`flex justify-center transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}>
|
<div className={`flex justify-center transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}>
|
||||||
<button
|
<button
|
||||||
|
76
components/layout/CategoryGroup.tsx
Normal file
76
components/layout/CategoryGroup.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import WebsiteTile from '../WebsiteTile';
|
||||||
|
import { Category, Website } from '../../types';
|
||||||
|
|
||||||
|
interface CategoryGroupProps {
|
||||||
|
category: Category;
|
||||||
|
isEditing: boolean;
|
||||||
|
setEditingCategory: (category: Category) => void;
|
||||||
|
setIsCategoryModalOpen: (isOpen: boolean) => void;
|
||||||
|
setAddingWebsite: (category: Category) => void;
|
||||||
|
setEditingWebsite: (website: Website) => void;
|
||||||
|
handleMoveWebsite: (website: Website, direction: 'left' | 'right') => void;
|
||||||
|
getHorizontalAlignmentClass: (alignment: string) => string;
|
||||||
|
config: {
|
||||||
|
horizontalAlignment: string;
|
||||||
|
tileSize?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryGroup: React.FC<CategoryGroupProps> = ({
|
||||||
|
category,
|
||||||
|
isEditing,
|
||||||
|
setEditingCategory,
|
||||||
|
setIsCategoryModalOpen,
|
||||||
|
setAddingWebsite,
|
||||||
|
setEditingWebsite,
|
||||||
|
handleMoveWebsite,
|
||||||
|
getHorizontalAlignmentClass,
|
||||||
|
config,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div key={category.id} className="w-full">
|
||||||
|
<div className={`flex ${getHorizontalAlignmentClass(config.horizontalAlignment)} items-center mb-4 w-full ${config.horizontalAlignment !== 'middle' ? 'px-8' : ''}`}>
|
||||||
|
<h2 className={`text-2xl font-bold text-white ${config.horizontalAlignment === 'left' ? 'text-left' : config.horizontalAlignment === 'right' ? 'text-right' : 'text-center'} ${config.horizontalAlignment !== 'middle' ? 'w-full' : ''}`}>{category.name}</h2>
|
||||||
|
{isEditing && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingCategory(category);
|
||||||
|
setIsCategoryModalOpen(true);
|
||||||
|
}}
|
||||||
|
className={`ml-2 text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zM1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`flex flex-wrap ${getHorizontalAlignmentClass(config.horizontalAlignment)} gap-6`}>
|
||||||
|
{category.websites.map((website) => (
|
||||||
|
<WebsiteTile
|
||||||
|
key={website.id}
|
||||||
|
website={website}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onEdit={setEditingWebsite}
|
||||||
|
onMove={handleMoveWebsite}
|
||||||
|
tileSize={config.tileSize}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{isEditing && (
|
||||||
|
<button
|
||||||
|
onClick={() => setAddingWebsite(category)}
|
||||||
|
className={`text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||||
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryGroup;
|
22
components/layout/ConfigurationButton.tsx
Normal file
22
components/layout/ConfigurationButton.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ConfigurationButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigurationButton: React.FC<ConfigurationButtonProps> = ({ onClick }) => {
|
||||||
|
return (
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-gear-wide" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.932.727c-.243-.97-1.62-.97-1.864 0l-.071.286a.96.96 0 0 1-1.622.434l-.205-.211c-.695-.719-1.888-.03-1.613.931l.08.284a.96.96 0 0 1-1.186 1.187l-.284-.081c-.96-.275-1.65.918-.931 1.613l.211.205a.96.96 0 0 1-.434 1.622l-.286.071c-.97.243-.97 1.62 0 1.864l.286.071a.96.96 0 0 1 .434 1.622l-.211.205c-.719.695-.03 1.888.931 1.613l.284-.08a.96.96 0 0 1 1.187 1.187l-.081.283c-.275.96.918 1.65 1.613.931l.205-.211a.96.96 0 0 1 1.622.434l.071.286c.243.97 1.62.97 1.864 0l.071-.286a.96.96 0 0 1 1.622-.434l.205.211c.695.719 1.888.03 1.613-.931l-.08-.284a.96.96 0 0 1 1.187-1.187l.283.081c.96.275 1.65-.918-.931-1.613l-.211-.205a.96.96 0 0 1 .434-1.622l.286-.071c.97-.243.97-1.62 0-1.864l-.286-.071a.96.96 0 0 1-.434-1.622l.211-.205c.719-.695.03-1.888-.931-1.613l-.284.08a.96.96 0 0 1-1.187-1.186l.081-.284c.275-.96-.918-1.65-1.613-.931l-.205.211a.96.96 0 0 1-1.622-.434zM8 12.997a4.998 4.998 0 1 1 0-9.995 4.998 4.998 0 0 1 0 9.996z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationButton;
|
25
components/layout/EditButton.tsx
Normal file
25
components/layout/EditButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface EditButtonProps {
|
||||||
|
isEditing: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditButton: React.FC<EditButtonProps> = ({ isEditing, onClick }) => {
|
||||||
|
return (
|
||||||
|
<div className="absolute top-4 left-4">
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
|
</svg>
|
||||||
|
{isEditing ? 'Done' : ''}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditButton;
|
84
components/layout/Header.tsx
Normal file
84
components/layout/Header.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Clock from '../Clock';
|
||||||
|
import { Config } from '../../types';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
config: Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Header: React.FC<HeaderProps> = ({ config }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{config.clock.enabled && (
|
||||||
|
<div className="absolute top-5 left-1/2 -translate-x-1/2 z-10 flex justify-center w-auto p-2">
|
||||||
|
<Clock config={config} getClockSizeClass={getClockSizeClass} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`flex flex-col ${config.alignment === 'bottom' ? 'mt-auto' : ''} items-center`}>
|
||||||
|
{(config.title || config.subtitle) && (
|
||||||
|
<div className="text-center">
|
||||||
|
<h1
|
||||||
|
className={`${getTitleSizeClass(config.titleSize)} font-extrabold text-white tracking-tighter mb-3 mt-4`}
|
||||||
|
style={{ textShadow: '0 2px 4px rgba(0,0,0,0.5)' }}
|
||||||
|
>
|
||||||
|
{config.title}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className={`${getSubtitleSizeClass(config.subtitleSize)} text-slate-300`}
|
||||||
|
style={{ textShadow: '0 1px 3px rgba(0,0,0,0.5)' }}
|
||||||
|
>
|
||||||
|
{config.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
27
types.ts
27
types.ts
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export interface Website {
|
export interface Website {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -24,3 +23,29 @@ export interface Wallpaper {
|
|||||||
url?: string;
|
url?: string;
|
||||||
base64?: string;
|
base64?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
backgroundUrls: string[];
|
||||||
|
wallpaperFrequency: string;
|
||||||
|
wallpaperBlur: number;
|
||||||
|
wallpaperBrightness: number;
|
||||||
|
wallpaperOpacity: number;
|
||||||
|
titleSize: string;
|
||||||
|
subtitleSize: string;
|
||||||
|
alignment: string;
|
||||||
|
horizontalAlignment: string;
|
||||||
|
clock: {
|
||||||
|
enabled: boolean;
|
||||||
|
size: string;
|
||||||
|
font: string;
|
||||||
|
format: string;
|
||||||
|
};
|
||||||
|
serverWidget: {
|
||||||
|
enabled: boolean;
|
||||||
|
pingFrequency: number;
|
||||||
|
servers: Server[];
|
||||||
|
};
|
||||||
|
tileSize?: string;
|
||||||
|
}
|
Reference in New Issue
Block a user