feat: update ProjectListPage layout and button styles for improved UI/UX

This commit is contained in:
2025-10-09 20:56:55 +02:00
parent ac5fedb61a
commit ec5b60d478
2 changed files with 192 additions and 131 deletions

View File

@@ -183,73 +183,12 @@ export default function ProjectListPage() {
return ( return (
<PageContainer> <PageContainer>
<PageHeader title={t('projects.title')} description={t('projects.subtitle')}> <PageHeader title={t('projects.title')} description={t('projects.subtitle')}>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-2 sm:gap-2"> <div className="flex items-center gap-2 sm:gap-3">
<Link href="/projects/map" className="w-full sm:w-auto"> {/* Primary Action - New Project */}
<Button variant="outline" size="lg" className="w-full"> <Link href="/projects/new" className="flex-shrink-0">
<Button variant="primary" size="lg">
<svg <svg
className="w-5 h-5 mr-2" className="w-5 h-5 sm:mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
/>
</svg>
{t('projects.mapView') || 'Widok mapy'}
</Button>
</Link>
{session?.user && (
<Button
variant={filters.mine ? "primary" : "outline"}
size="lg"
className="w-full sm:w-auto"
onClick={() => handleFilterChange('mine', !filters.mine)}
>
<svg
className="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{t('projects.mine') || 'Moje'}
</Button>
)}
<Button
variant="outline"
size="lg"
className="w-full sm:w-auto"
onClick={handleExportExcel}
>
<svg
className="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
{t('projects.exportExcel') || 'Export to Excel'}
</Button>
<Link href="/projects/new" className="w-full sm:w-auto">
<Button variant="primary" size="lg" className="w-full">
<svg
className="w-5 h-5 mr-2"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -261,9 +200,55 @@ export default function ProjectListPage() {
d="M12 4v16m8-8H4" d="M12 4v16m8-8H4"
/> />
</svg> </svg>
{t('projects.newProject')} <span className="hidden sm:inline">{t('projects.newProject')}</span>
</Button> </Button>
</Link> </Link>
{/* Spacer */}
<div className="flex-1"></div>
{/* Utility Actions - Icon Buttons */}
<div className="flex items-center gap-2">
<Link href="/projects/map" title={t('projects.mapView') || 'Widok mapy'}>
<Button variant="ghost" size="icon" className="h-10 w-10">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
/>
</svg>
</Button>
</Link>
<Button
variant="ghost"
size="icon"
className="h-10 w-10"
onClick={handleExportExcel}
title={t('projects.exportExcel') || 'Export to Excel'}
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</Button>
</div>
</div> </div>
</PageHeader> </PageHeader>
@@ -380,6 +365,34 @@ export default function ProjectListPage() {
))} ))}
</select> </select>
</div> </div>
{session?.user && (
<button
onClick={() => handleFilterChange('mine', !filters.mine)}
className={`
inline-flex items-center space-x-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all
${filters.mine
? 'bg-blue-100 text-blue-700 border-2 border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'
: 'bg-gray-100 text-gray-700 border-2 border-gray-200 hover:border-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
}
`}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span>{t('projects.mine') || 'Tylko moje'}</span>
</button>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -387,72 +400,119 @@ export default function ProjectListPage() {
{/* Desktop always visible content */} {/* Desktop always visible content */}
<div className="hidden md:block"> <div className="hidden md:block">
<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-3">
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2"> {/* Filters row */}
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap"> <div className="flex flex-wrap gap-4 items-center">
{t('common.status') || 'Status'}: <div className="flex items-center space-x-2">
</label> <label className="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
<select {t('common.status') || 'Status'}:
value={filters.status} </label>
onChange={(e) => handleFilterChange('status', e.target.value)} <select
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" value={filters.status}
> onChange={(e) => handleFilterChange('status', e.target.value)}
<option value="all">{t('common.all')}</option> className="px-3 py-1 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"
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option> >
<option value="registered">{t('projectStatus.registered')}</option> <option value="all">{t('common.all')}</option>
<option value="in_progress_design">{t('projectStatus.in_progress_design')}</option> <option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
<option value="in_progress_construction">{t('projectStatus.in_progress_construction')}</option> <option value="registered">{t('projectStatus.registered')}</option>
<option value="fulfilled">{t('projectStatus.fulfilled')}</option> <option value="in_progress_design">{t('projectStatus.in_progress_design')}</option>
</select> <option value="in_progress_construction">{t('projectStatus.in_progress_construction')}</option>
<option value="fulfilled">{t('projectStatus.fulfilled')}</option>
</select>
</div>
<div className="flex items-center space-x-2">
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
{t('common.type') || 'Typ'}:
</label>
<select
value={filters.type}
onChange={(e) => handleFilterChange('type', e.target.value)}
className="px-3 py-1 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"
>
<option value="all">{t('common.all')}</option>
<option value="design">{t('projectType.design')}</option>
<option value="construction">{t('projectType.construction')}</option>
<option value="design+construction">{t('projectType.design+construction')}</option>
</select>
</div>
<div className="flex items-center space-x-2">
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
{t('contracts.customer') || 'Klient'}:
</label>
<select
value={filters.customer}
onChange={(e) => handleFilterChange('customer', e.target.value)}
className="px-3 py-1 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"
>
<option value="all">{t('common.all')}</option>
{customers.map((customer) => (
<option key={customer} value={customer}>
{customer}
</option>
))}
</select>
</div>
{session?.user && (
<button
onClick={() => handleFilterChange('mine', !filters.mine)}
className={`
inline-flex items-center space-x-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all
${filters.mine
? 'bg-blue-100 text-blue-700 border-2 border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'
: 'bg-gray-100 text-gray-700 border-2 border-gray-200 hover:border-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
}
`}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span>{t('projects.mine') || 'Tylko moje'}</span>
</button>
)}
</div> </div>
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2"> {/* Results and clear button row */}
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap"> <div className="flex items-center justify-between pt-2 border-t border-gray-100">
{t('common.type') || 'Typ'}: <div className="text-sm text-gray-500">
</label> {t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
<select </div>
value={filters.type}
onChange={(e) => handleFilterChange('type', e.target.value)}
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="design">{t('projectType.design')}</option>
<option value="construction">{t('projectType.construction')}</option>
<option value="design+construction">{t('projectType.design+construction')}</option>
</select>
</div>
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2"> {(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm) && (
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 md:text-xs md:whitespace-nowrap"> <Button
{t('contracts.customer') || 'Klient'}: variant="ghost"
</label> size="sm"
<select onClick={clearAllFilters}
value={filters.customer} className="text-xs"
onChange={(e) => handleFilterChange('customer', e.target.value)} >
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" <svg
> className="w-4 h-4 mr-1"
<option value="all">{t('common.all')}</option> fill="none"
{customers.map((customer) => ( stroke="currentColor"
<option key={customer} value={customer}> viewBox="0 0 24 24"
{customer} >
</option> <path
))} strokeLinecap="round"
</select> strokeLinejoin="round"
</div> strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || searchTerm) && ( />
<Button </svg>
variant="outline" {t('common.clearAllFilters') || 'Wyczyść wszystkie filtry'}
size="sm" </Button>
onClick={clearAllFilters} )}
className="text-xs self-start md:self-auto"
>
{t('common.clearAllFilters') || 'Wyczyść wszystkie filtry'}
</Button>
)}
<div className="text-sm text-gray-500 md:ml-auto md:text-right">
{t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,6 +16,7 @@ const buttonSizes = {
sm: "px-3 py-1.5 text-sm", sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm", md: "px-4 py-2 text-sm",
lg: "px-6 py-3 text-base", lg: "px-6 py-3 text-base",
icon: "p-2",
}; };
const Button = forwardRef( const Button = forwardRef(