Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/app/_components/DarkModeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'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<DarkModeContextType | undefined>(undefined);

export function DarkModeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>('system');
const [isDark, setIsDark] = useState(false);
const [mounted, setMounted] = useState(false);

// Initialize theme from localStorage after mount
useEffect(() => {
setMounted(true);
const stored = localStorage.getItem('theme') as Theme;
if (stored && ['light', 'dark', 'system'].includes(stored)) {
setThemeState(stored);
}

// Set initial isDark state based on current DOM state
const currentlyDark = document.documentElement.classList.contains('dark');
setIsDark(currentlyDark);
}, []);

// Update dark mode state and DOM when theme changes
useEffect(() => {
if (!mounted) return;

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, mounted]);

const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem('theme', newTheme);
};

return (
<DarkModeContext.Provider value={{ theme, setTheme, isDark }}>
{children}
</DarkModeContext.Provider>
);
}

export function useDarkMode() {
const context = useContext(DarkModeContext);
if (context === undefined) {
throw new Error('useDarkMode must be used within a DarkModeProvider');
}
return context;
}
66 changes: 66 additions & 0 deletions src/app/_components/DarkModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
</svg>
);
} else if (theme === 'dark') {
return (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
);
} else {
// System theme icon
return (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7l.159-1.591A3.001 3.001 0 0112 8a3 3 0 01-3.229 2.409L8.771 12z" clipRule="evenodd" />
</svg>
);
}
};

const getLabel = () => {
if (theme === 'light') return 'Light mode';
if (theme === 'dark') return 'Dark mode';
return 'System theme';
};

return (
<button
onClick={toggleTheme}
className={`
flex items-center justify-center
w-10 h-10 rounded-lg
transition-all duration-200
hover:scale-105 active:scale-95
${isDark
? 'bg-gray-800 text-yellow-400 hover:bg-gray-700 border border-gray-600'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-300'
}
`}
title={getLabel()}
aria-label={getLabel()}
>
{getIcon()}
</button>
);
}
59 changes: 28 additions & 31 deletions src/app/_components/InstalledScriptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}
};

Expand All @@ -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 (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading installed scripts...</div>
<div className="text-gray-500 dark:text-gray-400">Loading installed scripts...</div>
</div>
);
}
Expand All @@ -160,8 +160,8 @@ export function InstalledScriptsTab() {
)}

{/* Header with Stats */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Installed Scripts</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4">Installed Scripts</h2>

{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
Expand Down Expand Up @@ -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"
/>
</div>

<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as 'all' | 'success' | 'failed' | 'in_progress')}
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"
>
<option value="all">All Status</option>
<option value="success">Success</option>
Expand All @@ -210,7 +210,7 @@ export function InstalledScriptsTab() {
<select
value={serverFilter}
onChange={(e) => 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"
>
<option value="all">All Servers</option>
<option value="local">Local</option>
Expand All @@ -222,60 +222,57 @@ export function InstalledScriptsTab() {
</div>

{/* Scripts Table */}
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
{filteredScripts.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
{scripts.length === 0 ? 'No installed scripts found.' : 'No scripts match your filters.'}
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Script Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Container ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Server
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mode
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Installation Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredScripts.map((script) => (
<tr key={script.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{script.script_name}</div>
<div className="text-sm text-gray-500">{script.script_path}</div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{script.script_name}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{script.script_path}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{script.container_id ? (
<span className="text-sm font-mono text-gray-900">{String(script.container_id)}</span>
<span className="text-sm font-mono text-gray-900 dark:text-gray-100">{String(script.container_id)}</span>
) : (
<span className="text-sm text-gray-400">-</span>
<span className="text-sm text-gray-400 dark:text-gray-500">-</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{script.execution_mode === 'local' ? (
<span className="text-sm text-gray-900">Local</span>
<span className="text-sm text-gray-900 dark:text-gray-100">Local</span>
) : (
<div>
<div className="text-sm font-medium text-gray-900">{script.server_name}</div>
<div className="text-sm text-gray-500">{script.server_ip}</div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{script.server_name}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{script.server_ip}</div>
</div>
)}
</td>
Expand All @@ -289,7 +286,7 @@ export function InstalledScriptsTab() {
{String(script.status).replace('_', ' ').toUpperCase()}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{formatDate(String(script.installation_date))}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
Expand Down
10 changes: 5 additions & 5 deletions src/app/_components/ResyncButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
Expand All @@ -64,16 +64,16 @@ export function ResyncButton() {
</button>

{lastSync && (
<div className="text-sm text-gray-500">
<div className="text-sm text-gray-500 dark:text-gray-400">
Last sync: {lastSync.toLocaleTimeString()}
</div>
)}

{syncMessage && (
<div className={`text-sm px-3 py-1 rounded-lg ${
syncMessage.includes('Error') || syncMessage.includes('Failed')
? 'bg-red-100 text-red-700'
: 'bg-green-100 text-green-700'
? 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300'
: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'
}`}>
{syncMessage}
</div>
Expand Down
Loading