Seed: project-management template (23 files)
This commit is contained in:
@@ -0,0 +1,296 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Settings as SettingsIcon, Users, Puzzle, Bell, Save,
|
||||||
|
Mail, Shield, Crown, ExternalLink, CreditCard, Check
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import Sidebar from "@/components/Sidebar";
|
||||||
|
import Header from "@/components/Header";
|
||||||
|
import { useSaasMutation } from "@/hooks/use-tygarun";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "general", label: "General", icon: SettingsIcon },
|
||||||
|
{ id: "team", label: "Team", icon: Users },
|
||||||
|
{ id: "integrations", label: "Integrations", icon: Puzzle },
|
||||||
|
{ id: "billing", label: "Billing", icon: CreditCard },
|
||||||
|
{ id: "notifications", label: "Notifications", icon: Bell },
|
||||||
|
];
|
||||||
|
|
||||||
|
const plans = [
|
||||||
|
{ name: "Free", price: "$0", period: "/mo", amount: 0, current: true, features: ["Up to 3 projects", "5 team members", "Basic kanban boards", "Community support"] },
|
||||||
|
{ name: "Pro", price: "$12", period: "/user/mo", amount: 1200, popular: true, features: ["Unlimited projects", "Unlimited members", "Timeline & Gantt", "Time tracking", "Priority support"] },
|
||||||
|
{ name: "Enterprise", price: "Custom", period: "", amount: 0, features: ["Everything in Pro", "SSO & SAML", "Advanced permissions", "Dedicated success manager", "99.9% uptime SLA"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const teamMembers = [
|
||||||
|
{ name: "Alex Kim", email: "alex@taskflow.io", initials: "AK", role: "Admin", color: "bg-violet-500" },
|
||||||
|
{ name: "Maria Rodriguez", email: "maria@taskflow.io", initials: "MR", role: "Editor", color: "bg-indigo-500" },
|
||||||
|
{ name: "Jordan Lee", email: "jordan@taskflow.io", initials: "JL", role: "Editor", color: "bg-emerald-500" },
|
||||||
|
{ name: "Sam Chen", email: "sam@taskflow.io", initials: "SC", role: "Viewer", color: "bg-amber-500" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const integrations = [
|
||||||
|
{ name: "Slack", description: "Send notifications and updates to Slack channels", icon: "S", connected: true, color: "bg-[#4A154B]" },
|
||||||
|
{ name: "GitHub", description: "Link commits and PRs to tasks automatically", icon: "G", connected: true, color: "bg-slate-900" },
|
||||||
|
{ name: "Figma", description: "Embed designs and sync design tokens", icon: "F", connected: false, color: "bg-[#F24E1E]" },
|
||||||
|
{ name: "Jira", description: "Import and sync issues from Jira projects", icon: "J", connected: false, color: "bg-[#0052CC]" },
|
||||||
|
{ name: "Notion", description: "Sync documentation and project wikis", icon: "N", connected: true, color: "bg-slate-800" },
|
||||||
|
{ name: "Google Drive", description: "Attach files and documents from Google Drive", icon: "G", connected: false, color: "bg-[#4285F4]" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const notificationSettings = [
|
||||||
|
{ id: "task-assigned", label: "Task assigned to me", description: "Get notified when someone assigns a task to you", enabled: true },
|
||||||
|
{ id: "task-completed", label: "Task completed", description: "Get notified when tasks you created are completed", enabled: true },
|
||||||
|
{ id: "mentions", label: "Mentions", description: "Get notified when someone mentions you in a comment", enabled: true },
|
||||||
|
{ id: "project-updates", label: "Project updates", description: "Receive weekly digest of project progress", enabled: false },
|
||||||
|
{ id: "due-reminders", label: "Due date reminders", description: "Get reminded before tasks are due", enabled: true },
|
||||||
|
{ id: "new-comments", label: "New comments", description: "Notify on new comments in tasks you follow", enabled: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState("general");
|
||||||
|
const [notifications, setNotifications] = useState(notificationSettings);
|
||||||
|
const { mutate: startCheckout, loading: checkoutBusy } = useSaasMutation("billing", "/checkout");
|
||||||
|
|
||||||
|
const toggleNotification = (id: string) => {
|
||||||
|
setNotifications(prev => prev.map(n => n.id === id ? { ...n, enabled: !n.enabled } : n));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
toast("Settings saved successfully");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpgrade = async (plan: { name: string; amount: number }) => {
|
||||||
|
if (plan.name === "Enterprise") { toast("Our team will reach out about Enterprise plans"); return; }
|
||||||
|
try {
|
||||||
|
const res: any = await startCheckout({ plan: plan.name, amount: plan.amount });
|
||||||
|
if (res?.url) { window.location.href = res.url; return; }
|
||||||
|
toast(`Upgraded to \${plan.name}`);
|
||||||
|
} catch {
|
||||||
|
toast("Checkout could not be started. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-slate-50">
|
||||||
|
<Sidebar activePath="/settings" />
|
||||||
|
<div className="flex-1 lg:ml-64">
|
||||||
|
<Header />
|
||||||
|
<main className="p-6 space-y-6">
|
||||||
|
{/* ── Header ── */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900 animate-fade-up">Settings</h1>
|
||||||
|
<p className="text-slate-500 mt-1">Manage your workspace preferences and team</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Tabs ── */}
|
||||||
|
<div className="flex gap-1 border-b border-slate-200">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors border-b-2 -mb-[1px] \${
|
||||||
|
activeTab === tab.id
|
||||||
|
? "border-violet-600 text-violet-600"
|
||||||
|
: "border-transparent text-slate-500 hover:text-slate-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<tab.icon className="h-4 w-4" />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── General Tab ── */}
|
||||||
|
{activeTab === "general" && (
|
||||||
|
<Card className="border-0 shadow-sm animate-fade-up">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Workspace Settings</CardTitle>
|
||||||
|
<CardDescription>Configure your workspace name, description, and preferences</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ws-name">Workspace Name</Label>
|
||||||
|
<Input id="ws-name" defaultValue="TaskFlow Workspace" className="max-w-md" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ws-desc">Description</Label>
|
||||||
|
<Textarea id="ws-desc" defaultValue="Our team workspace for managing projects and tasks across all departments." className="max-w-md h-24 resize-none" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Timezone</Label>
|
||||||
|
<Select defaultValue="utc-8">
|
||||||
|
<SelectTrigger className="max-w-md">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="utc-8">Pacific Time (UTC-8)</SelectItem>
|
||||||
|
<SelectItem value="utc-5">Eastern Time (UTC-5)</SelectItem>
|
||||||
|
<SelectItem value="utc+0">UTC (UTC+0)</SelectItem>
|
||||||
|
<SelectItem value="utc+1">Central European (UTC+1)</SelectItem>
|
||||||
|
<SelectItem value="utc+8">Singapore (UTC+8)</SelectItem>
|
||||||
|
<SelectItem value="utc+9">Japan (UTC+9)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<Button className="bg-violet-600 hover:bg-violet-700 text-white" onClick={handleSave}>
|
||||||
|
<Save className="mr-2 h-4 w-4" /> Save Changes
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Team Tab ── */}
|
||||||
|
{activeTab === "team" && (
|
||||||
|
<div className="space-y-6 animate-fade-up">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm text-slate-500">{teamMembers.length} members in this workspace</p>
|
||||||
|
<Button className="bg-violet-600 hover:bg-violet-700 text-white">
|
||||||
|
<Mail className="mr-2 h-4 w-4" /> Invite Member
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{teamMembers.map((member) => (
|
||||||
|
<Card key={member.email} className="border-0 shadow-sm">
|
||||||
|
<CardContent className="p-4 flex items-center gap-4">
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarFallback className={`text-sm text-white \${member.color}`}>{member.initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-sm font-medium text-slate-900">{member.name}</p>
|
||||||
|
{member.role === "Admin" && <Crown className="h-3.5 w-3.5 text-amber-500" />}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500">{member.email}</p>
|
||||||
|
</div>
|
||||||
|
<Select defaultValue={member.role.toLowerCase()}>
|
||||||
|
<SelectTrigger className="w-[120px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
<SelectItem value="editor">Editor</SelectItem>
|
||||||
|
<SelectItem value="viewer">Viewer</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Integrations Tab ── */}
|
||||||
|
{activeTab === "integrations" && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 animate-fade-up">
|
||||||
|
{integrations.map((integration) => (
|
||||||
|
<Card key={integration.name} className="border-0 shadow-sm hover:shadow-md transition-all duration-200">
|
||||||
|
<CardContent className="p-5">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className={`h-10 w-10 rounded-lg \${integration.color} flex items-center justify-center`}>
|
||||||
|
<span className="text-sm font-bold text-white">{integration.icon}</span>
|
||||||
|
</div>
|
||||||
|
{integration.connected && (
|
||||||
|
<Badge className="bg-emerald-100 text-emerald-700 text-xs border-0">Connected</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-slate-900 mb-1">{integration.name}</h3>
|
||||||
|
<p className="text-xs text-slate-500 mb-4">{integration.description}</p>
|
||||||
|
<Button
|
||||||
|
variant={integration.connected ? "outline" : "default"}
|
||||||
|
size="sm"
|
||||||
|
className={`w-full \${!integration.connected ? "bg-violet-600 hover:bg-violet-700 text-white" : ""}`}
|
||||||
|
>
|
||||||
|
{integration.connected ? (
|
||||||
|
<>
|
||||||
|
<SettingsIcon className="mr-1 h-3.5 w-3.5" /> Configure
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ExternalLink className="mr-1 h-3.5 w-3.5" /> Connect
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Billing Tab ── */}
|
||||||
|
{activeTab === "billing" && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 animate-fade-up">
|
||||||
|
{plans.map((plan) => (
|
||||||
|
<Card key={plan.name} className={`border shadow-sm \${plan.popular ? "border-violet-500 ring-1 ring-violet-500" : "border-slate-100"}`}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg">{plan.name}</CardTitle>
|
||||||
|
{plan.popular && <Badge className="bg-violet-100 text-violet-700 border-0 text-xs">Most Popular</Badge>}
|
||||||
|
{plan.current && <Badge className="bg-slate-100 text-slate-600 border-0 text-xs">Current</Badge>}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline gap-1 pt-2">
|
||||||
|
<span className="text-3xl font-bold tabular-nums text-slate-900">{plan.price}</span>
|
||||||
|
<span className="text-sm text-slate-500">{plan.period}</span>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{plan.features.map((f) => (
|
||||||
|
<li key={f} className="flex items-center gap-2 text-sm text-slate-600">
|
||||||
|
<Check className="h-4 w-4 text-emerald-500 shrink-0" /> {f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleUpgrade(plan)}
|
||||||
|
disabled={checkoutBusy || plan.current}
|
||||||
|
variant={plan.popular ? "default" : "outline"}
|
||||||
|
className={`w-full min-h-[48px] \${plan.popular ? "bg-violet-600 hover:bg-violet-700 text-white" : ""}`}
|
||||||
|
>
|
||||||
|
{plan.current ? "Current Plan" : plan.name === "Enterprise" ? "Contact Sales" : `Upgrade to \${plan.name}`}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Notifications Tab ── */}
|
||||||
|
{activeTab === "notifications" && (
|
||||||
|
<Card className="border-0 shadow-sm animate-fade-up">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Notification Preferences</CardTitle>
|
||||||
|
<CardDescription>Choose which notifications you want to receive</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-1">
|
||||||
|
{notifications.map((setting, i) => (
|
||||||
|
<div key={setting.id}>
|
||||||
|
<div className="flex items-center justify-between py-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-900">{setting.label}</p>
|
||||||
|
<p className="text-xs text-slate-500 mt-0.5">{setting.description}</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={setting.enabled} onCheckedChange={() => toggleNotification(setting.id)} className="data-[state=checked]:bg-violet-600" />
|
||||||
|
</div>
|
||||||
|
{i < notifications.length - 1 && <Separator />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user