Refactor Project Tasks and Task Templates pages with new UI components

- Introduced PageContainer and PageHeader components for consistent layout.
- Added SearchBar and FilterBar components for improved task filtering and searching.
- Implemented LoadingState component for better loading indication.
- Updated ProjectTasksPage to utilize new components and enhance user experience.
- Refactored TaskTemplatesPage to use PageContainer and PageHeader for better structure.
- Created FilterBar component to manage filter options dynamically.
- Added SearchBar component for searching tasks with clear functionality.
- Introduced States component for loading and error states.
This commit is contained in:
Chop
2025-06-02 23:41:49 +02:00
parent fb00c1d2d6
commit a1261b2169
11 changed files with 1283 additions and 1176 deletions

View File

@@ -5,6 +5,11 @@ import Link from "next/link";
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import SearchBar from "@/components/ui/SearchBar";
import FilterBar from "@/components/ui/FilterBar";
import { LoadingState } from "@/components/ui/States";
export default function ContractsMainPage() {
const [contracts, setContracts] = useState([]);
@@ -162,37 +167,75 @@ export default function ContractsMainPage() {
};
if (loading) {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-2 text-gray-600">Ładowanie umów...</p>
</div>
</div>
<PageContainer>
<PageHeader
title="Umowy"
description="Zarządzaj swoimi umowami i kontraktami"
>
<Link href="/contracts/new">
<Button variant="primary" size="lg">
<span className="mr-2"></span>
Nowa umowa
</Button>
</Link>
</PageHeader>
<LoadingState message="Ładowanie umów..." />
</PageContainer>
);
}
const stats = getContractStats();
const filterOptions = [
{
label: "Status",
value: statusFilter,
onChange: (e) => setStatusFilter(e.target.value),
options: [
{ value: "all", label: "Wszystkie" },
{ value: "active", label: "Aktywne" },
{ value: "completed", label: "Zakończone" },
{ value: "no_end_date", label: "Bez daty końca" },
],
},
{
label: "Sortuj według",
value: sortBy,
onChange: (e) => setSortBy(e.target.value),
options: [
{ value: "contract_number", label: "Numer umowy" },
{ value: "contract_name", label: "Nazwa umowy" },
{ value: "customer", label: "Klient" },
{ value: "start_date", label: "Data rozpoczęcia" },
{ value: "finish_date", label: "Data zakończenia" },
],
},
{
label: "Kolejność",
value: sortOrder,
onChange: (e) => setSortOrder(e.target.value),
options: [
{ value: "asc", label: "Rosnąco" },
{ value: "desc", label: "Malejąco" },
],
},
];
return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900">Umowy</h1>
<p className="text-gray-600 mt-1">
Zarządzaj swoimi umowami i kontraktami
</p>
</div>{" "}
<Link
href="/contracts/new"
className="inline-flex items-center justify-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
<PageContainer>
<PageHeader
title="Umowy"
description="Zarządzaj swoimi umowami i kontraktami"
>
<Link href="/contracts/new">
<Button variant="primary" size="lg">
<span className="mr-2"></span>
Nowa umowa
</Link>
</div>
</Button>
</Link>{" "}
</PageHeader>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<Card>
<CardContent className="p-4">
<div className="flex items-center">
@@ -302,93 +345,17 @@ export default function ContractsMainPage() {
</p>
</div>
</div>
</CardContent>
</CardContent>{" "}
</Card>
</div>
{/* Filters and Search */}
<Card>
<CardContent className="p-4">
<div className="flex flex-col lg:flex-row gap-4">
{/* Search */}
<div className="flex-1">
<label
htmlFor="search"
className="block text-sm font-medium text-gray-700 mb-1"
>
Wyszukaj
</label>
<input
id="search"
type="text"
placeholder="Szukaj po numerze, nazwie, kliencie lub inwestorze..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
<SearchBar
searchTerm={searchTerm}
onSearchChange={handleSearchChange}
placeholder="Szukaj umów po numerze, nazwie, kliencie lub inwestorze..."
resultsCount={filteredContracts.length}
resultsText="umów"
/>
</div>
{/* Status Filter */}
<div className="w-full lg:w-48">
<label
htmlFor="status"
className="block text-sm font-medium text-gray-700 mb-1"
>
Status
</label>
<select
id="status"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Wszystkie</option>
<option value="active">Aktywne</option>
<option value="completed">Zakończone</option>
<option value="no_end_date">W trakcie</option>
</select>
</div>
{/* Sort */}
<div className="w-full lg:w-48">
<label
htmlFor="sort"
className="block text-sm font-medium text-gray-700 mb-1"
>
Sortuj według
</label>
<select
id="sort"
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [field, order] = e.target.value.split("-");
setSortBy(field);
setSortOrder(order);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="contract_number-asc">Numer umowy (A-Z)</option>
<option value="contract_number-desc">Numer umowy (Z-A)</option>
<option value="contract_name-asc">Nazwa (A-Z)</option>
<option value="contract_name-desc">Nazwa (Z-A)</option>
<option value="customer-asc">Klient (A-Z)</option>
<option value="customer-desc">Klient (Z-A)</option>
<option value="date_signed-desc">
Data zawarcia (najnowsze)
</option>
<option value="date_signed-asc">
Data zawarcia (najstarsze)
</option>
<option value="finish_date-desc">
Data zakończenia (najnowsze)
</option>
<option value="finish_date-asc">
Data zakończenia (najstarsze)
</option>
</select>
</div>
</div>
</CardContent>
</Card>{" "}
<FilterBar filters={filterOptions} className="mb-6" />{" "}
{/* Contracts List */}
{filteredContracts.length === 0 ? (
<Card>
@@ -624,9 +591,9 @@ export default function ContractsMainPage() {
Wyczyść filtry
</button>
)}
</p>
</p>{" "}
</div>
)}
</div>
</PageContainer>
);
}

View File

@@ -5,6 +5,9 @@ import Link from "next/link";
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import { LoadingState } from "@/components/ui/States";
export default function Home() {
const [stats, setStats] = useState({
@@ -44,15 +47,16 @@ export default function Home() {
}, []);
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600 mt-1">
Overview of your projects and tasks
</p>
</div>
<PageContainer>
<PageHeader
title="Dashboard"
description="Overview of your projects and tasks"
/>
{loading ? (
<LoadingState message="Loading dashboard data..." />
) : (
<>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card>
@@ -354,7 +358,8 @@ export default function Home() {
</CardContent>
</Card>
</div>
</div>
</div>
</>
)}
</PageContainer>
);
}

View File

@@ -9,6 +9,8 @@ import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
import Link from "next/link";
import { differenceInCalendarDays, parseISO } from "date-fns";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
export default async function ProjectViewPage({ params }) {
const project = getProjectWithContract(params.id);
@@ -17,10 +19,9 @@ export default async function ProjectViewPage({ params }) {
parseISO(project.finish_date),
new Date()
);
if (!project) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<PageContainer>
<Card>
<CardContent className="text-center py-8">
<p className="text-red-600 text-lg">Project not found.</p>
@@ -29,7 +30,7 @@ export default async function ProjectViewPage({ params }) {
</Link>
</CardContent>
</Card>
</div>
</PageContainer>
);
}
@@ -38,11 +39,20 @@ export default async function ProjectViewPage({ params }) {
if (days <= 7) return "warning";
return "success";
};
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<PageContainer>
<PageHeader
title={project.project_name}
description={`${project.city}${project.address}`}
action={
<div className="flex items-center gap-3">
<Badge variant={getDeadlineVariant(daysRemaining)} size="md">
{daysRemaining === 0
? "Due Today"
: daysRemaining > 0
? `${daysRemaining} days left`
: `${Math.abs(daysRemaining)} days overdue`}
</Badge>
<Link href="/projects">
<Button variant="outline" size="sm">
<svg
@@ -65,19 +75,8 @@ export default async function ProjectViewPage({ params }) {
<Button variant="secondary">Edit Project</Button>
</Link>
</div>
<div className="flex items-center gap-4 mb-6">
<h1 className="text-3xl font-bold text-gray-900">
{project.project_name}
</h1>
<Badge variant={getDeadlineVariant(daysRemaining)} size="md">
{daysRemaining === 0
? "Due Today"
: daysRemaining > 0
? `${daysRemaining} days left`
: `${Math.abs(daysRemaining)} days overdue`}
</Badge>
</div>
}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
@@ -101,9 +100,7 @@ export default async function ProjectViewPage({ params }) {
<p className="text-gray-900">{project.address}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Plot
</span>
<span className="text-sm font-medium text-gray-500">Plot</span>
<p className="text-gray-900">{project.plot}</p>
</div>
<div>
@@ -113,9 +110,7 @@ export default async function ProjectViewPage({ params }) {
<p className="text-gray-900">{project.district}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Unit
</span>
<span className="text-sm font-medium text-gray-500">Unit</span>
<p className="text-gray-900">{project.unit}</p>
</div>
<div>
@@ -136,16 +131,12 @@ export default async function ProjectViewPage({ params }) {
</div>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Contact
</span>
<span className="text-sm font-medium text-gray-500">Contact</span>
<p className="text-gray-900">{project.contact}</p>
</div>
{project.notes && (
<div>
<span className="text-sm font-medium text-gray-500">
Notes
</span>
<span className="text-sm font-medium text-gray-500">Notes</span>
<p className="text-gray-900">{project.notes}</p>
</div>
)}
@@ -228,7 +219,6 @@ export default async function ProjectViewPage({ params }) {
)}
</CardContent>
</Card>
</div>
</div>
</PageContainer>
);
}

View File

@@ -6,6 +6,10 @@ import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
import { Input } from "@/components/ui/Input";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import SearchBar from "@/components/ui/SearchBar";
import { LoadingState } from "@/components/ui/States";
export default function ProjectListPage() {
const [projects, setProjects] = useState([]);
@@ -54,13 +58,8 @@ export default function ProjectListPage() {
setSearchTerm(e.target.value);
};
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Projects</h1>
<p className="text-gray-600 mt-1">Manage and track your projects</p>
</div>
<PageContainer>
<PageHeader title="Projects" description="Manage and track your projects">
<Link href="/projects/new">
<Button variant="primary" size="lg">
<svg
@@ -78,86 +77,16 @@ export default function ProjectListPage() {
</svg>
Add Project
</Button>
</Link>{" "}
</div>{" "}
{/* Search Bar */}
<div className="mb-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center space-x-4">
<div className="flex-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
className="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<Input
type="text"
</Link>
</PageHeader>
<SearchBar
searchTerm={searchTerm}
onSearchChange={handleSearchChange}
placeholder="Search by project name, WP, plot, or investment number..."
value={searchTerm}
onChange={handleSearchChange}
className="pl-10 pr-10 w-full border-gray-300 focus:border-blue-500 focus:ring-blue-500"
resultsCount={filteredProjects.length}
resultsText="projects"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm("")}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg
className="h-5 w-5 text-gray-400 hover:text-gray-600 transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
</div>
{searchTerm && (
<div className="mt-3 pt-3 border-t border-gray-100">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">
Found{" "}
<span className="font-medium text-gray-900">
{filteredProjects.length}
</span>{" "}
project
{filteredProjects.length !== 1 ? "s" : ""} matching
<span className="font-medium text-blue-600">
{" "}
&quot;{searchTerm}&quot;
</span>
</p>
{filteredProjects.length === 0 && (
<Button
variant="outline"
size="sm"
onClick={() => setSearchTerm("")}
>
Clear Search
</Button>
)}
</div>
</div>
)}
</div>
</div>
{filteredProjects.length === 0 && searchTerm ? (
<Card>
<CardContent className="text-center py-12">
@@ -178,8 +107,8 @@ export default function ProjectListPage() {
No projects found
</h3>
<p className="text-gray-500 mb-6">
No projects match your search criteria. Try adjusting your
search terms.
No projects match your search criteria. Try adjusting your search
terms.
</p>
<Button variant="outline" onClick={() => setSearchTerm("")}>
Clear Search
@@ -269,12 +198,11 @@ export default function ProjectListPage() {
View
</Button>
</Link>
</div>
</div>{" "}
</div>
))}
</div>
)}
</div>
</div>
</PageContainer>
);
}

View File

@@ -7,6 +7,11 @@ import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
import { Input } from "@/components/ui/Input";
import { formatDistanceToNow, parseISO } from "date-fns";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import SearchBar from "@/components/ui/SearchBar";
import FilterBar from "@/components/ui/FilterBar";
import { LoadingState } from "@/components/ui/States";
export default function ProjectTasksPage() {
const [allTasks, setAllTasks] = useState([]);
@@ -153,32 +158,57 @@ export default function ProjectTasksPage() {
.length,
completed: allTasks.filter((task) => task.status === "completed").length,
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<p className="mt-4 text-gray-600">Loading tasks...</p>
</div>
</div>
</div>
<PageContainer>
<PageHeader
title="Project Tasks"
description="Monitor and manage tasks across all projects"
/>
<LoadingState message="Loading tasks..." />
</PageContainer>
);
}
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Project Tasks</h1>
<p className="text-gray-600 mt-1">
Monitor and manage tasks across all projects
</p>
</div>
</div>
const filterOptions = [
{
label: "Status",
value: statusFilter,
onChange: (e) => setStatusFilter(e.target.value),
options: [
{ value: "all", label: "All" },
{ value: "pending", label: "Pending" },
{ value: "in_progress", label: "In Progress" },
{ value: "completed", label: "Completed" },
],
},
{
label: "Priority",
value: priorityFilter,
onChange: (e) => setPriorityFilter(e.target.value),
options: [
{ value: "all", label: "All" },
{ value: "high", label: "High" },
{ value: "normal", label: "Normal" },
{ value: "low", label: "Low" },
],
},
];
return (
<PageContainer>
<PageHeader
title="Project Tasks"
description="Monitor and manage tasks across all projects"
/>
<SearchBar
searchTerm={searchTerm}
onSearchChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search tasks by name, project, WP, or plot..."
resultsCount={filteredTasks.length}
resultsText="tasks"
/>{" "}
<FilterBar filters={filterOptions} className="mb-6" />
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<Card>
@@ -200,9 +230,7 @@ export default function ProjectTasksPage() {
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">
Total Tasks
</p>
<p className="text-sm font-medium text-gray-600">Total Tasks</p>
<p className="text-2xl font-bold text-gray-900">
{statusCounts.all}
</p>
@@ -210,7 +238,6 @@ export default function ProjectTasksPage() {
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center">
@@ -238,7 +265,6 @@ export default function ProjectTasksPage() {
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center">
@@ -258,9 +284,7 @@ export default function ProjectTasksPage() {
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">
In Progress
</p>
<p className="text-sm font-medium text-gray-600">In Progress</p>
<p className="text-2xl font-bold text-gray-900">
{statusCounts.in_progress}
</p>
@@ -268,7 +292,6 @@ export default function ProjectTasksPage() {
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center">
@@ -295,60 +318,8 @@ export default function ProjectTasksPage() {
</div>
</div>
</CardContent>
</Card>
</Card>{" "}
</div>
{/* Filters */}
<Card className="mb-6">
<CardContent className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Search Tasks
</label>
<Input
type="text"
placeholder="Search by task name, project name, WP, or plot..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Priority
</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={priorityFilter}
onChange={(e) => setPriorityFilter(e.target.value)}
>
<option value="all">All Priorities</option>
<option value="high">High</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
</select>
</div>
</div>
</CardContent>
</Card>
{/* Tasks List */}
{filteredTasks.length === 0 ? (
<Card>
@@ -370,9 +341,7 @@ export default function ProjectTasksPage() {
No tasks found
</h3>
<p className="text-gray-500 mb-6">
{searchTerm ||
statusFilter !== "all" ||
priorityFilter !== "all"
{searchTerm || statusFilter !== "all" || priorityFilter !== "all"
? "Try adjusting your filters to see more tasks"
: "No tasks have been created yet"}
</p>
@@ -389,10 +358,7 @@ export default function ProjectTasksPage() {
<h3 className="text-lg font-semibold text-gray-900">
{task.task_name}
</h3>
<Badge
variant={getStatusVariant(task.status)}
size="sm"
>
<Badge variant={getStatusVariant(task.status)} size="sm">
{getStatusDisplayName(task.status)}
</Badge>
<Badge
@@ -418,9 +384,7 @@ export default function ProjectTasksPage() {
{task.wp && (
<div>
<p className="text-sm text-gray-600">WP</p>
<p className="font-medium text-gray-900">
{task.wp}
</p>
<p className="font-medium text-gray-900">{task.wp}</p>
</div>
)}
{task.plot && (
@@ -478,10 +442,9 @@ export default function ProjectTasksPage() {
</div>
</CardContent>
</Card>
))}
))}{" "}
</div>
)}
</div>
</div>
</PageContainer>
);
}

View File

@@ -3,6 +3,8 @@ import Link from "next/link";
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
export default function TaskTemplatesPage() {
const templates = db
@@ -12,15 +14,12 @@ export default function TaskTemplatesPage() {
`
)
.all();
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Task Templates</h1>
<p className="text-gray-600 mt-1">Manage reusable task templates</p>
</div>
<PageContainer>
<PageHeader
title="Task Templates"
description="Manage reusable task templates"
action={
<Link href="/tasks/templates/new">
<Button variant="primary" size="lg">
<svg
@@ -39,7 +38,8 @@ export default function TaskTemplatesPage() {
New Template
</Button>
</Link>
</div>
}
/>
{templates.length === 0 ? (
<Card>
@@ -106,10 +106,9 @@ export default function TaskTemplatesPage() {
</div>
</CardContent>
</Card>
))}
))}{" "}
</div>
)}
</div>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,30 @@
"use client";
import { Select } from "./Input";
const FilterBar = ({ filters, className = "" }) => {
if (!filters || filters.length === 0) return null;
return (
<div className={`flex flex-wrap gap-4 ${className}`}>
{filters.map((filter, index) => (
<div key={index} className="min-w-0 flex-shrink-0">
<Select
label={filter.label}
value={filter.value}
onChange={filter.onChange}
className="min-w-[150px]"
>
{filter.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</div>
))}
</div>
);
};
export default FilterBar;

View File

@@ -0,0 +1,11 @@
"use client";
const PageContainer = ({ children, className = "" }) => {
return (
<div className={`min-h-screen bg-gray-50 ${className}`}>
<div className="max-w-6xl mx-auto p-6">{children}</div>
</div>
);
};
export default PageContainer;

View File

@@ -0,0 +1,15 @@
"use client";
const PageHeader = ({ title, description, children, className = "" }) => {
return (
<div className={`flex justify-between items-start mb-8 ${className}`}>
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900">{title}</h1>
{description && <p className="text-gray-600 mt-1">{description}</p>}
</div>
{children && <div className="ml-6 flex-shrink-0">{children}</div>}
</div>
);
};
export default PageHeader;

View File

@@ -0,0 +1,86 @@
"use client";
import { Input } from "./Input";
const SearchBar = ({
searchTerm,
onSearchChange,
placeholder = "Search...",
resultsCount = null,
resultsText = "items",
onClear = null,
filters = null,
className = "",
}) => {
return (
<div
className={`bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6 ${className}`}
>
<div className="flex items-center space-x-4">
<div className="flex-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
className="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<Input
type="text"
placeholder={placeholder}
value={searchTerm}
onChange={onSearchChange}
className="pl-10 pr-10 w-full border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
{searchTerm && (
<button
onClick={
onClear || (() => onSearchChange({ target: { value: "" } }))
}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg
className="h-5 w-5 text-gray-400 hover:text-gray-600 transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
{filters}
</div>
{searchTerm && resultsCount !== null && (
<div className="mt-3 pt-3 border-t border-gray-100">
<p className="text-sm text-gray-600">
Found{" "}
<span className="font-medium text-gray-900">{resultsCount}</span>{" "}
{resultsText} matching
<span className="font-medium text-blue-600">
{" "}
&quot;{searchTerm}&quot;
</span>
</p>
</div>
)}
</div>
);
};
export default SearchBar;

113
src/components/ui/States.js Normal file
View File

@@ -0,0 +1,113 @@
"use client";
import { Card, CardContent } from "./Card";
import Button from "./Button";
const LoadingSpinner = ({ size = "md" }) => {
const sizeClasses = {
sm: "w-4 h-4",
md: "w-8 h-8",
lg: "w-12 h-12",
};
return (
<div className="flex items-center justify-center">
<div
className={`${sizeClasses[size]} animate-spin rounded-full border-2 border-gray-300 border-t-blue-600`}
></div>
</div>
);
};
const LoadingState = ({ message = "Loading...", className = "" }) => {
return (
<div className={`flex items-center justify-center py-12 ${className}`}>
<div className="text-center">
<LoadingSpinner size="lg" />
<p className="text-gray-600 mt-4">{message}</p>
</div>
</div>
);
};
const EmptyState = ({
icon,
title,
description,
actionLabel,
actionHref,
onAction,
className = "",
}) => {
return (
<Card className={className}>
<CardContent className="text-center py-12">
<div className="text-gray-400 mb-4">
{icon || (
<svg
className="w-16 h-16 mx-auto"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2z"
clipRule="evenodd"
/>
</svg>
)}
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">{title}</h3>
{description && <p className="text-gray-500 mb-6">{description}</p>}
{actionLabel && (actionHref || onAction) && (
<Button
variant="primary"
onClick={onAction}
as={actionHref ? "a" : "button"}
href={actionHref}
>
{actionLabel}
</Button>
)}
</CardContent>
</Card>
);
};
const ErrorState = ({
title = "Something went wrong",
description = "We encountered an error. Please try again.",
onRetry,
className = "",
}) => {
return (
<Card className={className}>
<CardContent className="text-center py-12">
<div className="text-red-400 mb-4">
<svg
className="w-16 h-16 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">{title}</h3>
<p className="text-gray-500 mb-6">{description}</p>
{onRetry && (
<Button variant="primary" onClick={onRetry}>
Try Again
</Button>
)}
</CardContent>
</Card>
);
};
export { LoadingSpinner, LoadingState, EmptyState, ErrorState };