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