Seed: product-management template (23 files)
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user