diff --git a/CHANGELOG.md b/CHANGELOG.md index 39db477..2f64383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Adjusted dark mode color palette to use lighter background tones for better readability and reduced eye strain (#52) +- Replaced project card grid with sortable data table on Home page for better handling of large project lists - Improved pod naming: Orchard pods now named `orchard-{env}-server-*` for clarity (#51) ### Fixed diff --git a/frontend/src/components/DataTable.css b/frontend/src/components/DataTable.css index 1716990..e7d5dfe 100644 --- a/frontend/src/components/DataTable.css +++ b/frontend/src/components/DataTable.css @@ -98,3 +98,58 @@ text-overflow: ellipsis; white-space: nowrap; } + +/* Clickable rows */ +.data-table__row--clickable { + cursor: pointer; +} + +.data-table__row--clickable:hover { + background: var(--bg-hover); +} + +/* Responsive table wrapper */ +.data-table--responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.data-table--responsive table { + min-width: 800px; +} + +/* Cell with name and icon */ +.data-table .cell-name { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + color: var(--text-primary); +} + +.data-table .cell-name:hover { + color: var(--accent-primary); +} + +/* Date cells */ +.data-table .cell-date { + color: var(--text-tertiary); + font-size: 0.8125rem; + white-space: nowrap; +} + +/* Description cell */ +.data-table .cell-description { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-secondary); + font-size: 0.875rem; +} + +/* Owner cell */ +.data-table .cell-owner { + color: var(--text-secondary); + font-size: 0.875rem; +} diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx index dc8e048..23d5896 100644 --- a/frontend/src/components/DataTable.tsx +++ b/frontend/src/components/DataTable.tsx @@ -18,6 +18,7 @@ interface DataTableProps { onSort?: (key: string) => void; sortKey?: string; sortOrder?: 'asc' | 'desc'; + onRowClick?: (item: T) => void; } export function DataTable({ @@ -29,6 +30,7 @@ export function DataTable({ onSort, sortKey, sortOrder, + onRowClick, }: DataTableProps) { if (data.length === 0) { return ( @@ -71,7 +73,11 @@ export function DataTable({ {data.map((item) => ( - + onRowClick?.(item)} + className={onRowClick ? 'data-table__row--clickable' : ''} + > {columns.map((column) => ( {column.render(item)} diff --git a/frontend/src/pages/Home.css b/frontend/src/pages/Home.css index f5891d8..6a2f176 100644 --- a/frontend/src/pages/Home.css +++ b/frontend/src/pages/Home.css @@ -1,6 +1,6 @@ /* Page Layout */ .home { - max-width: 1000px; + max-width: 1200px; margin: 0 auto; } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 6d45faf..fb6aeab 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback } from 'react'; -import { Link, useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams, useNavigate } from 'react-router-dom'; import { Project, PaginatedResponse } from '../types'; import { listProjects, createProject } from '../api'; import { Badge } from '../components/Badge'; -import { SortDropdown, SortOption } from '../components/SortDropdown'; +import { DataTable } from '../components/DataTable'; import { FilterDropdown, FilterOption } from '../components/FilterDropdown'; import { FilterChip, FilterChipGroup } from '../components/FilterChip'; import { Pagination } from '../components/Pagination'; @@ -20,12 +20,6 @@ function LockIcon() { ); } -const SORT_OPTIONS: SortOption[] = [ - { value: 'name', label: 'Name' }, - { value: 'created_at', label: 'Created' }, - { value: 'updated_at', label: 'Updated' }, -]; - const VISIBILITY_OPTIONS: FilterOption[] = [ { value: '', label: 'All Projects' }, { value: 'public', label: 'Public Only' }, @@ -34,6 +28,7 @@ const VISIBILITY_OPTIONS: FilterOption[] = [ function Home() { const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); const { user } = useAuth(); const [projectsData, setProjectsData] = useState | null>(null); @@ -101,8 +96,10 @@ function Home() { } } - const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => { - updateParams({ sort: newSort, order: newOrder, page: '1' }); + const handleSortChange = (columnKey: string) => { + // Toggle order if clicking the same column, otherwise default to asc + const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc'; + updateParams({ sort: columnKey, order: newOrder, page: '1' }); }; const handleVisibilityChange = (value: string) => { @@ -189,7 +186,6 @@ function Home() { value={visibility} onChange={handleVisibilityChange} /> - {hasActiveFilters && ( @@ -204,69 +200,106 @@ function Home() { )} - {projects.length === 0 ? ( -
- {hasActiveFilters ? ( -

No projects match your filters. Try adjusting your search.

- ) : ( -

No projects yet. Create your first project to get started!

- )} -
- ) : ( - <> -
- {projects.map((project) => ( - -

+
+ project.id} + onRowClick={(project) => navigate(`/project/${project.name}`)} + onSort={handleSortChange} + sortKey={sort} + sortOrder={order} + emptyMessage={ + hasActiveFilters + ? 'No projects match your filters. Try adjusting your search.' + : 'No projects yet. Create your first project to get started!' + } + columns={[ + { + key: 'name', + header: 'Name', + sortable: true, + render: (project) => ( + {!project.is_public && } {project.name} -

- {project.description &&

{project.description}

} -
-
- - {project.is_public ? 'Public' : 'Private'} - - {user && project.access_level && ( - + ), + }, + { + key: 'description', + header: 'Description', + className: 'cell-description', + render: (project) => project.description || '—', + }, + { + key: 'visibility', + header: 'Visibility', + render: (project) => ( + + {project.is_public ? 'Public' : 'Private'} + + ), + }, + { + key: 'created_by', + header: 'Owner', + className: 'cell-owner', + render: (project) => project.created_by, + }, + ...(user + ? [ + { + key: 'access_level', + header: 'Access', + render: (project: Project) => + project.access_level ? ( + - {project.is_owner ? 'Owner' : project.access_level.charAt(0).toUpperCase() + project.access_level.slice(1)} - - )} -
-
- Created {new Date(project.created_at).toLocaleDateString()} - {project.updated_at !== project.created_at && ( - Updated {new Date(project.updated_at).toLocaleDateString()} - )} -
-
-
- by {project.created_by} -
- - ))} -
+ : project.access_level === 'admin' + ? 'success' + : project.access_level === 'write' + ? 'info' + : 'default' + } + > + {project.is_owner + ? 'Owner' + : project.access_level.charAt(0).toUpperCase() + project.access_level.slice(1)} + + ) : ( + '—' + ), + }, + ] + : []), + { + key: 'created_at', + header: 'Created', + sortable: true, + className: 'cell-date', + render: (project) => new Date(project.created_at).toLocaleDateString(), + }, + { + key: 'updated_at', + header: 'Updated', + sortable: true, + className: 'cell-date', + render: (project) => new Date(project.updated_at).toLocaleDateString(), + }, + ]} + /> + - {pagination && pagination.total_pages > 1 && ( - - )} - + {pagination && pagination.total_pages > 1 && ( + )} );