From d59fd168894c92e4e2505be5557a118eb574a987 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:15:07 +0200 Subject: [PATCH 1/2] Add dark mode support across UI Introduces DarkModeProvider and DarkModeToggle components for theme management. Updates all major UI components and pages to support dark mode styling using Tailwind CSS dark variants, improving accessibility and user experience for users preferring dark themes. --- src/app/_components/DarkModeProvider.tsx | 75 +++++++++++++++++ src/app/_components/DarkModeToggle.tsx | 66 +++++++++++++++ src/app/_components/InstalledScriptsTab.tsx | 59 +++++++------ src/app/_components/ResyncButton.tsx | 10 +-- src/app/_components/ScriptCard.tsx | 22 ++--- src/app/_components/ScriptDetailModal.tsx | 92 ++++++++++----------- src/app/_components/ScriptsGrid.tsx | 6 +- src/app/_components/ServerForm.tsx | 36 ++++---- src/app/_components/SettingsButton.tsx | 2 +- src/app/_components/SettingsModal.tsx | 32 +++---- src/app/layout.tsx | 13 ++- src/app/page.tsx | 16 ++-- src/styles/globals.css | 2 + 13 files changed, 290 insertions(+), 141 deletions(-) create mode 100644 src/app/_components/DarkModeProvider.tsx create mode 100644 src/app/_components/DarkModeToggle.tsx diff --git a/src/app/_components/DarkModeProvider.tsx b/src/app/_components/DarkModeProvider.tsx new file mode 100644 index 00000000..7a490a74 --- /dev/null +++ b/src/app/_components/DarkModeProvider.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { createContext, useContext, useEffect, useState } from 'react'; + +type Theme = 'light' | 'dark' | 'system'; + +interface DarkModeContextType { + theme: Theme; + setTheme: (theme: Theme) => void; + isDark: boolean; +} + +const DarkModeContext = createContext(undefined); + +export function DarkModeProvider({ children }: { children: React.ReactNode }) { + const [theme, setThemeState] = useState('system'); + const [isDark, setIsDark] = useState(false); + + // Initialize theme from localStorage or default to system + useEffect(() => { + const stored = localStorage.getItem('theme') as Theme; + if (stored && ['light', 'dark', 'system'].includes(stored)) { + setThemeState(stored); + } + }, []); + + // Update dark mode state and DOM + useEffect(() => { + const updateDarkMode = () => { + const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const shouldBeDark = theme === 'dark' || (theme === 'system' && systemDark); + + setIsDark(shouldBeDark); + + // Apply to document + if (shouldBeDark) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }; + + updateDarkMode(); + + // Listen for system theme changes + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = () => { + if (theme === 'system') { + updateDarkMode(); + } + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [theme]); + + const setTheme = (newTheme: Theme) => { + setThemeState(newTheme); + localStorage.setItem('theme', newTheme); + }; + + return ( + + {children} + + ); +} + +export function useDarkMode() { + const context = useContext(DarkModeContext); + if (context === undefined) { + throw new Error('useDarkMode must be used within a DarkModeProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/app/_components/DarkModeToggle.tsx b/src/app/_components/DarkModeToggle.tsx new file mode 100644 index 00000000..5cccd618 --- /dev/null +++ b/src/app/_components/DarkModeToggle.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useDarkMode } from './DarkModeProvider'; + +export function DarkModeToggle() { + const { theme, setTheme, isDark } = useDarkMode(); + + const toggleTheme = () => { + if (theme === 'light') { + setTheme('dark'); + } else if (theme === 'dark') { + setTheme('system'); + } else { + setTheme('light'); + } + }; + + const getIcon = () => { + if (theme === 'light') { + return ( + + + + ); + } else if (theme === 'dark') { + return ( + + + + ); + } else { + // System theme icon + return ( + + + + ); + } + }; + + const getLabel = () => { + if (theme === 'light') return 'Light mode'; + if (theme === 'dark') return 'Dark mode'; + return 'System theme'; + }; + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index 17f2f4a4..2db0ea9c 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -119,7 +119,7 @@ export function InstalledScriptsTab() { case 'in_progress': return `${baseClasses} bg-yellow-100 text-yellow-800`; default: - return `${baseClasses} bg-gray-100 text-gray-800`; + return `${baseClasses} bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200`; } }; @@ -131,14 +131,14 @@ export function InstalledScriptsTab() { case 'ssh': return `${baseClasses} bg-purple-100 text-purple-800`; default: - return `${baseClasses} bg-gray-100 text-gray-800`; + return `${baseClasses} bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200`; } }; if (isLoading) { return (
-
Loading installed scripts...
+
Loading installed scripts...
); } @@ -160,8 +160,8 @@ export function InstalledScriptsTab() { )} {/* Header with Stats */} -
-

Installed Scripts

+
+

Installed Scripts

{stats && (
@@ -192,14 +192,14 @@ export function InstalledScriptsTab() { placeholder="Search scripts, container IDs, or servers..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400" />
setServerFilter(e.target.value)} - className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400" > @@ -222,60 +222,57 @@ export function InstalledScriptsTab() {
{/* Scripts Table */} -
+
{filteredScripts.length === 0 ? ( -
+
{scripts.length === 0 ? 'No installed scripts found.' : 'No scripts match your filters.'}
) : (
- - +
+ - - - - - - - - + {filteredScripts.map((script) => ( @@ -289,7 +286,7 @@ export function InstalledScriptsTab() { {String(script.status).replace('_', ' ').toUpperCase()} -
+ Script Name + Container ID + Server - Mode - + Status - Date + + Installation Date + Actions
-
{script.script_name}
-
{script.script_path}
+
{script.script_name}
+
{script.script_path}
{script.container_id ? ( - {String(script.container_id)} + {String(script.container_id)} ) : ( - - + - )} {script.execution_mode === 'local' ? ( - Local + Local ) : (
-
{script.server_name}
-
{script.server_ip}
+
{script.server_name}
+
{script.server_ip}
)}
+ {formatDate(String(script.installation_date))} diff --git a/src/app/_components/ResyncButton.tsx b/src/app/_components/ResyncButton.tsx index 7ceb67a9..fb028820 100644 --- a/src/app/_components/ResyncButton.tsx +++ b/src/app/_components/ResyncButton.tsx @@ -44,8 +44,8 @@ export function ResyncButton() { disabled={isResyncing} className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-colors ${ isResyncing - ? 'bg-gray-400 text-white cursor-not-allowed' - : 'bg-blue-600 text-white hover:bg-blue-700' + ? 'bg-gray-400 dark:bg-gray-600 text-white cursor-not-allowed' + : 'bg-blue-600 dark:bg-blue-700 text-white hover:bg-blue-700 dark:hover:bg-blue-600' }`} > {isResyncing ? ( @@ -64,7 +64,7 @@ export function ResyncButton() { {lastSync && ( -
+
Last sync: {lastSync.toLocaleTimeString()}
)} @@ -72,8 +72,8 @@ export function ResyncButton() { {syncMessage && (
{syncMessage}
diff --git a/src/app/_components/ScriptCard.tsx b/src/app/_components/ScriptCard.tsx index c3bb58fb..85ad8c76 100644 --- a/src/app/_components/ScriptCard.tsx +++ b/src/app/_components/ScriptCard.tsx @@ -18,7 +18,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) { return (
onClick(script)} >
@@ -35,15 +35,15 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) { onError={handleImageError} /> ) : ( -
- +
+ {script.name?.charAt(0)?.toUpperCase() || '?'}
)}
-

+

{script.name || 'Unnamed Script'}

@@ -51,15 +51,15 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
{script.type?.toUpperCase() || 'UNKNOWN'} {script.updateable && ( - + Updateable )} @@ -71,7 +71,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) { script.isDownloaded ? 'bg-green-500' : 'bg-red-500' }`}>
{script.isDownloaded ? 'Downloaded' : 'Not Downloaded'} @@ -81,7 +81,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) {
{/* Description */} -

+

{script.description || 'No description available'}

@@ -92,7 +92,7 @@ export function ScriptCard({ script, onClick }: ScriptCardProps) { href={script.website} target="_blank" rel="noopener noreferrer" - className="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center space-x-1" + className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-sm font-medium flex items-center space-x-1" onClick={(e) => e.stopPropagation()} > Website diff --git a/src/app/_components/ScriptDetailModal.tsx b/src/app/_components/ScriptDetailModal.tsx index 07fae891..f0f50c4a 100644 --- a/src/app/_components/ScriptDetailModal.tsx +++ b/src/app/_components/ScriptDetailModal.tsx @@ -114,9 +114,9 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }: className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" onClick={handleBackdropClick} > -
+
{/* Header */} -
+
{script.logo && !imageError ? ( ) : ( -
- +
+ {script.name.charAt(0).toUpperCase()}
)}
-

{script.name}

+

{script.name}

{script.type.toUpperCase()} @@ -262,7 +262,7 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }: })()} )} diff --git a/src/app/_components/SettingsButton.tsx b/src/app/_components/SettingsButton.tsx index 0d26422e..9b16cb57 100644 --- a/src/app/_components/SettingsButton.tsx +++ b/src/app/_components/SettingsButton.tsx @@ -10,7 +10,7 @@ export function SettingsButton() { <> - )} + {scriptFilesData?.success && + scriptFilesData.ctExists && + onInstallScript && ( + + )} {/* View Button - only show if script files exist */} - {scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && ( - - )} - + {scriptFilesData?.success && + (scriptFilesData.ctExists || scriptFilesData.installExists) && ( + + )} + {/* Load/Update Script Button */} {(() => { - const hasLocalFiles = scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists); - const hasDifferences = comparisonData?.success && comparisonData.hasDifferences; + const hasLocalFiles = + scriptFilesData?.success && + (scriptFilesData.ctExists || scriptFilesData.installExists); + const hasDifferences = + comparisonData?.success && comparisonData.hasDifferences; const isUpToDate = hasLocalFiles && !hasDifferences; - + if (!hasLocalFiles) { // No local files - show Load Script button return ( @@ -237,21 +312,31 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
@@ -273,114 +368,169 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }: {/* Load Message */} {loadMessage && ( -
+
{loadMessage}
)} {/* Script Files Status */} {(scriptFilesLoading || comparisonLoading) && ( -
+
-
+
Loading script status...
)} - - {scriptFilesData?.success && !scriptFilesLoading && (() => { - // Determine script type from the first install method - const firstScript = script?.install_methods?.[0]?.script; - let scriptType = 'Script'; - if (firstScript?.startsWith('ct/')) { - scriptType = 'CT Script'; - } else if (firstScript?.startsWith('tools/')) { - scriptType = 'Tools Script'; - } else if (firstScript?.startsWith('vm/')) { - scriptType = 'VM Script'; - } else if (firstScript?.startsWith('vw/')) { - scriptType = 'VW Script'; - } - return ( -
-
-
-
- {scriptType}: {scriptFilesData.ctExists ? 'Available' : 'Not loaded'} -
-
-
- Install Script: {scriptFilesData.installExists ? 'Available' : 'Not loaded'} -
- {scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && comparisonData?.success && !comparisonLoading && ( + {scriptFilesData?.success && + !scriptFilesLoading && + (() => { + // Determine script type from the first install method + const firstScript = script?.install_methods?.[0]?.script; + let scriptType = "Script"; + if (firstScript?.startsWith("ct/")) { + scriptType = "CT Script"; + } else if (firstScript?.startsWith("tools/")) { + scriptType = "Tools Script"; + } else if (firstScript?.startsWith("vm/")) { + scriptType = "VM Script"; + } else if (firstScript?.startsWith("vw/")) { + scriptType = "VW Script"; + } + + return ( +
+
-
- Status: {comparisonData.hasDifferences ? 'Update available' : 'Up to date'} +
+ + {scriptType}:{" "} + {scriptFilesData.ctExists ? "Available" : "Not loaded"} + +
+
+
+ + Install Script:{" "} + {scriptFilesData.installExists + ? "Available" + : "Not loaded"} + +
+ {scriptFilesData?.success && + (scriptFilesData.ctExists || + scriptFilesData.installExists) && + comparisonData?.success && + !comparisonLoading && ( +
+
+ + Status:{" "} + {comparisonData.hasDifferences + ? "Update available" + : "Up to date"} + +
+ )} +
+ {scriptFilesData.files.length > 0 && ( +
+ Files: {scriptFilesData.files.join(", ")}
)}
- {scriptFilesData.files.length > 0 && ( -
- Files: {scriptFilesData.files.join(', ')} -
- )} -
- ); - })()} + ); + })()} {/* Content */} -
+
{/* Description */}
-

Description

-

{script.description}

+

+ Description +

+

+ {script.description} +

{/* Basic Information */} -
+
-

Basic Information

+

+ Basic Information +

-
Slug
-
{script.slug}
+
+ Slug +
+
+ {script.slug} +
-
Date Created
-
{script.date_created}
+
+ Date Created +
+
+ {script.date_created} +
-
Categories
-
{script.categories.join(', ')}
+
+ Categories +
+
+ {script.categories.join(", ")} +
{script.interface_port && (
-
Interface Port
-
{script.interface_port}
+
+ Interface Port +
+
+ {script.interface_port} +
)} {script.config_path && (
-
Config Path
-
{script.config_path}
+
+ Config Path +
+
+ {script.config_path} +
)}
-

Links

+

+ Links +

{script.website && (
-
Website
+
+ Website +
{script.website} @@ -389,13 +539,15 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }: )} {script.documentation && (
-
Documentation
+
+ Documentation +
{script.documentation} @@ -406,56 +558,94 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
- {/* Install Methods */} - {script.install_methods.length > 0 && ( -
-

Install Methods

-
- {script.install_methods.map((method, index) => ( -
-
-

{method.type}

- {method.script} -
-
-
-
CPU
-
{method.resources.cpu} cores
-
-
-
RAM
-
{method.resources.ram} MB
-
-
-
HDD
-
{method.resources.hdd} GB
+ {/* Install Methods - Hide for PVE and ADDON types as they typically don't have install methods */} + {script.install_methods.length > 0 && + script.type !== "pve" && + script.type !== "addon" && ( +
+

+ Install Methods +

+
+ {script.install_methods.map((method, index) => ( +
+
+

+ {method.type} +

+ + {method.script} +
-
-
OS
-
{method.resources.os} {method.resources.version}
+
+
+
+ CPU +
+
+ {method.resources.cpu} cores +
+
+
+
+ RAM +
+
+ {method.resources.ram} MB +
+
+
+
+ HDD +
+
+ {method.resources.hdd} GB +
+
+
+
+ OS +
+
+ {method.resources.os} {method.resources.version} +
+
-
- ))} + ))} +
-
- )} + )} {/* Default Credentials */} - {(script.default_credentials.username ?? script.default_credentials.password) && ( + {(script.default_credentials.username ?? + script.default_credentials.password) && (
-

Default Credentials

+

+ Default Credentials +

{script.default_credentials.username && (
-
Username
-
{script.default_credentials.username}
+
+ Username +
+
+ {script.default_credentials.username} +
)} {script.default_credentials.password && (
-
Password
-
{script.default_credentials.password}
+
+ Password +
+
+ {script.default_credentials.password} +
)}
@@ -465,29 +655,37 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }: {/* Notes */} {script.notes.length > 0 && (
-

Notes

+

+ Notes +

    {script.notes.map((note, index) => { // Handle both object and string note formats - const noteText = typeof note === 'string' ? note : note.text; - const noteType = typeof note === 'string' ? 'info' : note.type; - + const noteText = typeof note === "string" ? note : note.text; + const noteType = + typeof note === "string" ? "info" : note.type; + return ( -
  • +
  • - + {noteType} {noteText} @@ -517,7 +715,12 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }: {/* Text Viewer Modal */} {script && ( method.script?.startsWith('ct/'))?.script?.split('/').pop() ?? `${script.slug}.sh`} + scriptName={ + script.install_methods + ?.find((method) => method.script?.startsWith("ct/")) + ?.script?.split("/") + .pop() ?? `${script.slug}.sh` + } isOpen={textViewerOpen} onClose={() => setTextViewerOpen(false)} /> diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8f00e88b..36d1edbd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -27,6 +27,28 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode }>) { return ( + +