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">
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"
> >
<Link href="/contracts/new">
<Button variant="primary" size="lg">
<span className="mr-2"></span> <span className="mr-2"></span>
Nowa umowa Nowa umowa
</Link> </Button>
</div> </Link>{" "}
</PageHeader>
{/* 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"
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> <FilterBar filters={filterOptions} className="mb-6" />{" "}
{/* 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,15 +47,16 @@ 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>
{loading ? (
<LoadingState message="Loading dashboard data..." />
) : (
<>
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card> <Card>
@@ -354,7 +358,8 @@ export default function Home() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </>
</div> )}
</PageContainer>
); );
} }

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,11 +39,20 @@ 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}
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"> <Link href="/projects">
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<svg <svg
@@ -65,19 +75,8 @@ export default async function ProjectViewPage({ params }) {
<Button variant="secondary">Edit Project</Button> <Button variant="secondary">Edit Project</Button>
</Link> </Link>
</div> </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"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card> <Card>
@@ -101,9 +100,7 @@ export default async function ProjectViewPage({ params }) {
<p className="text-gray-900">{project.address}</p> <p className="text-gray-900">{project.address}</p>
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500"> <span className="text-sm font-medium text-gray-500">Plot</span>
Plot
</span>
<p className="text-gray-900">{project.plot}</p> <p className="text-gray-900">{project.plot}</p>
</div> </div>
<div> <div>
@@ -113,9 +110,7 @@ export default async function ProjectViewPage({ params }) {
<p className="text-gray-900">{project.district}</p> <p className="text-gray-900">{project.district}</p>
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500"> <span className="text-sm font-medium text-gray-500">Unit</span>
Unit
</span>
<p className="text-gray-900">{project.unit}</p> <p className="text-gray-900">{project.unit}</p>
</div> </div>
<div> <div>
@@ -136,16 +131,12 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500"> <span className="text-sm font-medium text-gray-500">Contact</span>
Contact
</span>
<p className="text-gray-900">{project.contact}</p> <p className="text-gray-900">{project.contact}</p>
</div> </div>
{project.notes && ( {project.notes && (
<div> <div>
<span className="text-sm font-medium text-gray-500"> <span className="text-sm font-medium text-gray-500">Notes</span>
Notes
</span>
<p className="text-gray-900">{project.notes}</p> <p className="text-gray-900">{project.notes}</p>
</div> </div>
)} )}
@@ -228,7 +219,6 @@ export default async function ProjectViewPage({ params }) {
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div> </PageContainer>
</div>
); );
} }

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,13 +58,8 @@ 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">
<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>
<Link href="/projects/new"> <Link href="/projects/new">
<Button variant="primary" size="lg"> <Button variant="primary" size="lg">
<svg <svg
@@ -78,86 +77,16 @@ export default function ProjectListPage() {
</svg> </svg>
Add Project Add Project
</Button> </Button>
</Link>{" "} </Link>
</div>{" "} </PageHeader>
{/* Search Bar */}
<div className="mb-8"> <SearchBar
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4"> searchTerm={searchTerm}
<div className="flex items-center space-x-4"> onSearchChange={handleSearchChange}
<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..." placeholder="Search by project name, WP, plot, or investment number..."
value={searchTerm} resultsCount={filteredProjects.length}
onChange={handleSearchChange} resultsText="projects"
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>
{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 ? ( {filteredProjects.length === 0 && searchTerm ? (
<Card> <Card>
<CardContent className="text-center py-12"> <CardContent className="text-center py-12">
@@ -178,8 +107,8 @@ export default function ProjectListPage() {
No projects found No projects found
</h3> </h3>
<p className="text-gray-500 mb-6"> <p className="text-gray-500 mb-6">
No projects match your search criteria. Try adjusting your No projects match your search criteria. Try adjusting your search
search terms. terms.
</p> </p>
<Button variant="outline" onClick={() => setSearchTerm("")}> <Button variant="outline" onClick={() => setSearchTerm("")}>
Clear Search Clear Search
@@ -269,12 +198,11 @@ export default function ProjectListPage() {
View View
</Button> </Button>
</Link> </Link>
</div> </div>{" "}
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </PageContainer>
</div>
); );
} }

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,32 +158,57 @@ 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>
); );
} }
return ( const filterOptions = [
<div className="min-h-screen bg-gray-50"> {
<div className="max-w-6xl mx-auto p-6"> label: "Status",
<div className="flex justify-between items-center mb-8"> value: statusFilter,
<div> onChange: (e) => setStatusFilter(e.target.value),
<h1 className="text-3xl font-bold text-gray-900">Project Tasks</h1> options: [
<p className="text-gray-600 mt-1"> { value: "all", label: "All" },
Monitor and manage tasks across all projects { value: "pending", label: "Pending" },
</p> { value: "in_progress", label: "In Progress" },
</div> { value: "completed", label: "Completed" },
</div> ],
},
{
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 */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<Card> <Card>
@@ -200,9 +230,7 @@ export default function ProjectTasksPage() {
</svg> </svg>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">Total Tasks</p>
Total Tasks
</p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{statusCounts.all} {statusCounts.all}
</p> </p>
@@ -210,7 +238,6 @@ export default function ProjectTasksPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center"> <div className="flex items-center">
@@ -238,7 +265,6 @@ export default function ProjectTasksPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center"> <div className="flex items-center">
@@ -258,9 +284,7 @@ export default function ProjectTasksPage() {
</svg> </svg>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600"> <p className="text-sm font-medium text-gray-600">In Progress</p>
In Progress
</p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{statusCounts.in_progress} {statusCounts.in_progress}
</p> </p>
@@ -268,7 +292,6 @@ export default function ProjectTasksPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center"> <div className="flex items-center">
@@ -295,60 +318,8 @@ export default function ProjectTasksPage() {
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>{" "}
</div> </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 */} {/* Tasks List */}
{filteredTasks.length === 0 ? ( {filteredTasks.length === 0 ? (
<Card> <Card>
@@ -370,9 +341,7 @@ export default function ProjectTasksPage() {
No tasks found No tasks found
</h3> </h3>
<p className="text-gray-500 mb-6"> <p className="text-gray-500 mb-6">
{searchTerm || {searchTerm || statusFilter !== "all" || priorityFilter !== "all"
statusFilter !== "all" ||
priorityFilter !== "all"
? "Try adjusting your filters to see more tasks" ? "Try adjusting your filters to see more tasks"
: "No tasks have been created yet"} : "No tasks have been created yet"}
</p> </p>
@@ -389,10 +358,7 @@ export default function ProjectTasksPage() {
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-semibold text-gray-900">
{task.task_name} {task.task_name}
</h3> </h3>
<Badge <Badge variant={getStatusVariant(task.status)} size="sm">
variant={getStatusVariant(task.status)}
size="sm"
>
{getStatusDisplayName(task.status)} {getStatusDisplayName(task.status)}
</Badge> </Badge>
<Badge <Badge
@@ -418,9 +384,7 @@ export default function ProjectTasksPage() {
{task.wp && ( {task.wp && (
<div> <div>
<p className="text-sm text-gray-600">WP</p> <p className="text-sm text-gray-600">WP</p>
<p className="font-medium text-gray-900"> <p className="font-medium text-gray-900">{task.wp}</p>
{task.wp}
</p>
</div> </div>
)} )}
{task.plot && ( {task.plot && (
@@ -478,10 +442,9 @@ export default function ProjectTasksPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
))} ))}{" "}
</div> </div>
)} )}
</div> </PageContainer>
</div>
); );
} }

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,7 +38,8 @@ export default function TaskTemplatesPage() {
New Template New Template
</Button> </Button>
</Link> </Link>
</div> }
/>
{templates.length === 0 ? ( {templates.length === 0 ? (
<Card> <Card>
@@ -106,10 +106,9 @@ export default function TaskTemplatesPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
))} ))}{" "}
</div> </div>
)} )}
</div> </PageContainer>
</div>
); );
} }

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 };