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 { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge"; 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() { export default function ContractsMainPage() {
const [contracts, setContracts] = useState([]); const [contracts, setContracts] = useState([]);
@@ -162,37 +167,75 @@ export default function ContractsMainPage() {
}; };
if (loading) { if (loading) {
return ( return (
<div className="p-6 max-w-7xl mx-auto"> <PageContainer>
<div className="text-center"> <PageHeader
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div> title="Umowy"
<p className="mt-2 text-gray-600">Ładowanie umów...</p> description="Zarządzaj swoimi umowami i kontraktami"
</div> >
</div> <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 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 ( return (
<div className="p-6 max-w-7xl mx-auto space-y-6"> <PageContainer>
{/* Header */} <PageHeader
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> title="Umowy"
<div> description="Zarządzaj swoimi umowami i kontraktami"
<h1 className="text-3xl font-bold text-gray-900">Umowy</h1> >
<p className="text-gray-600 mt-1"> <Link href="/contracts/new">
Zarządzaj swoimi umowami i kontraktami <Button variant="primary" size="lg">
</p> <span className="mr-2"></span>
</div>{" "} Nowa umowa
<Link </Button>
href="/contracts/new" </Link>{" "}
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" </PageHeader>
>
<span className="mr-2"></span>
Nowa umowa
</Link>
</div>
{/* Statistics Cards */} {/* 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> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center"> <div className="flex items-center">
@@ -302,93 +345,17 @@ export default function ContractsMainPage() {
</p> </p>
</div> </div>
</div> </div>
</CardContent> </CardContent>{" "}
</Card> </Card>
</div> </div>
{/* Filters and Search */} <SearchBar
<Card> searchTerm={searchTerm}
<CardContent className="p-4"> onSearchChange={handleSearchChange}
<div className="flex flex-col lg:flex-row gap-4"> placeholder="Szukaj umów po numerze, nazwie, kliencie lub inwestorze..."
{/* Search */} resultsCount={filteredContracts.length}
<div className="flex-1"> resultsText="umów"
<label />
htmlFor="search" <FilterBar filters={filterOptions} className="mb-6" />{" "}
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"
/>
</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>{" "}
{/* Contracts List */} {/* Contracts List */}
{filteredContracts.length === 0 ? ( {filteredContracts.length === 0 ? (
<Card> <Card>
@@ -624,9 +591,9 @@ export default function ContractsMainPage() {
Wyczyść filtry Wyczyść filtry
</button> </button>
)} )}
</p> </p>{" "}
</div> </div>
)} )}
</div> </PageContainer>
); );
} }

View File

@@ -5,6 +5,9 @@ import Link from "next/link";
import { Card, CardHeader, CardContent } from "@/components/ui/Card"; import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge"; 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() { export default function Home() {
const [stats, setStats] = useState({ const [stats, setStats] = useState({
@@ -44,317 +47,319 @@ export default function Home() {
}, []); }, []);
return ( return (
<div className="min-h-screen bg-gray-50"> <PageContainer>
<div className="max-w-6xl mx-auto p-6"> <PageHeader
<div className="mb-8"> title="Dashboard"
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1> description="Overview of your projects and tasks"
<p className="text-gray-600 mt-1"> />
Overview of your projects and tasks
</p>
</div>
{/* Stats Cards */} {loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <LoadingState message="Loading dashboard data..." />
<Card> ) : (
<CardContent className="p-6"> <>
<div className="flex items-center"> {/* Stats Cards */}
<div className="flex-1"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<p className="text-sm font-medium text-gray-600"> <Card>
Total Projects <CardContent className="p-6">
</p> <div className="flex items-center">
<p className="text-2xl font-bold text-gray-900"> <div className="flex-1">
{stats.totalProjects} <p className="text-sm font-medium text-gray-600">
</p> Total Projects
</p>
<p className="text-2xl font-bold text-gray-900">
{stats.totalProjects}
</p>
</div>
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
</div>
</div> </div>
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center"> </CardContent>
<svg </Card>
className="w-6 h-6 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
</div>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">
Active Projects Active Projects
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.activeProjects} {stats.activeProjects}
</p> </p>
</div>
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div> </div>
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center"> </CardContent>
<svg </Card>
className="w-6 h-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">
Pending Tasks Pending Tasks
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.pendingTasks} {stats.pendingTasks}
</p> </p>
</div>
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-yellow-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div> </div>
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center"> </CardContent>
<svg </Card>
className="w-6 h-6 text-yellow-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">
Completed Tasks Completed Tasks
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.completedTasks} {stats.completedTasks}
</p> </p>
</div>
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div> </div>
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center"> </CardContent>
<svg </Card>
className="w-6 h-6 text-purple-600" </div>
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Recent Projects */} {/* Recent Projects */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900"> <h2 className="text-xl font-semibold text-gray-900">
Recent Projects Recent Projects
</h2> </h2>
<Link href="/projects"> <Link href="/projects">
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
View All View All
</Button> </Button>
</Link> </Link>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
{loading ? ( {loading ? (
<div className="p-6"> <div className="p-6">
<div className="animate-pulse space-y-3"> <div className="animate-pulse space-y-3">
{[...Array(3)].map((_, i) => ( {[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center space-x-3"> <div key={i} className="flex items-center space-x-3">
<div className="w-3 h-3 bg-gray-200 rounded-full"></div> <div className="w-3 h-3 bg-gray-200 rounded-full"></div>
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<div className="h-4 bg-gray-200 rounded w-3/4"></div> <div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div> <div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
))}
</div>
</div>
) : recentProjects.length === 0 ? (
<div className="p-6 text-center">
<p className="text-gray-500">No projects yet.</p>
<Link href="/projects/new" className="mt-2 inline-block">
<Button variant="primary" size="sm">
Create First Project
</Button>
</Link>
</div>
) : (
<div className="divide-y divide-gray-200">
{recentProjects.map((project) => (
<div
key={project.project_id}
className="p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<Link
href={`/projects/${project.project_id}`}
className="text-sm font-medium text-blue-600 hover:text-blue-800 truncate block"
>
{project.project_name}
</Link>
<p className="text-xs text-gray-500 mt-1">
{project.city} Due: {project.finish_date}
</p>
</div>
<Badge
variant={
new Date(project.finish_date) >= new Date()
? "success"
: "danger"
}
size="xs"
>
{new Date(project.finish_date) >= new Date()
? "Active"
: "Overdue"}
</Badge>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> )}
) : recentProjects.length === 0 ? ( </CardContent>
<div className="p-6 text-center"> </Card>
<p className="text-gray-500">No projects yet.</p>
<Link href="/projects/new" className="mt-2 inline-block"> {/* Quick Actions */}
<Button variant="primary" size="sm"> <Card>
Create First Project <CardHeader>
</Button> <h2 className="text-xl font-semibold text-gray-900">
</Link> Quick Actions
</div> </h2>
) : ( </CardHeader>
<div className="divide-y divide-gray-200"> <CardContent className="space-y-4">
{recentProjects.map((project) => ( <Link href="/projects/new" className="block">
<div <div className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
key={project.project_id} <div className="flex items-center">
className="p-4 hover:bg-gray-50 transition-colors" <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-3">
> <svg
<div className="flex items-center justify-between"> className="w-6 h-6 text-blue-600"
<div className="flex-1 min-w-0"> fill="none"
<Link stroke="currentColor"
href={`/projects/${project.project_id}`} viewBox="0 0 24 24"
className="text-sm font-medium text-blue-600 hover:text-blue-800 truncate block"
>
{project.project_name}
</Link>
<p className="text-xs text-gray-500 mt-1">
{project.city} Due: {project.finish_date}
</p>
</div>
<Badge
variant={
new Date(project.finish_date) >= new Date()
? "success"
: "danger"
}
size="xs"
> >
{new Date(project.finish_date) >= new Date() <path
? "Active" strokeLinecap="round"
: "Overdue"} strokeLinejoin="round"
</Badge> strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
</div>
<div>
<h3 className="text-sm font-medium text-gray-900">
New Project
</h3>
<p className="text-xs text-gray-500">
Create a new project
</p>
</div> </div>
</div> </div>
))} </div>
</div> </Link>
)}
</CardContent>
</Card>
{/* Quick Actions */} <Link href="/contracts/new" className="block">
<Card> <div className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
<CardHeader> <div className="flex items-center">
<h2 className="text-xl font-semibold text-gray-900"> <div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-3">
Quick Actions <svg
</h2> className="w-6 h-6 text-green-600"
</CardHeader> fill="none"
<CardContent className="space-y-4"> stroke="currentColor"
<Link href="/projects/new" className="block"> viewBox="0 0 24 24"
<div className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"> >
<div className="flex items-center"> <path
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-3"> strokeLinecap="round"
<svg strokeLinejoin="round"
className="w-6 h-6 text-blue-600" strokeWidth={2}
fill="none" d="M9 12h6m-6 4h6m2 5H7a2 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"
stroke="currentColor" />
viewBox="0 0 24 24" </svg>
> </div>
<path <div>
strokeLinecap="round" <h3 className="text-sm font-medium text-gray-900">
strokeLinejoin="round" New Contract
strokeWidth={2} </h3>
d="M12 4v16m8-8H4" <p className="text-xs text-gray-500">
/> Add a new contract
</svg> </p>
</div> </div>
<div>
<h3 className="text-sm font-medium text-gray-900">
New Project
</h3>
<p className="text-xs text-gray-500">
Create a new project
</p>
</div> </div>
</div> </div>
</div> </Link>
</Link>
<Link href="/contracts/new" className="block"> <Link href="/tasks/templates/new" className="block">
<div className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"> <div className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
<div className="flex items-center"> <div className="flex items-center">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-3"> <div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-3">
<svg <svg
className="w-6 h-6 text-green-600" className="w-6 h-6 text-purple-600"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 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" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/> />
</svg> </svg>
</div> </div>
<div> <div>
<h3 className="text-sm font-medium text-gray-900"> <h3 className="text-sm font-medium text-gray-900">
New Contract Task Template
</h3> </h3>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Add a new contract Create task template
</p> </p>
</div>
</div> </div>
</div> </div>
</div> </Link>
</Link> </CardContent>
</Card>
<Link href="/tasks/templates/new" className="block"> </div>
<div className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"> </>
<div className="flex items-center"> )}
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-3"> </PageContainer>
<svg
className="w-6 h-6 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
</div>
<div>
<h3 className="text-sm font-medium text-gray-900">
Task Template
</h3>
<p className="text-xs text-gray-500">
Create task template
</p>
</div>
</div>
</div>
</Link>
</CardContent>
</Card>
</div>
</div>
</div>
); );
} }

View File

@@ -9,6 +9,8 @@ import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
import Link from "next/link"; import Link from "next/link";
import { differenceInCalendarDays, parseISO } from "date-fns"; import { differenceInCalendarDays, parseISO } from "date-fns";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
export default async function ProjectViewPage({ params }) { export default async function ProjectViewPage({ params }) {
const project = getProjectWithContract(params.id); const project = getProjectWithContract(params.id);
@@ -17,10 +19,9 @@ export default async function ProjectViewPage({ params }) {
parseISO(project.finish_date), parseISO(project.finish_date),
new Date() new Date()
); );
if (!project) { if (!project) {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <PageContainer>
<Card> <Card>
<CardContent className="text-center py-8"> <CardContent className="text-center py-8">
<p className="text-red-600 text-lg">Project not found.</p> <p className="text-red-600 text-lg">Project not found.</p>
@@ -29,7 +30,7 @@ export default async function ProjectViewPage({ params }) {
</Link> </Link>
</CardContent> </CardContent>
</Card> </Card>
</div> </PageContainer>
); );
} }
@@ -38,197 +39,186 @@ export default async function ProjectViewPage({ params }) {
if (days <= 7) return "warning"; if (days <= 7) return "warning";
return "success"; return "success";
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <PageContainer>
<div className="max-w-6xl mx-auto p-6 space-y-6"> <PageHeader
<div className="flex items-center justify-between"> title={project.project_name}
<Link href="/projects"> description={`${project.city}${project.address}`}
<Button variant="outline" size="sm"> action={
<svg <div className="flex items-center gap-3">
className="w-4 h-4 mr-2" <Badge variant={getDeadlineVariant(daysRemaining)} size="md">
fill="none" {daysRemaining === 0
stroke="currentColor" ? "Due Today"
viewBox="0 0 24 24" : daysRemaining > 0
> ? `${daysRemaining} days left`
<path : `${Math.abs(daysRemaining)} days overdue`}
strokeLinecap="round" </Badge>
strokeLinejoin="round" <Link href="/projects">
strokeWidth={2} <Button variant="outline" size="sm">
d="M15 19l-7-7 7-7" <svg
/> className="w-4 h-4 mr-2"
</svg> fill="none"
Back to Projects stroke="currentColor"
</Button> viewBox="0 0 24 24"
</Link> >
<Link href={`/projects/${params.id}/edit`}> <path
<Button variant="secondary">Edit Project</Button> strokeLinecap="round"
</Link> strokeLinejoin="round"
</div> strokeWidth={2}
d="M15 19l-7-7 7-7"
<div className="flex items-center gap-4 mb-6"> />
<h1 className="text-3xl font-bold text-gray-900"> </svg>
{project.project_name} Back to Projects
</h1> </Button>
<Badge variant={getDeadlineVariant(daysRemaining)} size="md"> </Link>
{daysRemaining === 0 <Link href={`/projects/${params.id}/edit`}>
? "Due Today" <Button variant="secondary">Edit Project</Button>
: daysRemaining > 0 </Link>
? `${daysRemaining} days left` </div>
: `${Math.abs(daysRemaining)} days overdue`} }
</Badge> />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h2 className="text-xl font-semibold text-gray-900">
Project Information
</h2>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<span className="text-sm font-medium text-gray-500">
Location
</span>
<p className="text-gray-900">{project.city}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Address
</span>
<p className="text-gray-900">{project.address}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Plot
</span>
<p className="text-gray-900">{project.plot}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
District
</span>
<p className="text-gray-900">{project.district}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Unit
</span>
<p className="text-gray-900">{project.unit}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Deadline
</span>
<p className="text-gray-900">{project.finish_date}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">WP</span>
<p className="text-gray-900">{project.wp}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Investment Number
</span>
<p className="text-gray-900">{project.investment_number}</p>
</div>
</div>
<div>
<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>
<p className="text-gray-900">{project.notes}</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<h2 className="text-xl font-semibold text-gray-900">
Contract Details
</h2>
</CardHeader>
<CardContent className="space-y-4">
<div>
<span className="text-sm font-medium text-gray-500">
Contract Number
</span>
<p className="text-gray-900">{project.contract_number}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Contract Name
</span>
<p className="text-gray-900">{project.contract_name}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Customer
</span>
<p className="text-gray-900">{project.customer}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Investor
</span>
<p className="text-gray-900">{project.investor}</p>
</div>
</CardContent>
</Card>
</div>
<ProjectTasksSection projectId={params.id} />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card> <Card>
<CardHeader> <CardHeader>
<h2 className="text-xl font-semibold text-gray-900">Notes</h2> <h2 className="text-xl font-semibold text-gray-900">
Project Information
</h2>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-4">
<div className="mb-6"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<NoteForm projectId={params.id} /> <div>
</div> <span className="text-sm font-medium text-gray-500">
{notes.length === 0 ? ( Location
<div className="text-center py-8"> </span>
<div className="text-gray-400 mb-2"> <p className="text-gray-900">{project.city}</p>
<svg
className="w-12 h-12 mx-auto"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-1H8v1a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v4a1 1 0 001 1h1a1 1 0 100-2v-1a2 2 0 00-2-2H7a1 1 0 000 2z"
/>
</svg>
</div>
<p className="text-gray-500">No notes yet.</p>
</div> </div>
) : ( <div>
<div className="space-y-3"> <span className="text-sm font-medium text-gray-500">
{notes.map((n) => ( Address
<div </span>
key={n.note_id} <p className="text-gray-900">{project.address}</p>
className="border border-gray-200 p-4 rounded-lg bg-gray-50" </div>
> <div>
<p className="text-sm text-gray-500 mb-2">{n.note_date}</p> <span className="text-sm font-medium text-gray-500">Plot</span>
<p className="text-gray-900">{n.note}</p> <p className="text-gray-900">{project.plot}</p>
</div> </div>
))} <div>
<span className="text-sm font-medium text-gray-500">
District
</span>
<p className="text-gray-900">{project.district}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">Unit</span>
<p className="text-gray-900">{project.unit}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Deadline
</span>
<p className="text-gray-900">{project.finish_date}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">WP</span>
<p className="text-gray-900">{project.wp}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Investment Number
</span>
<p className="text-gray-900">{project.investment_number}</p>
</div>
</div>
<div>
<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>
<p className="text-gray-900">{project.notes}</p>
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<h2 className="text-xl font-semibold text-gray-900">
Contract Details
</h2>
</CardHeader>
<CardContent className="space-y-4">
<div>
<span className="text-sm font-medium text-gray-500">
Contract Number
</span>
<p className="text-gray-900">{project.contract_number}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Contract Name
</span>
<p className="text-gray-900">{project.contract_name}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Customer
</span>
<p className="text-gray-900">{project.customer}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">
Investor
</span>
<p className="text-gray-900">{project.investor}</p>
</div>
</CardContent>
</Card>
</div> </div>
</div>
<ProjectTasksSection projectId={params.id} />
<Card>
<CardHeader>
<h2 className="text-xl font-semibold text-gray-900">Notes</h2>
</CardHeader>
<CardContent>
<div className="mb-6">
<NoteForm projectId={params.id} />
</div>
{notes.length === 0 ? (
<div className="text-center py-8">
<div className="text-gray-400 mb-2">
<svg
className="w-12 h-12 mx-auto"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-1H8v1a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v4a1 1 0 001 1h1a1 1 0 100-2v-1a2 2 0 00-2-2H7a1 1 0 000 2z"
/>
</svg>
</div>
<p className="text-gray-500">No notes yet.</p>
</div>
) : (
<div className="space-y-3">
{notes.map((n) => (
<div
key={n.note_id}
className="border border-gray-200 p-4 rounded-lg bg-gray-50"
>
<p className="text-sm text-gray-500 mb-2">{n.note_date}</p>
<p className="text-gray-900">{n.note}</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
</PageContainer>
); );
} }

View File

@@ -6,6 +6,10 @@ import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
import { Input } from "@/components/ui/Input"; 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() { export default function ProjectListPage() {
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
@@ -54,227 +58,151 @@ export default function ProjectListPage() {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <PageContainer>
<div className="max-w-6xl mx-auto p-6"> <PageHeader title="Projects" description="Manage and track your projects">
<div className="flex justify-between items-center mb-8"> <Link href="/projects/new">
<div> <Button variant="primary" size="lg">
<h1 className="text-3xl font-bold text-gray-900">Projects</h1> <svg
<p className="text-gray-600 mt-1">Manage and track your projects</p> className="w-5 h-5 mr-2"
</div> fill="none"
<Link href="/projects/new"> stroke="currentColor"
<Button variant="primary" size="lg"> viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add Project
</Button>
</Link>
</PageHeader>
<SearchBar
searchTerm={searchTerm}
onSearchChange={handleSearchChange}
placeholder="Search by project name, WP, plot, or investment number..."
resultsCount={filteredProjects.length}
resultsText="projects"
/>
{filteredProjects.length === 0 && searchTerm ? (
<Card>
<CardContent className="text-center py-12">
<div className="text-gray-400 mb-4">
<svg <svg
className="w-5 h-5 mr-2" className="w-16 h-16 mx-auto"
fill="none" fill="currentColor"
stroke="currentColor" viewBox="0 0 20 20"
viewBox="0 0 24 24"
> >
<path <path
strokeLinecap="round" fillRule="evenodd"
strokeLinejoin="round" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
strokeWidth={2} clipRule="evenodd"
d="M12 4v16m8-8H4"
/> />
</svg> </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"
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"
/>
{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> </div>
{searchTerm && ( <h3 className="text-lg font-medium text-gray-900 mb-2">
<div className="mt-3 pt-3 border-t border-gray-100"> No projects found
<div className="flex items-center justify-between"> </h3>
<p className="text-sm text-gray-600"> <p className="text-gray-500 mb-6">
Found{" "} No projects match your search criteria. Try adjusting your search
<span className="font-medium text-gray-900"> terms.
{filteredProjects.length} </p>
</span>{" "} <Button variant="outline" onClick={() => setSearchTerm("")}>
project Clear Search
{filteredProjects.length !== 1 ? "s" : ""} matching </Button>
<span className="font-medium text-blue-600"> </CardContent>
{" "} </Card>
&quot;{searchTerm}&quot; ) : projects.length === 0 ? (
</span> <Card>
</p> <CardContent className="text-center py-12">
{filteredProjects.length === 0 && ( <div className="text-gray-400 mb-4">
<Button <svg
variant="outline" className="w-16 h-16 mx-auto"
size="sm" fill="currentColor"
onClick={() => setSearchTerm("")} viewBox="0 0 20 20"
>
Clear Search
</Button>
)}
</div>
</div>
)}
</div>
</div>
{filteredProjects.length === 0 && searchTerm ? (
<Card>
<CardContent className="text-center py-12">
<div className="text-gray-400 mb-4">
<svg
className="w-16 h-16 mx-auto"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
No projects found
</h3>
<p className="text-gray-500 mb-6">
No projects match your search criteria. Try adjusting your
search terms.
</p>
<Button variant="outline" onClick={() => setSearchTerm("")}>
Clear Search
</Button>
</CardContent>
</Card>
) : projects.length === 0 ? (
<Card>
<CardContent className="text-center py-12">
<div className="text-gray-400 mb-4">
<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">
No projects yet
</h3>
<p className="text-gray-500 mb-6">
Get started by creating your first project
</p>
<Link href="/projects/new">
<Button variant="primary">Create First Project</Button>
</Link>
</CardContent>
</Card>
) : (
<div className="bg-white rounded-lg shadow overflow-hidden">
{/* Header Row */}
<div className="grid grid-cols-12 gap-4 p-4 bg-gray-100 border-b font-semibold text-sm text-gray-700">
{" "}
<div className="col-span-1">Number</div>
<div className="col-span-3">Project Name</div>
<div className="col-span-2">WP</div>
<div className="col-span-1">City</div>
<div className="col-span-2">Address</div>
<div className="col-span-1">Plot</div>{" "}
<div className="col-span-1">Finish Date</div>
<div className="col-span-1">Actions</div>
</div>{" "}
{/* Data Rows */}
{filteredProjects.map((project, index) => (
<div
key={project.project_id}
className={`grid grid-cols-12 gap-4 p-4 border-b hover:bg-gray-50 transition-colors items-center ${
index % 2 === 0 ? "bg-white" : "bg-gray-25"
}`}
> >
<div className="col-span-1"> <path
<Badge variant="primary" size="sm"> fillRule="evenodd"
{project.project_number} 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"
</Badge> clipRule="evenodd"
</div>{" "} />
<div className="col-span-3"> </svg>
<Link </div>
href={`/projects/${project.project_id}`} <h3 className="text-lg font-medium text-gray-900 mb-2">
className="font-medium text-blue-600 hover:text-blue-800 transition-colors truncate block" No projects yet
> </h3>
{project.project_name} <p className="text-gray-500 mb-6">
</Link> Get started by creating your first project
</div> </p>
<div className="col-span-2 text-sm text-gray-600 truncate"> <Link href="/projects/new">
{project.wp || "N/A"} <Button variant="primary">Create First Project</Button>
</div> </Link>
<div className="col-span-1 text-sm text-gray-600 truncate"> </CardContent>
{project.city || "N/A"} </Card>
</div> ) : (
<div className="col-span-2 text-sm text-gray-600 truncate"> <div className="bg-white rounded-lg shadow overflow-hidden">
{project.address || "N/A"} {/* Header Row */}
</div> <div className="grid grid-cols-12 gap-4 p-4 bg-gray-100 border-b font-semibold text-sm text-gray-700">
<div className="col-span-1 text-sm text-gray-600 truncate"> {" "}
{project.plot || "N/A"} <div className="col-span-1">Number</div>
</div>{" "} <div className="col-span-3">Project Name</div>
<div className="col-span-1 text-sm text-gray-600 truncate"> <div className="col-span-2">WP</div>
{project.finish_date || "N/A"} <div className="col-span-1">City</div>
</div> <div className="col-span-2">Address</div>
<div className="col-span-1"> <div className="col-span-1">Plot</div>{" "}
<Link href={`/projects/${project.project_id}`}> <div className="col-span-1">Finish Date</div>
<Button variant="outline" size="sm"> <div className="col-span-1">Actions</div>
View </div>{" "}
</Button> {/* Data Rows */}
</Link> {filteredProjects.map((project, index) => (
</div> <div
key={project.project_id}
className={`grid grid-cols-12 gap-4 p-4 border-b hover:bg-gray-50 transition-colors items-center ${
index % 2 === 0 ? "bg-white" : "bg-gray-25"
}`}
>
<div className="col-span-1">
<Badge variant="primary" size="sm">
{project.project_number}
</Badge>
</div>{" "}
<div className="col-span-3">
<Link
href={`/projects/${project.project_id}`}
className="font-medium text-blue-600 hover:text-blue-800 transition-colors truncate block"
>
{project.project_name}
</Link>
</div> </div>
))} <div className="col-span-2 text-sm text-gray-600 truncate">
</div> {project.wp || "N/A"}
)} </div>
</div> <div className="col-span-1 text-sm text-gray-600 truncate">
</div> {project.city || "N/A"}
</div>
<div className="col-span-2 text-sm text-gray-600 truncate">
{project.address || "N/A"}
</div>
<div className="col-span-1 text-sm text-gray-600 truncate">
{project.plot || "N/A"}
</div>{" "}
<div className="col-span-1 text-sm text-gray-600 truncate">
{project.finish_date || "N/A"}
</div>
<div className="col-span-1">
<Link href={`/projects/${project.project_id}`}>
<Button variant="outline" size="sm">
View
</Button>
</Link>
</div>{" "}
</div>
))}
</div>
)}
</PageContainer>
); );
} }

View File

@@ -7,6 +7,11 @@ import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { formatDistanceToNow, parseISO } from "date-fns"; 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() { export default function ProjectTasksPage() {
const [allTasks, setAllTasks] = useState([]); const [allTasks, setAllTasks] = useState([]);
@@ -153,335 +158,293 @@ export default function ProjectTasksPage() {
.length, .length,
completed: allTasks.filter((task) => task.status === "completed").length, completed: allTasks.filter((task) => task.status === "completed").length,
}; };
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gray-50"> <PageContainer>
<div className="max-w-6xl mx-auto p-6"> <PageHeader
<div className="text-center py-12"> title="Project Tasks"
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> description="Monitor and manage tasks across all projects"
<p className="mt-4 text-gray-600">Loading tasks...</p> />
</div> <LoadingState message="Loading tasks..." />
</div> </PageContainer>
</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 ( return (
<div className="min-h-screen bg-gray-50"> <PageContainer>
<div className="max-w-6xl mx-auto p-6"> <PageHeader
<div className="flex justify-between items-center mb-8"> title="Project Tasks"
<div> description="Monitor and manage tasks across all projects"
<h1 className="text-3xl font-bold text-gray-900">Project Tasks</h1> />
<p className="text-gray-600 mt-1"> <SearchBar
Monitor and manage tasks across all projects searchTerm={searchTerm}
</p> onSearchChange={(e) => setSearchTerm(e.target.value)}
</div> placeholder="Search tasks by name, project, WP, or plot..."
</div> resultsCount={filteredTasks.length}
resultsText="tasks"
{/* Stats Cards */} />{" "}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8"> <FilterBar filters={filterOptions} className="mb-6" />
<Card> {/* Stats Cards */}
<CardContent className="p-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="flex items-center"> <Card>
<div className="p-2 bg-blue-100 rounded-lg"> <CardContent className="p-4">
<svg <div className="flex items-center">
className="w-6 h-6 text-blue-600" <div className="p-2 bg-blue-100 rounded-lg">
fill="none" <svg
stroke="currentColor" className="w-6 h-6 text-blue-600"
viewBox="0 0 24 24" fill="none"
> stroke="currentColor"
<path viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">
Total Tasks
</p>
<p className="text-2xl font-bold text-gray-900">
{statusCounts.all}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center">
<div className="p-2 bg-gray-100 rounded-lg">
<svg
className="w-6 h-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Pending</p>
<p className="text-2xl font-bold text-gray-900">
{statusCounts.pending}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 rounded-lg">
<svg
className="w-6 h-6 text-yellow-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<div className="ml-4">
<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>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<svg
className="w-6 h-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Completed</p>
<p className="text-2xl font-bold text-gray-900">
{statusCounts.completed}
</p>
</div>
</div>
</CardContent>
</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> <path
<option value="pending">Pending</option> strokeLinecap="round"
<option value="in_progress">In Progress</option> strokeLinejoin="round"
<option value="completed">Completed</option> strokeWidth={2}
</select> d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
</div> </div>
<div className="ml-4">
<div> <p className="text-sm font-medium text-gray-600">Total Tasks</p>
<label className="block text-sm font-medium text-gray-700 mb-2"> <p className="text-2xl font-bold text-gray-900">
Priority {statusCounts.all}
</label> </p>
<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>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card>
{/* Tasks List */} <CardContent className="p-4">
{filteredTasks.length === 0 ? ( <div className="flex items-center">
<Card> <div className="p-2 bg-gray-100 rounded-lg">
<CardContent className="text-center py-12">
<div className="text-gray-400 mb-4">
<svg <svg
className="w-16 h-16 mx-auto" className="w-6 h-6 text-gray-600"
fill="currentColor" fill="none"
viewBox="0 0 20 20" stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <path
fillRule="evenodd" strokeLinecap="round"
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" strokeLinejoin="round"
clipRule="evenodd" strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
</div> </div>
<h3 className="text-lg font-medium text-gray-900 mb-2"> <div className="ml-4">
No tasks found <p className="text-sm font-medium text-gray-600">Pending</p>
</h3> <p className="text-2xl font-bold text-gray-900">
<p className="text-gray-500 mb-6"> {statusCounts.pending}
{searchTerm || </p>
statusFilter !== "all" || </div>
priorityFilter !== "all" </div>
? "Try adjusting your filters to see more tasks" </CardContent>
: "No tasks have been created yet"} </Card>
</p> <Card>
</CardContent> <CardContent className="p-4">
</Card> <div className="flex items-center">
) : ( <div className="p-2 bg-yellow-100 rounded-lg">
<div className="space-y-4"> <svg
{filteredTasks.map((task) => ( className="w-6 h-6 text-yellow-600"
<Card key={task.id} className="hover:shadow-md transition-shadow"> fill="none"
<CardContent className="p-6"> stroke="currentColor"
<div className="flex items-start justify-between"> viewBox="0 0 24 24"
<div className="flex-1"> >
<div className="flex items-center gap-3 mb-2"> <path
<h3 className="text-lg font-semibold text-gray-900"> strokeLinecap="round"
{task.task_name} strokeLinejoin="round"
</h3> strokeWidth={2}
<Badge d="M13 10V3L4 14h7v7l9-11h-7z"
variant={getStatusVariant(task.status)} />
size="sm" </svg>
> </div>
{getStatusDisplayName(task.status)} <div className="ml-4">
<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>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<svg
className="w-6 h-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Completed</p>
<p className="text-2xl font-bold text-gray-900">
{statusCounts.completed}
</p>
</div>
</div>
</CardContent>
</Card>{" "}
</div>
{/* Tasks List */}
{filteredTasks.length === 0 ? (
<Card>
<CardContent className="text-center py-12">
<div className="text-gray-400 mb-4">
<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">
No tasks found
</h3>
<p className="text-gray-500 mb-6">
{searchTerm || statusFilter !== "all" || priorityFilter !== "all"
? "Try adjusting your filters to see more tasks"
: "No tasks have been created yet"}
</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{filteredTasks.map((task) => (
<Card key={task.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">
{task.task_name}
</h3>
<Badge variant={getStatusVariant(task.status)} size="sm">
{getStatusDisplayName(task.status)}
</Badge>
<Badge
variant={getPriorityVariant(task.priority)}
size="sm"
>
{task.priority}
</Badge>
{task.task_type === "template" && (
<Badge variant="primary" size="sm">
Template
</Badge> </Badge>
<Badge )}
variant={getPriorityVariant(task.priority)} </div>
size="sm"
>
{task.priority}
</Badge>
{task.task_type === "template" && (
<Badge variant="primary" size="sm">
Template
</Badge>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<p className="text-sm text-gray-600">Project</p>
<p className="font-medium text-gray-900">
{task.project_name}
</p>
</div>
{task.wp && (
<div> <div>
<p className="text-sm text-gray-600">Project</p> <p className="text-sm text-gray-600">WP</p>
<p className="font-medium text-gray-900">{task.wp}</p>
</div>
)}
{task.plot && (
<div>
<p className="text-sm text-gray-600">Plot</p>
<p className="font-medium text-gray-900"> <p className="font-medium text-gray-900">
{task.project_name} {task.plot}
</p> </p>
</div> </div>
{task.wp && ( )}
<div>
<p className="text-sm text-gray-600">WP</p>
<p className="font-medium text-gray-900">
{task.wp}
</p>
</div>
)}
{task.plot && (
<div>
<p className="text-sm text-gray-600">Plot</p>
<p className="font-medium text-gray-900">
{task.plot}
</p>
</div>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>
Added{" "}
{formatDistanceToNow(parseISO(task.date_added), {
addSuffix: true,
})}
</span>
{task.max_wait_days > 0 && (
<span>Max wait: {task.max_wait_days} days</span>
)}
</div>
</div> </div>
<div className="flex items-center space-x-2 ml-6"> <div className="flex items-center gap-4 text-sm text-gray-500">
{task.status !== "completed" && ( <span>
<select Added{" "}
className="text-sm px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" {formatDistanceToNow(parseISO(task.date_added), {
value={task.status} addSuffix: true,
onChange={(e) => })}
handleStatusChange(task.id, e.target.value) </span>
} {task.max_wait_days > 0 && (
> <span>Max wait: {task.max_wait_days} days</span>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
)} )}
<Link href={`/projects/${task.project_id}`}>
<Button variant="outline" size="sm">
View Project
</Button>
</Link>
<Button
variant="secondary"
size="sm"
onClick={() => handleDeleteTask(task.id)}
>
Delete
</Button>
</div> </div>
</div> </div>
</CardContent>
</Card> <div className="flex items-center space-x-2 ml-6">
))} {task.status !== "completed" && (
</div> <select
)} className="text-sm px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
</div> value={task.status}
</div> onChange={(e) =>
handleStatusChange(task.id, e.target.value)
}
>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
)}
<Link href={`/projects/${task.project_id}`}>
<Button variant="outline" size="sm">
View Project
</Button>
</Link>
<Button
variant="secondary"
size="sm"
onClick={() => handleDeleteTask(task.id)}
>
Delete
</Button>
</div>
</div>
</CardContent>
</Card>
))}{" "}
</div>
)}
</PageContainer>
); );
} }

View File

@@ -3,6 +3,8 @@ import Link from "next/link";
import { Card, CardHeader, CardContent } from "@/components/ui/Card"; import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
export default function TaskTemplatesPage() { export default function TaskTemplatesPage() {
const templates = db const templates = db
@@ -12,15 +14,12 @@ export default function TaskTemplatesPage() {
` `
) )
.all(); .all();
return ( return (
<div className="min-h-screen bg-gray-50"> <PageContainer>
<div className="max-w-6xl mx-auto p-6"> <PageHeader
<div className="flex justify-between items-center mb-8"> title="Task Templates"
<div> description="Manage reusable task templates"
<h1 className="text-3xl font-bold text-gray-900">Task Templates</h1> action={
<p className="text-gray-600 mt-1">Manage reusable task templates</p>
</div>
<Link href="/tasks/templates/new"> <Link href="/tasks/templates/new">
<Button variant="primary" size="lg"> <Button variant="primary" size="lg">
<svg <svg
@@ -39,77 +38,77 @@ export default function TaskTemplatesPage() {
New Template New Template
</Button> </Button>
</Link> </Link>
</div> }
/>
{templates.length === 0 ? ( {templates.length === 0 ? (
<Card> <Card>
<CardContent className="text-center py-12"> <CardContent className="text-center py-12">
<div className="text-gray-400 mb-4"> <div className="text-gray-400 mb-4">
<svg <svg
className="w-16 h-16 mx-auto" className="w-16 h-16 mx-auto"
fill="currentColor" fill="currentColor"
viewBox="0 0 20 20" 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">
No task templates yet
</h3>
<p className="text-gray-500 mb-6">
Create reusable task templates to streamline your workflow
</p>
<Link href="/tasks/templates/new">
<Button variant="primary">Create First Template</Button>
</Link>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{templates.map((template) => (
<Card
key={template.task_id}
className="hover:shadow-md transition-shadow"
> >
<CardContent className="p-6"> <path
<div className="flex items-start justify-between mb-4"> fillRule="evenodd"
<h3 className="text-lg font-semibold text-gray-900 truncate pr-2"> 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"
{template.name} clipRule="evenodd"
</h3> />
<Badge variant="primary" size="sm"> </svg>
{template.max_wait_days} days </div>
</Badge> <h3 className="text-lg font-medium text-gray-900 mb-2">
</div> No task templates yet
{template.description && ( </h3>
<p className="text-gray-600 text-sm mb-4 line-clamp-2"> <p className="text-gray-500 mb-6">
{template.description} Create reusable task templates to streamline your workflow
</p> </p>
)}{" "} <Link href="/tasks/templates/new">
<div className="flex items-center justify-between"> <Button variant="primary">Create First Template</Button>
<span className="text-xs text-gray-500"> </Link>
Template ID: {template.task_id} </CardContent>
</span> </Card>
<div className="flex space-x-2"> ) : (
<Link href={`/tasks/templates/${template.task_id}/edit`}> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Button variant="outline" size="sm"> {templates.map((template) => (
Edit <Card
</Button> key={template.task_id}
</Link> className="hover:shadow-md transition-shadow"
<Button variant="secondary" size="sm"> >
Duplicate <CardContent className="p-6">
<div className="flex items-start justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 truncate pr-2">
{template.name}
</h3>
<Badge variant="primary" size="sm">
{template.max_wait_days} days
</Badge>
</div>
{template.description && (
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
{template.description}
</p>
)}{" "}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
Template ID: {template.task_id}
</span>
<div className="flex space-x-2">
<Link href={`/tasks/templates/${template.task_id}/edit`}>
<Button variant="outline" size="sm">
Edit
</Button> </Button>
</div> </Link>
<Button variant="secondary" size="sm">
Duplicate
</Button>
</div> </div>
</CardContent> </div>
</Card> </CardContent>
))} </Card>
</div> ))}{" "}
)} </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 };