Seed: product-management template (23 files)

This commit is contained in:
2026-07-23 22:14:58 +00:00
parent f78bc6f622
commit fe54c60807
+210
View File
@@ -0,0 +1,210 @@
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Plus, Search, ArrowUpDown, ThumbsUp, Calendar, Filter, LayoutList, LayoutGrid
} from "lucide-react";
import { toast } from "sonner";
import Sidebar from "@/components/Sidebar";
import Header from "@/components/Header";
interface Feature {
id: number;
title: string;
status: string;
statusColor: string;
priority: string;
priorityColor: string;
votes: number;
assignee: string;
assigneeInitials: string;
assigneeColor: string;
targetDate: string;
category: string;
}
const features: Feature[] = [
{ id: 1, title: "Visual Roadmap Board", status: "In Progress", statusColor: "bg-blue-100 text-blue-700", priority: "P0", priorityColor: "bg-red-100 text-red-700", votes: 48, assignee: "Sarah Kim", assigneeInitials: "SK", assigneeColor: "bg-blue-500", targetDate: "May 15, 2026", category: "Core" },
{ id: 2, title: "Slack Integration", status: "In Progress", statusColor: "bg-blue-100 text-blue-700", priority: "P0", priorityColor: "bg-red-100 text-red-700", votes: 36, assignee: "Alex Lee", assigneeInitials: "AL", assigneeColor: "bg-sky-500", targetDate: "May 20, 2026", category: "Integrations" },
{ id: 3, title: "Feedback Analytics", status: "Planned", statusColor: "bg-slate-100 text-slate-700", priority: "P1", priorityColor: "bg-orange-100 text-orange-700", votes: 52, assignee: "Nina Kumar", assigneeInitials: "NK", assigneeColor: "bg-emerald-500", targetDate: "Jun 1, 2026", category: "Analytics" },
{ id: 4, title: "Priority Scoring Engine", status: "In Review", statusColor: "bg-amber-100 text-amber-700", priority: "P1", priorityColor: "bg-orange-100 text-orange-700", votes: 29, assignee: "Raj Joshi", assigneeInitials: "RJ", assigneeColor: "bg-indigo-500", targetDate: "May 25, 2026", category: "Core" },
{ id: 5, title: "Public Changelog", status: "Planned", statusColor: "bg-slate-100 text-slate-700", priority: "P1", priorityColor: "bg-orange-100 text-orange-700", votes: 31, assignee: "Sarah Kim", assigneeInitials: "SK", assigneeColor: "bg-blue-500", targetDate: "Jun 10, 2026", category: "Communication" },
{ id: 6, title: "API Access", status: "Planned", statusColor: "bg-slate-100 text-slate-700", priority: "P2", priorityColor: "bg-slate-100 text-slate-600", votes: 22, assignee: "Alex Lee", assigneeInitials: "AL", assigneeColor: "bg-sky-500", targetDate: "Jun 20, 2026", category: "Platform" },
{ id: 7, title: "Mobile App", status: "Exploring", statusColor: "bg-purple-100 text-purple-700", priority: "P2", priorityColor: "bg-slate-100 text-slate-600", votes: 41, assignee: "Nina Kumar", assigneeInitials: "NK", assigneeColor: "bg-emerald-500", targetDate: "Q3 2026", category: "Platform" },
{ id: 8, title: "AI Roadmap Generator", status: "Exploring", statusColor: "bg-purple-100 text-purple-700", priority: "P2", priorityColor: "bg-slate-100 text-slate-600", votes: 33, assignee: "Sarah Kim", assigneeInitials: "SK", assigneeColor: "bg-blue-500", targetDate: "Q3 2026", category: "AI" },
{ id: 9, title: "Custom Dashboards", status: "Shipped", statusColor: "bg-emerald-100 text-emerald-700", priority: "P0", priorityColor: "bg-red-100 text-red-700", votes: 64, assignee: "Raj Joshi", assigneeInitials: "RJ", assigneeColor: "bg-indigo-500", targetDate: "Apr 30, 2026", category: "Core" },
{ id: 10, title: "Team Permissions", status: "Shipped", statusColor: "bg-emerald-100 text-emerald-700", priority: "P1", priorityColor: "bg-orange-100 text-orange-700", votes: 27, assignee: "Alex Lee", assigneeInitials: "AL", assigneeColor: "bg-sky-500", targetDate: "Apr 20, 2026", category: "Platform" },
];
export default function FeaturesPage() {
const [statusFilter, setStatusFilter] = useState("all");
const [priorityFilter, setPriorityFilter] = useState("all");
const [assigneeFilter, setAssigneeFilter] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
const [sortAsc, setSortAsc] = useState(false);
const filtered = features
.filter(f => {
if (statusFilter !== "all" && f.status !== statusFilter) return false;
if (priorityFilter !== "all" && f.priority !== priorityFilter) return false;
if (assigneeFilter !== "all" && f.assigneeInitials !== assigneeFilter) return false;
if (searchQuery && !f.title.toLowerCase().includes(searchQuery.toLowerCase())) return false;
return true;
})
.sort((a, b) => sortAsc ? a.votes - b.votes : b.votes - a.votes);
return (
<div className="flex min-h-screen bg-slate-50">
<Sidebar activePath="/features" />
<div className="flex-1 lg:ml-64">
<Header />
<main className="p-6 space-y-6">
{/* ── Page Header ── */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-900 animate-fade-up">Features</h1>
<p className="text-slate-500 mt-1">{features.length} features across all stages</p>
</div>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="mr-2 h-4 w-4" /> New Feature
</Button>
</div>
{/* ── Filter Bar ── */}
<Card className="border-0 shadow-sm">
<CardContent className="p-4">
<div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<Input
placeholder="Search features..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-slate-50 border-slate-200 h-9"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="In Review">In Review</SelectItem>
<SelectItem value="Planned">Planned</SelectItem>
<SelectItem value="Exploring">Exploring</SelectItem>
<SelectItem value="Shipped">Shipped</SelectItem>
</SelectContent>
</Select>
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Priority</SelectItem>
<SelectItem value="P0">P0 - Critical</SelectItem>
<SelectItem value="P1">P1 - High</SelectItem>
<SelectItem value="P2">P2 - Medium</SelectItem>
</SelectContent>
</Select>
<Select value={assigneeFilter} onValueChange={setAssigneeFilter}>
<SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="Assignee" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Assignees</SelectItem>
<SelectItem value="SK">Sarah Kim</SelectItem>
<SelectItem value="AL">Alex Lee</SelectItem>
<SelectItem value="NK">Nina Kumar</SelectItem>
<SelectItem value="RJ">Raj Joshi</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
className="h-9"
onClick={() => setSortAsc(!sortAsc)}
>
<ArrowUpDown className="mr-2 h-4 w-4" />
{sortAsc ? "Least Voted" : "Most Voted"}
</Button>
</div>
</CardContent>
</Card>
{/* ── Feature Table ── */}
<Card className="border-0 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-100 bg-slate-50/50">
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wider px-6 py-3">Feature</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wider px-4 py-3">Status</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wider px-4 py-3">Priority</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wider px-4 py-3">Votes</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wider px-4 py-3">Assignee</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wider px-4 py-3">Target</th>
</tr>
</thead>
<tbody>
{filtered.map((feature) => (
<tr key={feature.id} className="border-b border-slate-50 backdrop-blur-sm bg-card/80 border-border/50 hover:bg-blue-50/30 transition-colors cursor-pointer">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-lg bg-blue-100 flex items-center justify-center shrink-0">
<LayoutList className="h-4 w-4 text-blue-600" />
</div>
<div>
<p className="font-medium text-slate-900 text-sm">{feature.title}</p>
<p className="text-xs text-slate-400 mt-0.5">{feature.category}</p>
</div>
</div>
</td>
<td className="px-4 py-4">
<Badge className={`\${feature.statusColor} text-xs font-medium border-0`}>{feature.status}</Badge>
</td>
<td className="px-4 py-4">
<Badge className={`\${feature.priorityColor} text-xs font-semibold border-0`}>{feature.priority}</Badge>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-1.5 text-sm text-slate-600">
<ThumbsUp className="h-3.5 w-3.5 text-slate-400" />
<span className="font-medium">{feature.votes}</span>
</div>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
<AvatarFallback className={`text-[10px] text-white \${feature.assigneeColor}`}>{feature.assigneeInitials}</AvatarFallback>
</Avatar>
<span className="text-sm text-slate-600 hidden xl:inline">{feature.assignee}</span>
</div>
</td>
<td className="px-4 py-4">
<span className="text-sm text-slate-500 flex items-center gap-1.5">
<Calendar className="h-3.5 w-3.5 text-slate-400" />
{feature.targetDate}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filtered.length === 0 && (
<div className="p-12 text-center">
<Filter className="h-8 w-8 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500 font-medium">No features match your filters</p>
<p className="text-slate-400 text-sm mt-1">Try adjusting your search or filter criteria</p>
</div>
)}
</Card>
</main>
</div>
</div>
);
}