feat: Implement dark mode support across components and UI elements
- Added dark mode styles to TaskStatusDropdown, TaskStatusDropdownDebug, and TaskStatusDropdownSimple components. - Introduced ThemeProvider and useTheme hook for managing theme state. - Updated Button, Card, Input, Loading, Navigation, PageContainer, PageHeader, ProjectCalendarWidget, ProjectMap, SearchBar, States, Tooltip, and other UI components to support dark mode. - Created ThemeToggle component for switching between light and dark modes. - Enhanced i18n translations for settings related to theme and language preferences. - Configured Tailwind CSS to support dark mode with class-based toggling.
This commit is contained in:
@@ -5,6 +5,91 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
|
|
||||||
|
/* Surface colors */
|
||||||
|
--surface-primary: #ffffff;
|
||||||
|
--surface-secondary: #f9fafb;
|
||||||
|
--surface-tertiary: #f3f4f6;
|
||||||
|
--surface-modal: #ffffff;
|
||||||
|
--surface-card: #ffffff;
|
||||||
|
--surface-hover: #f9fafb;
|
||||||
|
--surface-active: #f3f4f6;
|
||||||
|
|
||||||
|
/* Text colors */
|
||||||
|
--text-primary: #111827;
|
||||||
|
--text-secondary: #6b7280;
|
||||||
|
--text-tertiary: #9ca3af;
|
||||||
|
--text-inverse: #ffffff;
|
||||||
|
--text-muted: #6b7280;
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
--border-default: #d1d5db;
|
||||||
|
--border-hover: #9ca3af;
|
||||||
|
--border-focus: #3b82f6;
|
||||||
|
--border-divider: #e5e7eb;
|
||||||
|
|
||||||
|
/* Interactive colors */
|
||||||
|
--interactive-primary: #3b82f6;
|
||||||
|
--interactive-primary-hover: #2563eb;
|
||||||
|
--interactive-secondary: #6b7280;
|
||||||
|
--interactive-secondary-hover: #4b5563;
|
||||||
|
--interactive-danger: #ef4444;
|
||||||
|
--interactive-danger-hover: #dc2626;
|
||||||
|
|
||||||
|
/* Status colors */
|
||||||
|
--status-success: #10b981;
|
||||||
|
--status-warning: #f59e0b;
|
||||||
|
--status-error: #ef4444;
|
||||||
|
--status-info: #3b82f6;
|
||||||
|
|
||||||
|
/* Shadow colors */
|
||||||
|
--shadow-default: rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-hover: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
|
||||||
|
/* Surface colors */
|
||||||
|
--surface-primary: #1f2937;
|
||||||
|
--surface-secondary: #374151;
|
||||||
|
--surface-tertiary: #4b5563;
|
||||||
|
--surface-modal: #1f2937;
|
||||||
|
--surface-card: #374151;
|
||||||
|
--surface-hover: #4b5563;
|
||||||
|
--surface-active: #6b7280;
|
||||||
|
|
||||||
|
/* Text colors */
|
||||||
|
--text-primary: #f9fafb;
|
||||||
|
--text-secondary: #d1d5db;
|
||||||
|
--text-tertiary: #9ca3af;
|
||||||
|
--text-inverse: #111827;
|
||||||
|
--text-muted: #9ca3af;
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
--border-default: #4b5563;
|
||||||
|
--border-hover: #6b7280;
|
||||||
|
--border-focus: #60a5fa;
|
||||||
|
--border-divider: #374151;
|
||||||
|
|
||||||
|
/* Interactive colors */
|
||||||
|
--interactive-primary: #3b82f6;
|
||||||
|
--interactive-primary-hover: #60a5fa;
|
||||||
|
--interactive-secondary: #6b7280;
|
||||||
|
--interactive-secondary-hover: #9ca3af;
|
||||||
|
--interactive-danger: #ef4444;
|
||||||
|
--interactive-danger-hover: #f87171;
|
||||||
|
|
||||||
|
/* Status colors */
|
||||||
|
--status-success: #10b981;
|
||||||
|
--status-warning: #f59e0b;
|
||||||
|
--status-error: #ef4444;
|
||||||
|
--status-info: #3b82f6;
|
||||||
|
|
||||||
|
/* Shadow colors */
|
||||||
|
--shadow-default: rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-hover: rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @media (prefers-color-scheme: dark) {
|
/* @media (prefers-color-scheme: dark) {
|
||||||
@@ -29,6 +114,18 @@ body {
|
|||||||
background: #f1f1f1;
|
background: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track:window-inactive {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-track {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-track:window-inactive {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #c1c1c1;
|
background: #c1c1c1;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -38,6 +135,14 @@ body {
|
|||||||
background: #a8a8a8;
|
background: #a8a8a8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
/* Focus styles */
|
/* Focus styles */
|
||||||
.focus-ring {
|
.focus-ring {
|
||||||
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2;
|
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "./globals.css";
|
|||||||
import Navigation from "@/components/ui/Navigation";
|
import Navigation from "@/components/ui/Navigation";
|
||||||
import { AuthProvider } from "@/components/auth/AuthProvider";
|
import { AuthProvider } from "@/components/auth/AuthProvider";
|
||||||
import { TranslationProvider } from "@/lib/i18n";
|
import { TranslationProvider } from "@/lib/i18n";
|
||||||
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -23,14 +24,16 @@ export default function RootLayout({ children }) {
|
|||||||
return (
|
return (
|
||||||
<html lang="pl">
|
<html lang="pl">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
||||||
>
|
>
|
||||||
<TranslationProvider initialLanguage="pl">
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<TranslationProvider initialLanguage="pl">
|
||||||
<Navigation />
|
<AuthProvider>
|
||||||
<main>{children}</main>
|
<Navigation />
|
||||||
</AuthProvider>
|
<main>{children}</main>
|
||||||
</TranslationProvider>
|
</AuthProvider>
|
||||||
|
</TranslationProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { formatProjectStatus } from "@/lib/utils";
|
|||||||
// Loading component that can access translations
|
// Loading component that can access translations
|
||||||
function MapLoadingComponent({ t }) {
|
function MapLoadingComponent({ t }) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-96 bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
|
<div className="w-full h-96 bg-gray-100 dark:bg-gray-700 animate-pulse rounded-lg flex items-center justify-center">
|
||||||
<span className="text-gray-500">{t ? t('map.loadingMap') : 'Loading map...'}</span>
|
<span className="text-gray-500 dark:text-gray-400">{t ? t('map.loadingMap') : 'Loading map...'}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -410,11 +410,11 @@ function ProjectsMapPageContent() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-50 flex items-center justify-center">
|
<div className="fixed inset-0 bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-12 h-12 mx-auto mb-4 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin"></div>
|
<div className="w-12 h-12 mx-auto mb-4 border-4 border-blue-200 dark:border-blue-800 border-t-blue-600 dark:border-t-blue-400 rounded-full animate-spin"></div>
|
||||||
<p className="text-gray-600 font-medium">{t('map.loadingProjectsMap')}</p>
|
<p className="text-gray-600 dark:text-gray-300 font-medium">{t('map.loadingProjectsMap')}</p>
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||||
{t('map.preparingMap')}
|
{t('map.preparingMap')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,7 +422,7 @@ function ProjectsMapPageContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-50 overflow-hidden">
|
<div className="fixed inset-0 bg-gray-50 dark:bg-gray-900 overflow-hidden">
|
||||||
{/* Floating Header - Left Side */}
|
{/* Floating Header - Left Side */}
|
||||||
<div className="absolute top-4 left-4 z-[1000]">
|
<div className="absolute top-4 left-4 z-[1000]">
|
||||||
{/* Title Box */}
|
{/* Title Box */}
|
||||||
@@ -989,10 +989,10 @@ function ProjectsMapPageContent() {
|
|||||||
export default function ProjectsMapPage() {
|
export default function ProjectsMapPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={
|
<Suspense fallback={
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 dark:border-blue-400 mx-auto mb-4"></div>
|
||||||
<p className="text-gray-600">Loading map...</p>
|
<p className="text-gray-600 dark:text-gray-300">Loading map...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
|
|||||||
@@ -254,13 +254,13 @@ export default function ProjectListPage() {
|
|||||||
<div className="px-4 pb-4 border-t border-gray-100">
|
<div className="px-4 pb-4 border-t border-gray-100">
|
||||||
<div className="flex flex-col space-y-4 md:flex-row md:flex-wrap md:gap-4 md:space-y-0 md:items-center pt-4">
|
<div className="flex flex-col space-y-4 md:flex-row md:flex-wrap md:gap-4 md:space-y-0 md:items-center pt-4">
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap">
|
||||||
{t('common.status') || 'Status'}:
|
{t('common.status') || 'Status'}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={filters.status}
|
value={filters.status}
|
||||||
onChange={(e) => handleFilterChange('status', e.target.value)}
|
onChange={(e) => handleFilterChange('status', e.target.value)}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
>
|
>
|
||||||
<option value="all">{t('common.all')}</option>
|
<option value="all">{t('common.all')}</option>
|
||||||
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
|
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
|
||||||
@@ -272,13 +272,13 @@ export default function ProjectListPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap">
|
||||||
{t('common.type') || 'Typ'}:
|
{t('common.type') || 'Typ'}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={filters.type}
|
value={filters.type}
|
||||||
onChange={(e) => handleFilterChange('type', e.target.value)}
|
onChange={(e) => handleFilterChange('type', e.target.value)}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
>
|
>
|
||||||
<option value="all">{t('common.all')}</option>
|
<option value="all">{t('common.all')}</option>
|
||||||
<option value="design">{t('projectType.design')}</option>
|
<option value="design">{t('projectType.design')}</option>
|
||||||
@@ -288,13 +288,13 @@ export default function ProjectListPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap">
|
||||||
{t('contracts.customer') || 'Klient'}:
|
{t('contracts.customer') || 'Klient'}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={filters.customer}
|
value={filters.customer}
|
||||||
onChange={(e) => handleFilterChange('customer', e.target.value)}
|
onChange={(e) => handleFilterChange('customer', e.target.value)}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
>
|
>
|
||||||
<option value="all">{t('common.all')}</option>
|
<option value="all">{t('common.all')}</option>
|
||||||
{customers.map((customer) => (
|
{customers.map((customer) => (
|
||||||
@@ -313,13 +313,13 @@ export default function ProjectListPage() {
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex flex-col space-y-4 md:flex-row md:flex-wrap md:gap-4 md:space-y-0 md:items-center">
|
<div className="flex flex-col space-y-4 md:flex-row md:flex-wrap md:gap-4 md:space-y-0 md:items-center">
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap">
|
||||||
{t('common.status') || 'Status'}:
|
{t('common.status') || 'Status'}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={filters.status}
|
value={filters.status}
|
||||||
onChange={(e) => handleFilterChange('status', e.target.value)}
|
onChange={(e) => handleFilterChange('status', e.target.value)}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
>
|
>
|
||||||
<option value="all">{t('common.all')}</option>
|
<option value="all">{t('common.all')}</option>
|
||||||
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
|
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
|
||||||
@@ -331,13 +331,13 @@ export default function ProjectListPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap">
|
||||||
{t('common.type') || 'Typ'}:
|
{t('common.type') || 'Typ'}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={filters.type}
|
value={filters.type}
|
||||||
onChange={(e) => handleFilterChange('type', e.target.value)}
|
onChange={(e) => handleFilterChange('type', e.target.value)}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
>
|
>
|
||||||
<option value="all">{t('common.all')}</option>
|
<option value="all">{t('common.all')}</option>
|
||||||
<option value="design">{t('projectType.design')}</option>
|
<option value="design">{t('projectType.design')}</option>
|
||||||
@@ -347,13 +347,13 @@ export default function ProjectListPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap">
|
||||||
{t('contracts.customer') || 'Klient'}:
|
{t('contracts.customer') || 'Klient'}:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={filters.customer}
|
value={filters.customer}
|
||||||
onChange={(e) => handleFilterChange('customer', e.target.value)}
|
onChange={(e) => handleFilterChange('customer', e.target.value)}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
>
|
>
|
||||||
<option value="all">{t('common.all')}</option>
|
<option value="all">{t('common.all')}</option>
|
||||||
{customers.map((customer) => (
|
{customers.map((customer) => (
|
||||||
@@ -437,40 +437,40 @@ export default function ProjectListPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||||
{/* Mobile scroll container */}
|
{/* Mobile scroll container */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[600px] table-fixed">
|
<table className="w-full min-w-[600px] table-fixed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-100 border-b">
|
<tr className="bg-gray-100 dark:bg-gray-700 border-b dark:border-gray-600">
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20 md:w-24">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-20 md:w-24">
|
||||||
Nr.
|
Nr.
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-[200px] md:w-[250px]">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-[200px] md:w-[250px]">
|
||||||
{t('projects.projectName')}
|
{t('projects.projectName')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-16 md:w-20 hidden sm:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-16 md:w-20 hidden sm:table-cell">
|
||||||
WP
|
WP
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-14 md:w-16 hidden md:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16 hidden md:table-cell">
|
||||||
{t('projects.city')}
|
{t('projects.city')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20 md:w-24 hidden lg:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-20 md:w-24 hidden lg:table-cell">
|
||||||
{t('projects.address')}
|
{t('projects.address')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-14 md:w-16 hidden sm:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16 hidden sm:table-cell">
|
||||||
{t('projects.plot')}
|
{t('projects.plot')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-18 md:w-20 hidden md:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-18 md:w-20 hidden md:table-cell">
|
||||||
{t('projects.finishDate')}
|
{t('projects.finishDate')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-10">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-10">
|
||||||
{t('common.type') || 'Typ'}
|
{t('common.type') || 'Typ'}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-10">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-10">
|
||||||
{t('common.status') || 'Status'}
|
{t('common.status') || 'Status'}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-14 md:w-16">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16">
|
||||||
{t('common.actions') || 'Akcje'}
|
{t('common.actions') || 'Akcje'}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -479,8 +479,8 @@ export default function ProjectListPage() {
|
|||||||
{filteredProjects.map((project, index) => (
|
{filteredProjects.map((project, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={project.project_id}
|
key={project.project_id}
|
||||||
className={`border-b hover:bg-gray-50 transition-colors ${
|
className={`border-b dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${
|
||||||
index % 2 === 0 ? "bg-white" : "bg-gray-25"
|
index % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-25 dark:bg-gray-750"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td className="px-1 py-3">
|
<td className="px-1 py-3">
|
||||||
@@ -505,38 +505,38 @@ export default function ProjectListPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-2 py-3 text-xs text-gray-600 truncate hidden sm:table-cell"
|
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden sm:table-cell"
|
||||||
title={project.wp}
|
title={project.wp}
|
||||||
>
|
>
|
||||||
{project.wp || "N/A"}
|
{project.wp || "N/A"}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-2 py-3 text-xs text-gray-600 truncate hidden md:table-cell"
|
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden md:table-cell"
|
||||||
title={project.city}
|
title={project.city}
|
||||||
>
|
>
|
||||||
{project.city || "N/A"}
|
{project.city || "N/A"}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-2 py-3 text-xs text-gray-600 truncate hidden lg:table-cell"
|
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden lg:table-cell"
|
||||||
title={project.address}
|
title={project.address}
|
||||||
>
|
>
|
||||||
{project.address || "N/A"}
|
{project.address || "N/A"}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-2 py-3 text-xs text-gray-600 truncate hidden sm:table-cell"
|
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden sm:table-cell"
|
||||||
title={project.plot}
|
title={project.plot}
|
||||||
>
|
>
|
||||||
{project.plot || "N/A"}
|
{project.plot || "N/A"}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-2 py-3 text-xs text-gray-600 truncate hidden md:table-cell"
|
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden md:table-cell"
|
||||||
title={project.finish_date}
|
title={project.finish_date}
|
||||||
>
|
>
|
||||||
{project.finish_date
|
{project.finish_date
|
||||||
? formatDate(project.finish_date)
|
? formatDate(project.finish_date)
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-2 py-3 text-xs text-gray-600 text-center font-semibold">
|
<td className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 text-center font-semibold">
|
||||||
{project.project_type === "design"
|
{project.project_type === "design"
|
||||||
? "P"
|
? "P"
|
||||||
: project.project_type === "construction"
|
: project.project_type === "construction"
|
||||||
|
|||||||
64
src/app/settings/page.js
Normal file
64
src/app/settings/page.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslation } from "@/lib/i18n";
|
||||||
|
import PageContainer from "@/components/ui/PageContainer";
|
||||||
|
import PageHeader from "@/components/ui/PageHeader";
|
||||||
|
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||||
|
import ThemeToggle from "@/components/ui/ThemeToggle";
|
||||||
|
import LanguageSwitcher from "@/components/ui/LanguageSwitcher";
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader title={t('settings.title') || 'Settings'} />
|
||||||
|
|
||||||
|
<div className="max-w-2xl mx-auto space-y-6">
|
||||||
|
{/* Appearance Settings */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{t('settings.appearance') || 'Appearance'}
|
||||||
|
</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{t('settings.theme') || 'Theme'}
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{t('settings.themeDescription') || 'Choose your preferred theme'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Language Settings */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{t('settings.language') || 'Language'}
|
||||||
|
</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{t('settings.language') || 'Language'}
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{t('settings.languageDescription') || 'Select your preferred language'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -318,43 +318,43 @@ export default function AuditLogViewer() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Audit Logs Table */}
|
{/* Audit Logs Table */}
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
<tr>
|
<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">
|
||||||
Timestamp
|
Timestamp
|
||||||
</th>
|
</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">
|
||||||
User
|
User
|
||||||
</th>
|
</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">
|
||||||
Action
|
Action
|
||||||
</th>
|
</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">
|
||||||
Resource
|
Resource
|
||||||
</th>
|
</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">
|
||||||
IP Address
|
IP Address
|
||||||
</th>
|
</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">
|
||||||
Details
|
Details
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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-600">
|
||||||
{logs.map((log) => (
|
{logs.map((log) => (
|
||||||
<tr key={log.id} className="hover:bg-gray-50">
|
<tr key={log.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
{formatTimestamp(log.timestamp)}
|
{formatTimestamp(log.timestamp)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{log.user_name || "Anonymous"}
|
{log.user_name || "Anonymous"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-500">{log.user_email}</div>
|
<div className="text-gray-500 dark:text-gray-400">{log.user_email}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
@@ -364,26 +364,26 @@ export default function AuditLogViewer() {
|
|||||||
{log.action.replace(/_/g, " ").toUpperCase()}
|
{log.action.replace(/_/g, " ").toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{log.resource_type || "N/A"}
|
{log.resource_type || "N/A"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500 dark:text-gray-400">
|
||||||
ID: {log.resource_id || "N/A"}
|
ID: {log.resource_id || "N/A"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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">
|
||||||
{log.ip_address || "Unknown"}
|
{log.ip_address || "Unknown"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">
|
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{log.details && (
|
{log.details && (
|
||||||
<details className="cursor-pointer">
|
<details className="cursor-pointer">
|
||||||
<summary className="text-blue-600 hover:text-blue-800">
|
<summary className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300">
|
||||||
View Details
|
View Details
|
||||||
</summary>
|
</summary>
|
||||||
<pre className="mt-2 text-xs bg-gray-100 p-2 rounded overflow-auto max-w-md">
|
<pre className="mt-2 text-xs bg-gray-100 dark:bg-gray-700 p-2 rounded overflow-auto max-w-md">
|
||||||
{JSON.stringify(log.details, null, 2)}
|
{JSON.stringify(log.details, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -396,7 +396,7 @@ export default function AuditLogViewer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{logs.length === 0 && !loading && (
|
{logs.length === 0 && !loading && (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
No audit logs found matching your criteria.
|
No audit logs found matching your criteria.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ export default function FileUploadModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
<div className="bg-surface-modal rounded-lg p-6 w-full max-w-md mx-4">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">
|
<h3 className="text-lg font-semibold text-text-primary">
|
||||||
{t('contracts.uploadDocumentTitle')}
|
{t('contracts.uploadDocumentTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-text-tertiary hover:text-text-secondary"
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -106,7 +106,7 @@ export default function FileUploadModal({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Description Input */}
|
{/* Description Input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||||
{t('contracts.descriptionOptional')}
|
{t('contracts.descriptionOptional')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -114,7 +114,7 @@ export default function FileUploadModal({
|
|||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder={t('contracts.descriptionPlaceholder')}
|
placeholder={t('contracts.descriptionPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
className="w-full px-3 py-2 border border-border-default rounded-md shadow-sm bg-surface-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-interactive-primary focus:border-border-focus"
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,8 +123,8 @@ export default function FileUploadModal({
|
|||||||
<div
|
<div
|
||||||
className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||||
dragActive
|
dragActive
|
||||||
? "border-blue-400 bg-blue-50"
|
? "border-interactive-primary bg-surface-hover"
|
||||||
: "border-gray-300 hover:border-gray-400"
|
: "border-border-default hover:border-border-hover"
|
||||||
} ${uploading ? "opacity-50 pointer-events-none" : ""}`}
|
} ${uploading ? "opacity-50 pointer-events-none" : ""}`}
|
||||||
onDragEnter={handleDrag}
|
onDragEnter={handleDrag}
|
||||||
onDragLeave={handleDrag}
|
onDragLeave={handleDrag}
|
||||||
@@ -146,17 +146,17 @@ export default function FileUploadModal({
|
|||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-sm text-gray-600">{t('contracts.uploading')}</span>
|
<span className="text-sm text-text-secondary">{t('contracts.uploading')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<svg className="w-12 h-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-12 h-12 text-text-tertiary mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-sm font-medium text-gray-900 mb-2">
|
<span className="text-sm font-medium text-text-primary mb-2">
|
||||||
{t('contracts.dropFilesHere')}
|
{t('contracts.dropFilesHere')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500 mb-4">
|
<span className="text-xs text-text-secondary mb-4">
|
||||||
{t('contracts.supportedFiles')}
|
{t('contracts.supportedFiles')}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -154,14 +154,14 @@ export default function ProjectStatusDropdown({
|
|||||||
</button>{" "}
|
</button>{" "}
|
||||||
{/* Status Options Dropdown */}
|
{/* Status Options Dropdown */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-[9999] min-w-[140px]">
|
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg z-[9999] min-w-[140px]">
|
||||||
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
||||||
<button
|
<button
|
||||||
key={statusKey}
|
key={statusKey}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleChange(statusKey);
|
handleChange(statusKey);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last:rounded-b-md"
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors first:rounded-t-md last:rounded-b-md"
|
||||||
>
|
>
|
||||||
<Badge variant={config.variant} size="sm">
|
<Badge variant={config.variant} size="sm">
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ export default function ProjectStatusDropdownDebug({
|
|||||||
|
|
||||||
{/* Simple visible dropdown for debugging */}
|
{/* Simple visible dropdown for debugging */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[140px]">
|
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[140px]">
|
||||||
<div className="bg-yellow-100 p-2 text-xs text-center border-b">
|
<div className="bg-yellow-100 dark:bg-yellow-900 p-2 text-xs text-center border-b dark:border-gray-600">
|
||||||
DEBUG: Project Status Dropdown is visible
|
DEBUG: Project Status Dropdown is visible
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
||||||
@@ -122,7 +122,7 @@ export default function ProjectStatusDropdownDebug({
|
|||||||
console.log("Project Status Option clicked:", statusKey);
|
console.log("Project Status Option clicked:", statusKey);
|
||||||
handleChange(statusKey);
|
handleChange(statusKey);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last:rounded-b-md"
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors first:rounded-t-md last:rounded-b-md"
|
||||||
>
|
>
|
||||||
<Badge variant={config.variant} size="sm">
|
<Badge variant={config.variant} size="sm">
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ export default function ProjectStatusDropdownSimple({
|
|||||||
|
|
||||||
{/* Simple dropdown for debugging */}
|
{/* Simple dropdown for debugging */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[140px]">
|
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[140px]">
|
||||||
<div className="bg-yellow-100 p-2 text-xs text-center border-b">
|
<div className="bg-yellow-100 dark:bg-yellow-900 p-2 text-xs text-center border-b dark:border-gray-600">
|
||||||
DEBUG: ProjectStatus Dropdown is visible
|
DEBUG: ProjectStatus Dropdown is visible
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
||||||
@@ -125,7 +125,7 @@ export default function ProjectStatusDropdownSimple({
|
|||||||
console.log("ProjectStatus Option clicked:", statusKey);
|
console.log("ProjectStatus Option clicked:", statusKey);
|
||||||
handleChange(statusKey);
|
handleChange(statusKey);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last-rounded-b-md"
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors first:rounded-t-md last-rounded-b-md"
|
||||||
>
|
>
|
||||||
<Badge variant={config.variant} size="sm">
|
<Badge variant={config.variant} size="sm">
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ export default function ProjectTasksDashboard() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{tasks.length === 0 ? (
|
{tasks.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
<p className="text-sm">No tasks in this category</p>
|
<p className="text-sm">No tasks in this category</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -278,10 +278,10 @@ export default function ProjectTasksList() {
|
|||||||
return "high";
|
return "high";
|
||||||
};
|
};
|
||||||
const TaskRow = ({ task, showTimeLeft = false, showMaxWait = true }) => (
|
const TaskRow = ({ task, showTimeLeft = false, showMaxWait = true }) => (
|
||||||
<tr className="hover:bg-gray-50 border-b border-gray-200">
|
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-gray-900">{task.task_name}</span>
|
<span className="font-medium text-gray-900 dark:text-gray-100">{task.task_name}</span>
|
||||||
<Badge variant={getPriorityVariant(task.priority)} size="sm">
|
<Badge variant={getPriorityVariant(task.priority)} size="sm">
|
||||||
{t(`tasks.${task.priority}`)}
|
{t(`tasks.${task.priority}`)}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -291,25 +291,25 @@ export default function ProjectTasksList() {
|
|||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/projects/${task.project_id}`}
|
href={`/projects/${task.project_id}`}
|
||||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium"
|
||||||
>
|
>
|
||||||
{task.project_name}
|
{task.project_name}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-600">{task.city || "N/A"}</td>
|
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{task.city || "N/A"}</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-600">
|
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{task.address || "N/A"}
|
{task.address || "N/A"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-600">
|
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{task.assigned_to_name ? (
|
{task.assigned_to_name ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{task.assigned_to_name}</div>
|
<div className="font-medium text-gray-900 dark:text-gray-100">{task.assigned_to_name}</div>
|
||||||
{/* <div className="text-xs text-gray-500">
|
{/* <div className="text-xs text-gray-500">
|
||||||
{task.assigned_to_email}
|
{task.assigned_to_email}
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400 italic">{t("projects.unassigned")}</span>
|
<span className="text-gray-400 dark:text-gray-500 italic">{t("projects.unassigned")}</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
{showTimeLeft && (
|
{showTimeLeft && (
|
||||||
@@ -341,7 +341,7 @@ export default function ProjectTasksList() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td className="px-4 py-3 text-sm text-gray-500">
|
<td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{task.status === "completed" && task.date_completed ? (
|
{task.status === "completed" && task.date_completed ? (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
@@ -386,7 +386,7 @@ export default function ProjectTasksList() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
{showMaxWait && (
|
{showMaxWait && (
|
||||||
<td className="px-4 py-3 text-sm text-gray-500">
|
<td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{task.max_wait_days} {t("tasks.days")}
|
{task.max_wait_days} {t("tasks.days")}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
@@ -416,38 +416,38 @@ export default function ProjectTasksList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full bg-white rounded-lg shadow-sm">
|
<table className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.taskName")}
|
{t("tasks.taskName")}
|
||||||
</th>{" "}
|
</th>{" "}
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.project")}
|
{t("tasks.project")}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("projects.city")}
|
{t("projects.city")}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("projects.address")}
|
{t("projects.address")}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.assignedTo")}
|
{t("tasks.assignedTo")}
|
||||||
</th>
|
</th>
|
||||||
{showTimeLeft && (
|
{showTimeLeft && (
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.daysLeft")}
|
{t("tasks.daysLeft")}
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.dateCreated")}
|
{t("tasks.dateCreated")}
|
||||||
</th>
|
</th>
|
||||||
{showMaxWait && (
|
{showMaxWait && (
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.maxWait")}
|
{t("tasks.maxWait")}
|
||||||
</th>
|
</th>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.actions")}
|
{t("tasks.actions")}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -459,7 +459,7 @@ export default function ProjectTasksList() {
|
|||||||
<tr key={`group-${groupName}`}>
|
<tr key={`group-${groupName}`}>
|
||||||
<td
|
<td
|
||||||
colSpan={colSpan}
|
colSpan={colSpan}
|
||||||
className="px-4 py-2 bg-gray-100 font-medium text-gray-800 text-sm"
|
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 font-medium text-gray-800 dark:text-gray-200 text-sm"
|
||||||
>
|
>
|
||||||
{groupName} ({groupTasks.length} {t("tasks.tasks")})
|
{groupName} ({groupTasks.length} {t("tasks.tasks")})
|
||||||
</td>
|
</td>
|
||||||
@@ -478,7 +478,7 @@ export default function ProjectTasksList() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{filteredTasks.length === 0 && (
|
{filteredTasks.length === 0 && (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
<p>{t("tasks.noTasks")}</p>
|
<p>{t("tasks.noTasks")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -465,42 +465,42 @@ export default function ProjectTasksSection({ projectId }) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-50 border-b border-gray-200">
|
<thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
{t("tasks.taskName")}
|
{t("tasks.taskName")}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
{t("tasks.priority")}
|
{t("tasks.priority")}
|
||||||
</th>{" "}
|
</th>{" "}
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
{t("tasks.maxWait")}
|
{t("tasks.maxWait")}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
{t("tasks.dateStarted")}
|
{t("tasks.dateStarted")}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
{t("tasks.status")}
|
{t("tasks.status")}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-48">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-48">
|
||||||
{t("tasks.actions")}
|
{t("tasks.actions")}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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-600">
|
||||||
{projectTasks.map((task) => (
|
{projectTasks.map((task) => (
|
||||||
<React.Fragment key={task.id}>
|
<React.Fragment key={task.id}>
|
||||||
{/* Main task row */}
|
{/* Main task row */}
|
||||||
<tr className="hover:bg-gray-50 transition-colors">
|
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
<td className="px-4 py-4">
|
<td className="px-4 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h4 className="text-sm font-medium text-gray-900">
|
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{task.task_name}
|
{task.task_name}
|
||||||
</h4>
|
</h4>
|
||||||
{task.description && (
|
{task.description && (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleDescription(task.id)}
|
onClick={() => toggleDescription(task.id)}
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
title="Toggle description"
|
title="Toggle description"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -532,10 +532,10 @@ export default function ProjectTasksSection({ projectId }) {
|
|||||||
{t(`tasks.${task.priority}`)}
|
{t(`tasks.${task.priority}`)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-4 text-sm text-gray-600">
|
<td className="px-4 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{task.max_wait_days} {t("tasks.days")}
|
{task.max_wait_days} {t("tasks.days")}
|
||||||
</td>{" "}
|
</td>{" "}
|
||||||
<td className="px-4 py-4 text-sm text-gray-600">
|
<td className="px-4 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{task.date_started
|
{task.date_started
|
||||||
? formatDate(task.date_started)
|
? formatDate(task.date_started)
|
||||||
: t("tasks.notStarted")}
|
: t("tasks.notStarted")}
|
||||||
|
|||||||
@@ -140,13 +140,13 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
|
||||||
onClick={(e) => e.target === e.currentTarget && onClose()}
|
onClick={(e) => e.target === e.currentTarget && onClose()}
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-lg w-full max-w-4xl mx-4 max-h-[90vh] flex flex-col">
|
<div className="bg-white dark:bg-gray-800 rounded-lg w-full max-w-4xl mx-4 max-h-[90vh] flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-6 border-b">
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-start justify-between mb-4">
|
<div className="flex items-start justify-between mb-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-xl font-semibold text-gray-900">
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{task?.task_name}
|
{task?.task_name}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant={getPriorityVariant(task?.priority)} size="sm">
|
<Badge variant={getPriorityVariant(task?.priority)} size="sm">
|
||||||
@@ -156,13 +156,13 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
{t(`taskStatus.${task?.status}`)}
|
{t(`taskStatus.${task?.status}`)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 mb-3">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
{t("tasks.project")}: <span className="font-medium">{task?.project_name}</span>
|
{t("tasks.project")}: <span className="font-medium">{task?.project_name}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 text-xl font-semibold ml-4"
|
className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 text-xl font-semibold ml-4"
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -170,38 +170,38 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task Details Grid */}
|
{/* Task Details Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||||
{/* Location Information */}
|
{/* Location Information */}
|
||||||
{(task?.city || task?.address) && (
|
{(task?.city || task?.address) && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("projects.locationDetails")}</h4>
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{t("projects.locationDetails")}</h4>
|
||||||
{task?.city && (
|
{task?.city && (
|
||||||
<p className="text-sm text-gray-900">{task.city}</p>
|
<p className="text-sm text-gray-900 dark:text-gray-100">{task.city}</p>
|
||||||
)}
|
)}
|
||||||
{task?.address && (
|
{task?.address && (
|
||||||
<p className="text-xs text-gray-600">{task.address}</p>
|
<p className="text-xs text-gray-600 dark:text-gray-400">{task.address}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Assignment Information */}
|
{/* Assignment Information */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("tasks.assignedTo")}</h4>
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{t("tasks.assignedTo")}</h4>
|
||||||
{task?.assigned_to_name ? (
|
{task?.assigned_to_name ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-900">{task.assigned_to_name}</p>
|
<p className="text-sm text-gray-900 dark:text-gray-100">{task.assigned_to_name}</p>
|
||||||
<p className="text-xs text-gray-600">{task.assigned_to_email}</p>
|
<p className="text-xs text-gray-600 dark:text-gray-400">{task.assigned_to_email}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-500 italic">{t("projects.unassigned")}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 italic">{t("projects.unassigned")}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task Timing */}
|
{/* Task Timing */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("tasks.dateCreated")}</h4>
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{t("tasks.dateCreated")}</h4>
|
||||||
{task?.max_wait_days && (
|
{task?.max_wait_days && (
|
||||||
<p className="text-xs text-gray-600">{t("tasks.maxWait")}: {task.max_wait_days} {t("tasks.days")}</p>
|
<p className="text-xs text-gray-600 dark:text-gray-400">{t("tasks.maxWait")}: {task.max_wait_days} {t("tasks.days")}</p>
|
||||||
)}
|
)}
|
||||||
{(() => {
|
{(() => {
|
||||||
if (task?.status === "completed" && task?.date_completed) {
|
if (task?.status === "completed" && task?.date_completed) {
|
||||||
@@ -236,8 +236,8 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
{/* Task Description */}
|
{/* Task Description */}
|
||||||
{task?.description && (
|
{task?.description && (
|
||||||
<div className="space-y-1 md:col-span-2 lg:col-span-3">
|
<div className="space-y-1 md:col-span-2 lg:col-span-3">
|
||||||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("tasks.description")}</h4>
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{t("tasks.description")}</h4>
|
||||||
<p className="text-sm text-gray-900 leading-relaxed">{task.description}</p>
|
<p className="text-sm text-gray-900 dark:text-gray-100 leading-relaxed">{task.description}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -248,15 +248,15 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-pulse space-y-4">
|
<div className="animate-pulse space-y-4">
|
||||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-3/4"></div>
|
||||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-1/2"></div>
|
||||||
<div className="h-4 bg-gray-200 rounded w-2/3"></div>
|
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-2/3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<h5 className="text-lg font-medium text-gray-900">
|
<h5 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
{t("tasks.comments")}
|
{t("tasks.comments")}
|
||||||
</h5>
|
</h5>
|
||||||
<Badge variant="secondary" size="sm">
|
<Badge variant="secondary" size="sm">
|
||||||
@@ -265,8 +265,8 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{notes.length === 0 ? (
|
{notes.length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
|
||||||
<span className="text-2xl">💬</span>
|
<span className="text-2xl">💬</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-medium mb-1">{t("tasks.noComments")}</p>
|
<p className="text-lg font-medium mb-1">{t("tasks.noComments")}</p>
|
||||||
@@ -279,37 +279,37 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
key={note.note_id}
|
key={note.note_id}
|
||||||
className={`p-4 rounded-lg border flex justify-between items-start transition-colors ${
|
className={`p-4 rounded-lg border flex justify-between items-start transition-colors ${
|
||||||
note.is_system
|
note.is_system
|
||||||
? "bg-blue-50 border-blue-200 hover:bg-blue-100"
|
? "bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/30"
|
||||||
: "bg-white border-gray-200 hover:bg-gray-50 shadow-sm"
|
: "bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 shadow-sm"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{note.is_system ? (
|
{note.is_system ? (
|
||||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full font-medium flex items-center gap-1">
|
<span className="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-300 rounded-full font-medium flex items-center gap-1">
|
||||||
<span>🤖</span>
|
<span>🤖</span>
|
||||||
{t("admin.system")}
|
{t("admin.system")}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded-full font-medium flex items-center gap-1">
|
<span className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-full font-medium flex items-center gap-1">
|
||||||
<span>👤</span>
|
<span>👤</span>
|
||||||
{note.created_by_name || t("userRoles.user")}
|
{note.created_by_name || t("userRoles.user")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-gray-500 font-medium">
|
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||||
{formatDate(note.note_date, {
|
{formatDate(note.note_date, {
|
||||||
includeTime: true,
|
includeTime: true,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-800 leading-relaxed">
|
<p className="text-sm text-gray-800 dark:text-gray-200 leading-relaxed">
|
||||||
{note.note}
|
{note.note}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!note.is_system && (
|
{!note.is_system && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteNote(note.note_id)}
|
onClick={() => handleDeleteNote(note.note_id)}
|
||||||
className="ml-3 p-1 text-red-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
className="ml-3 p-1 text-red-400 hover:text-red-600 dark:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
||||||
title={t("common.delete")}
|
title={t("common.delete")}
|
||||||
>
|
>
|
||||||
<span className="text-sm">🗑️</span>
|
<span className="text-sm">🗑️</span>
|
||||||
@@ -324,11 +324,11 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer - Add new comment */}
|
{/* Footer - Add new comment */}
|
||||||
<div className="p-6 border-t bg-gradient-to-r from-gray-50 to-gray-100">
|
<div className="p-6 border-t border-gray-200 dark:border-gray-700 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-600">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-lg">💬</span>
|
<span className="text-lg">💬</span>
|
||||||
<label className="text-sm font-medium text-gray-700">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{t("tasks.addComment")}
|
{t("tasks.addComment")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,12 +336,12 @@ export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
|||||||
value={newNote}
|
value={newNote}
|
||||||
onChange={(e) => setNewNote(e.target.value)}
|
onChange={(e) => setNewNote(e.target.value)}
|
||||||
placeholder={t("common.addNotePlaceholder")}
|
placeholder={t("common.addNotePlaceholder")}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none shadow-sm"
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none shadow-sm"
|
||||||
rows={3}
|
rows={3}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
<p className="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||||
<span>⌨️</span>
|
<span>⌨️</span>
|
||||||
Naciśnij Ctrl+Enter aby wysłać lub Escape aby zamknąć
|
Naciśnij Ctrl+Enter aby wysłać lub Escape aby zamknąć
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ export default function TaskStatusDropdown({
|
|||||||
</button>{" "}
|
</button>{" "}
|
||||||
{/* Simple dropdown for debugging */}
|
{/* Simple dropdown for debugging */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[120px]">
|
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[120px]">
|
||||||
<div className="bg-yellow-100 p-2 text-xs text-center border-b">
|
<div className="bg-yellow-100 dark:bg-yellow-900 p-2 text-xs text-center border-b dark:border-gray-600">
|
||||||
DEBUG: TaskStatus Dropdown is visible
|
DEBUG: TaskStatus Dropdown is visible
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
||||||
@@ -162,7 +162,7 @@ export default function TaskStatusDropdown({
|
|||||||
console.log("TaskStatus Option clicked:", statusKey);
|
console.log("TaskStatus Option clicked:", statusKey);
|
||||||
handleChange(statusKey);
|
handleChange(statusKey);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last:rounded-b-md"
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors first:rounded-t-md last:rounded-b-md"
|
||||||
>
|
>
|
||||||
<Badge variant={config.variant} size="sm">
|
<Badge variant={config.variant} size="sm">
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ export default function TaskStatusDropdownDebug({
|
|||||||
|
|
||||||
{/* Simple visible dropdown for debugging */}
|
{/* Simple visible dropdown for debugging */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[120px]">
|
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[120px]">
|
||||||
<div className="bg-yellow-100 p-2 text-xs text-center border-b">
|
<div className="bg-yellow-100 dark:bg-yellow-900 p-2 text-xs text-center border-b dark:border-gray-600">
|
||||||
DEBUG: Dropdown is visible
|
DEBUG: Dropdown is visible
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
||||||
@@ -128,7 +128,7 @@ export default function TaskStatusDropdownDebug({
|
|||||||
console.log("Option clicked:", statusKey);
|
console.log("Option clicked:", statusKey);
|
||||||
handleChange(statusKey);
|
handleChange(statusKey);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last:rounded-b-md"
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors first:rounded-t-md last:rounded-b-md"
|
||||||
>
|
>
|
||||||
<Badge variant={config.variant} size="sm">
|
<Badge variant={config.variant} size="sm">
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export default function TaskStatusDropdown({
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className="fixed bg-white border border-gray-200 rounded-md shadow-lg z-[9999] min-w-[140px]"
|
className="fixed bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg z-[9999] min-w-[140px]"
|
||||||
style={{
|
style={{
|
||||||
left: `${dropdownPosition.x}px`,
|
left: `${dropdownPosition.x}px`,
|
||||||
top: `${dropdownPosition.y}px`,
|
top: `${dropdownPosition.y}px`,
|
||||||
@@ -182,7 +182,7 @@ export default function TaskStatusDropdown({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleChange(statusKey);
|
handleChange(statusKey);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last:rounded-b-md"
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors first:rounded-t-md last:rounded-b-md"
|
||||||
>
|
>
|
||||||
<Badge variant={config.variant} size="sm">
|
<Badge variant={config.variant} size="sm">
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|||||||
47
src/components/ThemeProvider.js
Normal file
47
src/components/ThemeProvider.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [isDark, setIsDark] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check for saved theme preference or default to light mode
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||||
|
setIsDark(true);
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const newIsDark = !isDark;
|
||||||
|
setIsDark(newIsDark);
|
||||||
|
|
||||||
|
if (newIsDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ isDark, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
const buttonVariants = {
|
const buttonVariants = {
|
||||||
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
primary: "bg-blue-600 hover:bg-blue-700 text-white dark:bg-blue-700 dark:hover:bg-blue-800",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-gray-100 hover:bg-gray-200 text-gray-900 border border-gray-300",
|
"bg-gray-100 hover:bg-gray-200 text-gray-900 border border-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-100 dark:border-gray-600",
|
||||||
danger: "bg-red-600 hover:bg-red-700 text-white",
|
danger: "bg-red-600 hover:bg-red-700 text-white dark:bg-red-700 dark:hover:bg-red-800",
|
||||||
success: "bg-green-600 hover:bg-green-700 text-white",
|
success: "bg-green-600 hover:bg-green-700 text-white dark:bg-green-700 dark:hover:bg-green-800",
|
||||||
outline: "border border-blue-600 text-blue-600 hover:bg-blue-50",
|
outline: "border border-blue-600 text-blue-600 hover:bg-blue-50 dark:border-blue-400 dark:text-blue-400 dark:hover:bg-blue-900/20",
|
||||||
ghost: "text-gray-600 hover:text-gray-900 hover:bg-gray-50",
|
ghost: "text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800",
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonSizes = {
|
const buttonSizes = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Card = forwardRef(({ children, className = "", ...props }, ref) => {
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`
|
className={`
|
||||||
bg-white rounded-lg border border-gray-200 shadow-sm
|
bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm
|
||||||
${className}
|
${className}
|
||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -21,7 +21,7 @@ Card.displayName = "Card";
|
|||||||
|
|
||||||
const CardHeader = ({ children, className = "" }) => {
|
const CardHeader = ({ children, className = "" }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`px-6 py-4 border-b border-gray-200 ${className}`}>
|
<div className={`px-6 py-4 border-b border-gray-200 dark:border-gray-700 ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -33,7 +33,7 @@ const CardContent = ({ children, className = "" }) => {
|
|||||||
|
|
||||||
const CardFooter = ({ children, className = "" }) => {
|
const CardFooter = ({ children, className = "" }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`px-6 py-4 border-t border-gray-200 ${className}`}>
|
<div className={`px-6 py-4 border-t border-gray-200 dark:border-gray-700 ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Input = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{label && (
|
{label && (
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
@@ -15,9 +15,11 @@ const Input = forwardRef(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
type={type}
|
type={type}
|
||||||
className={`
|
className={`
|
||||||
w-full px-3 py-2 border border-gray-300 rounded-lg
|
w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||||
|
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 focus:border-blue-500
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||||
disabled:bg-gray-50 disabled:text-gray-500
|
disabled:bg-gray-50 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400
|
||||||
${
|
${
|
||||||
error
|
error
|
||||||
? "border-red-300 focus:ring-red-500 focus:border-red-500"
|
? "border-red-300 focus:ring-red-500 focus:border-red-500"
|
||||||
@@ -27,7 +29,7 @@ const Input = forwardRef(
|
|||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,16 +42,17 @@ const Select = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{label && (
|
{label && (
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`
|
className={`
|
||||||
w-full px-3 py-2 border border-gray-300 rounded-lg
|
w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||||
|
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||||
disabled:bg-gray-50 disabled:text-gray-500
|
disabled:bg-gray-50 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400
|
||||||
${
|
${
|
||||||
error
|
error
|
||||||
? "border-red-300 focus:ring-red-500 focus:border-red-500"
|
? "border-red-300 focus:ring-red-500 focus:border-red-500"
|
||||||
@@ -61,7 +64,7 @@ const Select = forwardRef(
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</select>
|
</select>
|
||||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,16 +77,18 @@ const Textarea = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{label && (
|
{label && (
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<textarea
|
<textarea
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`
|
className={`
|
||||||
w-full px-3 py-2 border border-gray-300 rounded-lg
|
w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||||
|
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 focus:border-blue-500
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||||
disabled:bg-gray-50 disabled:text-gray-500
|
disabled:bg-gray-50 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400
|
||||||
${
|
${
|
||||||
error
|
error
|
||||||
? "border-red-300 focus:ring-red-500 focus:border-red-500"
|
? "border-red-300 focus:ring-red-500 focus:border-red-500"
|
||||||
@@ -93,7 +98,7 @@ const Textarea = forwardRef(
|
|||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ const LoadingSpinner = ({ size = "md", className = "" }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`animate-spin rounded-full border-2 border-gray-300 border-t-blue-600 ${sizes[size]} ${className}`}
|
className={`animate-spin rounded-full border-2 border-gray-300 dark:border-gray-600 border-t-blue-600 dark:border-t-blue-500 ${sizes[size]} ${className}`}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoadingCard = ({ className = "" }) => (
|
const LoadingCard = ({ className = "" }) => (
|
||||||
<div className={`animate-pulse ${className}`}>
|
<div className={`animate-pulse ${className}`}>
|
||||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm p-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
||||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-2/3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Link from "next/link";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useSession, signOut } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import LanguageSwitcher from "./LanguageSwitcher";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
@@ -42,12 +41,12 @@ const Navigation = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-white border-b border-gray-200 shadow-sm">
|
<nav className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 shadow-sm">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between h-16">
|
<div className="flex items-center justify-between h-16">
|
||||||
{/* Logo/Brand */}
|
{/* Logo/Brand */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Link href="/projects" className="text-xl font-bold text-gray-900 hover:text-blue-600 transition-colors">
|
<Link href="/projects" className="text-xl font-bold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||||
{t('navigation.projectPanel')}
|
{t('navigation.projectPanel')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,8 +63,8 @@ const Navigation = () => {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
isActive(item.href)
|
isActive(item.href)
|
||||||
? "bg-blue-50 text-blue-700 border border-blue-200"
|
? "bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-700"
|
||||||
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
: "text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -83,12 +82,12 @@ const Navigation = () => {
|
|||||||
<>
|
<>
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="hidden md:flex items-center space-x-3">
|
<div className="hidden md:flex items-center space-x-3">
|
||||||
<div className="text-right">
|
<Link href="/settings" className="text-right hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md px-2 py-1 transition-colors">
|
||||||
<div className="text-sm font-medium text-gray-900">{session.user.name}</div>
|
<div className="text-sm font-medium text-gray-900 dark:text-white">{session.user.name}</div>
|
||||||
{/* <div className="text-xs text-gray-500 capitalize">
|
{/* <div className="text-xs text-gray-500 capitalize">
|
||||||
{t(`userRoles.${session.user.role}`) || session.user.role?.replace('_', ' ')}
|
{t(`userRoles.${session.user.role}`) || session.user.role?.replace('_', ' ')}
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</Link>
|
||||||
{/* <div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
|
{/* <div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
<span className="text-gray-600 font-medium text-sm">
|
<span className="text-gray-600 font-medium text-sm">
|
||||||
{session.user.name?.charAt(0).toUpperCase()}
|
{session.user.name?.charAt(0).toUpperCase()}
|
||||||
@@ -99,17 +98,16 @@ const Navigation = () => {
|
|||||||
{/* Sign Out Button */}
|
{/* Sign Out Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
className="hidden md:block bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-md text-sm font-medium transition-colors"
|
className="hidden md:block bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 px-4 py-2 rounded-md text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{t('navigation.signOut')}
|
{t('navigation.signOut')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LanguageSwitcher />
|
|
||||||
<Link
|
<Link
|
||||||
href="/auth/signin"
|
href="/auth/signin"
|
||||||
className="bg-white hover:bg-gray-50 text-blue-600 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-md"
|
className="bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-blue-600 dark:text-blue-400 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-md border border-gray-200 dark:border-gray-600"
|
||||||
>
|
>
|
||||||
{t('navigation.signIn')}
|
{t('navigation.signIn')}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -120,7 +118,7 @@ const Navigation = () => {
|
|||||||
{session && (
|
{session && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
className="md:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors"
|
className="md:hidden p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
{isMobileMenuOpen ? (
|
{isMobileMenuOpen ? (
|
||||||
@@ -136,7 +134,7 @@ const Navigation = () => {
|
|||||||
|
|
||||||
{/* Mobile Navigation Menu */}
|
{/* Mobile Navigation Menu */}
|
||||||
{session && isMobileMenuOpen && (
|
{session && isMobileMenuOpen && (
|
||||||
<div className="md:hidden mt-4 pb-4 border-t border-gray-200">
|
<div className="md:hidden mt-4 pb-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex flex-col space-y-2 pt-4">
|
<div className="flex flex-col space-y-2 pt-4">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -145,8 +143,8 @@ const Navigation = () => {
|
|||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className={`px-4 py-3 rounded-md text-sm font-medium transition-colors ${
|
className={`px-4 py-3 rounded-md text-sm font-medium transition-colors ${
|
||||||
isActive(item.href)
|
isActive(item.href)
|
||||||
? "bg-blue-50 text-blue-700 border border-blue-200"
|
? "bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-700"
|
||||||
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
: "text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -154,29 +152,31 @@ const Navigation = () => {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Mobile User Info */}
|
{/* Mobile User Info */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 mt-4">
|
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 mt-4">
|
||||||
<div className="flex items-center space-x-3">
|
<Link href="/settings" className="flex items-center space-x-3 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md px-2 py-1 transition-colors flex-1">
|
||||||
<div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
|
<div className="w-8 h-8 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
|
||||||
<span className="text-gray-600 font-medium text-sm">
|
<span className="text-gray-600 dark:text-gray-300 font-medium text-sm">
|
||||||
{session.user.name?.charAt(0).toUpperCase()}
|
{session.user.name?.charAt(0).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-900">{session.user.name}</div>
|
<div className="text-sm font-medium text-gray-900 dark:text-white">{session.user.name}</div>
|
||||||
{/* <div className="text-xs text-gray-500 capitalize">
|
{/* <div className="text-xs text-gray-500 capitalize">
|
||||||
{t(`userRoles.${session.user.role}`) || session.user.role?.replace('_', ' ')}
|
{t(`userRoles.${session.user.role}`) || session.user.role?.replace('_', ' ')}
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsMobileMenuOpen(false);
|
||||||
|
handleSignOut();
|
||||||
|
}}
|
||||||
|
className="bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 px-3 py-1 rounded text-sm transition-colors"
|
||||||
|
>
|
||||||
|
{t('navigation.signOut')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
handleSignOut();
|
|
||||||
}}
|
|
||||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded text-sm transition-colors"
|
|
||||||
>
|
|
||||||
{t('navigation.signOut')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const PageContainer = ({ children, className = "" }) => {
|
const PageContainer = ({ children, className = "" }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen bg-gray-50 ${className}`}>
|
<div className={`min-h-screen bg-gray-50 dark:bg-gray-900 ${className}`}>
|
||||||
<div className="max-w-6xl mx-auto p-6">{children}</div>
|
<div className="max-w-6xl mx-auto p-6">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ const PageHeader = ({ title, description, children, action, className = "" }) =>
|
|||||||
return (
|
return (
|
||||||
<div className={`flex justify-between items-start mb-8 ${className}`}>
|
<div className={`flex justify-between items-start mb-8 ${className}`}>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">{title}</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">{title}</h1>
|
||||||
{description && <p className="text-gray-600 mt-1">{description}</p>}
|
{description && <p className="text-gray-600 dark:text-gray-400 mt-1">{description}</p>}
|
||||||
</div>
|
</div>
|
||||||
{(children || action) && (
|
{(children || action) && (
|
||||||
<div className="ml-6 flex-shrink-0">{action || children}</div>
|
<div className="ml-6 flex-shrink-0">{action || children}</div>
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ export default function ProjectCalendarWidget({
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
Brak aktywnych projektów z terminami
|
Brak aktywnych projektów z terminami
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import dynamic from "next/dynamic";
|
|||||||
const DynamicMap = dynamic(() => import("./LeafletMap"), {
|
const DynamicMap = dynamic(() => import("./LeafletMap"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => (
|
loading: () => (
|
||||||
<div className="w-full h-64 bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
|
<div className="w-full h-64 bg-gray-100 dark:bg-gray-700 animate-pulse rounded-lg flex items-center justify-center">
|
||||||
<span className="text-gray-500">Loading map...</span>
|
<span className="text-gray-500 dark:text-gray-400">Loading map...</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -52,15 +52,15 @@ export default function ProjectMap({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-medium text-gray-700">
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Lokalizacja projektu
|
Lokalizacja projektu
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-xs text-gray-500">No coordinates available</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">No coordinates available</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-full ${mapHeight} rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center`}
|
className={`w-full ${mapHeight} rounded-lg bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 flex items-center justify-center`}
|
||||||
>
|
>
|
||||||
<div className="text-center text-gray-500">
|
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||||
<svg
|
<svg
|
||||||
className="w-8 h-8 mx-auto mb-2"
|
className="w-8 h-8 mx-auto mb-2"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -86,23 +86,23 @@ export default function ProjectMap({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-sm font-medium text-gray-700">
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Lokalizacja projektu
|
Lokalizacja projektu
|
||||||
</h3>
|
</h3>
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full border border-white shadow-sm"
|
className="w-3 h-3 rounded-full border border-white dark:border-gray-800 shadow-sm"
|
||||||
style={{ backgroundColor: statusInfo.color }}
|
style={{ backgroundColor: statusInfo.color }}
|
||||||
title={`Status: ${statusInfo.label}`}
|
title={`Status: ${statusInfo.label}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
{showLayerControl && (
|
{showLayerControl && (
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{/* Use the layer control (📚) to switch map views */}
|
{/* Use the layer control (📚) to switch map views */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-full ${mapHeight} rounded-lg border border-gray-200`}
|
className={`w-full ${mapHeight} rounded-lg border border-gray-200 dark:border-gray-600`}
|
||||||
>
|
>
|
||||||
<DynamicMap
|
<DynamicMap
|
||||||
center={[coords.lat, coords.lng]}
|
center={[coords.lat, coords.lng]}
|
||||||
@@ -113,8 +113,8 @@ export default function ProjectMap({
|
|||||||
color: statusInfo.color,
|
color: statusInfo.color,
|
||||||
popup: (
|
popup: (
|
||||||
<div className="min-w-48">
|
<div className="min-w-48">
|
||||||
<div className="mb-2 pb-2 border-b border-gray-200">
|
<div className="mb-2 pb-2 border-b border-gray-200 dark:border-gray-600">
|
||||||
<h4 className="font-semibold text-gray-900 mb-1">
|
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
||||||
{projectName || "Project Location"}
|
{projectName || "Project Location"}
|
||||||
</h4>
|
</h4>
|
||||||
<span
|
<span
|
||||||
@@ -124,10 +124,10 @@ export default function ProjectMap({
|
|||||||
{statusInfo.label}
|
{statusInfo.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-gray-400"
|
className="w-4 h-4 text-gray-400 dark:text-gray-500"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ const SearchBar = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6 ${className}`}
|
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-6 ${className}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<svg
|
<svg
|
||||||
className="h-5 w-5 text-gray-400"
|
className="h-5 w-5 text-gray-400 dark:text-gray-500"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -38,7 +38,7 @@ const SearchBar = ({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={onSearchChange}
|
onChange={onSearchChange}
|
||||||
className="pl-10 pr-10 w-full border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
className="pl-10 pr-10 w-full border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<button
|
<button
|
||||||
@@ -48,7 +48,7 @@ const SearchBar = ({
|
|||||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="h-5 w-5 text-gray-400 hover:text-gray-600 transition-colors"
|
className="h-5 w-5 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -67,12 +67,12 @@ const SearchBar = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{searchTerm && resultsCount !== null && (
|
{searchTerm && resultsCount !== null && (
|
||||||
<div className="mt-3 pt-3 border-t border-gray-100">
|
<div className="mt-3 pt-3 border-t border-gray-100 dark:border-gray-700">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Found{" "}
|
Found{" "}
|
||||||
<span className="font-medium text-gray-900">{resultsCount}</span>{" "}
|
<span className="font-medium text-gray-900 dark:text-gray-100">{resultsCount}</span>{" "}
|
||||||
{resultsText} matching
|
{resultsText} matching
|
||||||
<span className="font-medium text-blue-600">
|
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||||
{" "}
|
{" "}
|
||||||
"{searchTerm}"
|
"{searchTerm}"
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const LoadingSpinner = ({ size = "md" }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
className={`${sizeClasses[size]} animate-spin rounded-full border-2 border-gray-300 border-t-blue-600`}
|
className={`${sizeClasses[size]} animate-spin rounded-full border-2 border-gray-300 dark:border-gray-600 border-t-blue-600 dark:border-t-blue-500`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -24,7 +24,7 @@ const LoadingState = ({ message = "Loading...", className = "" }) => {
|
|||||||
<div className={`flex items-center justify-center py-12 ${className}`}>
|
<div className={`flex items-center justify-center py-12 ${className}`}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<LoadingSpinner size="lg" />
|
<LoadingSpinner size="lg" />
|
||||||
<p className="text-gray-600 mt-4">{message}</p>
|
<p className="text-gray-600 dark:text-gray-400 mt-4">{message}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -42,7 +42,7 @@ const EmptyState = ({
|
|||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardContent className="text-center py-12">
|
<CardContent className="text-center py-12">
|
||||||
<div className="text-gray-400 mb-4">
|
<div className="text-gray-400 dark:text-gray-500 mb-4">
|
||||||
{icon || (
|
{icon || (
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 mx-auto"
|
className="w-16 h-16 mx-auto"
|
||||||
@@ -57,8 +57,8 @@ const EmptyState = ({
|
|||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">{title}</h3>
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">{title}</h3>
|
||||||
{description && <p className="text-gray-500 mb-6">{description}</p>}
|
{description && <p className="text-gray-500 dark:text-gray-400 mb-6">{description}</p>}
|
||||||
{actionLabel && (actionHref || onAction) && (
|
{actionLabel && (actionHref || onAction) && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -83,7 +83,7 @@ const ErrorState = ({
|
|||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardContent className="text-center py-12">
|
<CardContent className="text-center py-12">
|
||||||
<div className="text-red-400 mb-4">
|
<div className="text-red-400 dark:text-red-500 mb-4">
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 mx-auto"
|
className="w-16 h-16 mx-auto"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -98,8 +98,8 @@ const ErrorState = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">{title}</h3>
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">{title}</h3>
|
||||||
<p className="text-gray-500 mb-6">{description}</p>
|
<p className="text-gray-500 dark:text-gray-400 mb-6">{description}</p>
|
||||||
{onRetry && (
|
{onRetry && (
|
||||||
<Button variant="primary" onClick={onRetry}>
|
<Button variant="primary" onClick={onRetry}>
|
||||||
Try Again
|
Try Again
|
||||||
|
|||||||
27
src/components/ui/ThemeToggle.js
Normal file
27
src/components/ui/ThemeToggle.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme } from "@/components/ThemeProvider";
|
||||||
|
|
||||||
|
export default function ThemeToggle() {
|
||||||
|
const { isDark, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||||
|
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||||
|
>
|
||||||
|
{isDark ? (
|
||||||
|
// Sun icon for light mode
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
// Moon icon for dark mode
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ export default function Tooltip({ children, content, className = "" }) {
|
|||||||
const tooltip = isVisible && (
|
const tooltip = isVisible && (
|
||||||
<div
|
<div
|
||||||
ref={tooltipRef}
|
ref={tooltipRef}
|
||||||
className={`fixed z-50 px-3 py-2 text-sm bg-gray-900 text-white rounded-lg shadow-lg border max-w-sm ${className}`}
|
className={`fixed z-50 px-3 py-2 text-sm bg-gray-900 dark:bg-gray-700 text-white rounded-lg shadow-lg border border-gray-700 dark:border-gray-600 max-w-sm ${className}`}
|
||||||
style={{
|
style={{
|
||||||
top: position.top,
|
top: position.top,
|
||||||
left: position.left,
|
left: position.left,
|
||||||
@@ -75,7 +75,7 @@ export default function Tooltip({ children, content, className = "" }) {
|
|||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
{/* Arrow pointing down */}
|
{/* Arrow pointing down */}
|
||||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -526,6 +526,16 @@ const translations = {
|
|||||||
createdAt: "Data utworzenia",
|
createdAt: "Data utworzenia",
|
||||||
noUsers: "Brak użytkowników",
|
noUsers: "Brak użytkowników",
|
||||||
system: "System"
|
system: "System"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
settings: {
|
||||||
|
title: "Ustawienia",
|
||||||
|
appearance: "Wygląd",
|
||||||
|
theme: "Motyw",
|
||||||
|
themeDescription: "Wybierz preferowany motyw",
|
||||||
|
language: "Język",
|
||||||
|
languageDescription: "Wybierz preferowany język"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -984,6 +994,16 @@ const translations = {
|
|||||||
createdAt: "Created At",
|
createdAt: "Created At",
|
||||||
noUsers: "No users",
|
noUsers: "No users",
|
||||||
system: "System"
|
system: "System"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
settings: {
|
||||||
|
title: "Settings",
|
||||||
|
appearance: "Appearance",
|
||||||
|
theme: "Theme",
|
||||||
|
themeDescription: "Choose your preferred theme",
|
||||||
|
language: "Language",
|
||||||
|
languageDescription: "Select your preferred language"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,63 @@ export default {
|
|||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
// Base colors using CSS variables
|
||||||
background: "var(--background)",
|
background: "var(--background)",
|
||||||
foreground: "var(--foreground)",
|
foreground: "var(--foreground)",
|
||||||
|
|
||||||
|
// Surface colors
|
||||||
|
surface: {
|
||||||
|
primary: "var(--surface-primary)",
|
||||||
|
secondary: "var(--surface-secondary)",
|
||||||
|
tertiary: "var(--surface-tertiary)",
|
||||||
|
modal: "var(--surface-modal)",
|
||||||
|
card: "var(--surface-card)",
|
||||||
|
hover: "var(--surface-hover)",
|
||||||
|
active: "var(--surface-active)",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
text: {
|
||||||
|
primary: "var(--text-primary)",
|
||||||
|
secondary: "var(--text-secondary)",
|
||||||
|
tertiary: "var(--text-tertiary)",
|
||||||
|
inverse: "var(--text-inverse)",
|
||||||
|
muted: "var(--text-muted)",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Border colors
|
||||||
|
border: {
|
||||||
|
default: "var(--border-default)",
|
||||||
|
hover: "var(--border-hover)",
|
||||||
|
focus: "var(--border-focus)",
|
||||||
|
divider: "var(--border-divider)",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Interactive colors
|
||||||
|
interactive: {
|
||||||
|
primary: "var(--interactive-primary)",
|
||||||
|
"primary-hover": "var(--interactive-primary-hover)",
|
||||||
|
secondary: "var(--interactive-secondary)",
|
||||||
|
"secondary-hover": "var(--interactive-secondary-hover)",
|
||||||
|
danger: "var(--interactive-danger)",
|
||||||
|
"danger-hover": "var(--interactive-danger-hover)",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Status colors
|
||||||
|
status: {
|
||||||
|
success: "var(--status-success)",
|
||||||
|
warning: "var(--status-warning)",
|
||||||
|
error: "var(--status-error)",
|
||||||
|
info: "var(--status-info)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'custom': 'var(--shadow-default)',
|
||||||
|
'custom-hover': 'var(--shadow-hover)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user