Polish UI: enhance dialogs, command palette, and page layouts
Expand NewIssueDialog with richer form fields. Add NewProjectDialog. Enhance CommandPalette with more actions and search. Improve CompanySwitcher, EmptyState, and IssueProperties. Flesh out Activity, Companies, Dashboard, and Inbox pages with real content and layouts. Refine sidebar, routing, and dialog context. CSS tweaks for dark theme. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
251
ui/src/components/NewProjectDialog.tsx
Normal file
251
ui/src/components/NewProjectDialog.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { goalsApi } from "../api/goals";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Target,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import type { Goal } from "@paperclip/shared";
|
||||
|
||||
const projectStatuses = [
|
||||
{ value: "backlog", label: "Backlog" },
|
||||
{ value: "planned", label: "Planned" },
|
||||
{ value: "in_progress", label: "In Progress" },
|
||||
{ value: "completed", label: "Completed" },
|
||||
{ value: "cancelled", label: "Cancelled" },
|
||||
];
|
||||
|
||||
interface NewProjectDialogProps {
|
||||
onCreated?: () => void;
|
||||
}
|
||||
|
||||
export function NewProjectDialog({ onCreated }: NewProjectDialogProps) {
|
||||
const { newProjectOpen, closeNewProject } = useDialog();
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [status, setStatus] = useState("planned");
|
||||
const [goalId, setGoalId] = useState("");
|
||||
const [targetDate, setTargetDate] = useState("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
const [goalOpen, setGoalOpen] = useState(false);
|
||||
|
||||
const goalsFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as Goal[]);
|
||||
return goalsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data: goals } = useApi(goalsFetcher);
|
||||
|
||||
function reset() {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setStatus("planned");
|
||||
setGoalId("");
|
||||
setTargetDate("");
|
||||
setExpanded(false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!selectedCompanyId || !name.trim()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await projectsApi.create(selectedCompanyId, {
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
status,
|
||||
...(goalId ? { goalId } : {}),
|
||||
...(targetDate ? { targetDate } : {}),
|
||||
});
|
||||
reset();
|
||||
closeNewProject();
|
||||
onCreated?.();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
const currentGoal = (goals ?? []).find((g) => g.id === goalId);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={newProjectOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
reset();
|
||||
closeNewProject();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className={cn("p-0 gap-0", expanded ? "sm:max-w-2xl" : "sm:max-w-lg")}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
{selectedCompany && (
|
||||
<span className="bg-muted px-1.5 py-0.5 rounded text-xs font-medium">
|
||||
{selectedCompany.name.slice(0, 3).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-muted-foreground/60">›</span>
|
||||
<span>New project</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => { reset(); closeNewProject(); }}
|
||||
>
|
||||
<span className="text-lg leading-none">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="px-4 pt-3">
|
||||
<input
|
||||
className="w-full text-base font-medium bg-transparent outline-none placeholder:text-muted-foreground/50"
|
||||
placeholder="Project name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="px-4 pb-2">
|
||||
<textarea
|
||||
className={cn(
|
||||
"w-full bg-transparent outline-none text-sm text-muted-foreground placeholder:text-muted-foreground/40 resize-none",
|
||||
expanded ? "min-h-[160px]" : "min-h-[48px]"
|
||||
)}
|
||||
placeholder="Add description..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Property chips */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-2 border-t border-border flex-wrap">
|
||||
{/* Status */}
|
||||
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<StatusBadge status={status} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-40 p-1" align="start">
|
||||
{projectStatuses.map((s) => (
|
||||
<button
|
||||
key={s.value}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
s.value === status && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setStatus(s.value); setStatusOpen(false); }}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Goal */}
|
||||
<Popover open={goalOpen} onOpenChange={setGoalOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<Target className="h-3 w-3 text-muted-foreground" />
|
||||
{currentGoal ? currentGoal.title : "Goal"}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-1" align="start">
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
!goalId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setGoalId(""); setGoalOpen(false); }}
|
||||
>
|
||||
No goal
|
||||
</button>
|
||||
{(goals ?? []).map((g) => (
|
||||
<button
|
||||
key={g.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 truncate",
|
||||
g.id === goalId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setGoalId(g.id); setGoalOpen(false); }}
|
||||
>
|
||||
{g.title}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Target date */}
|
||||
<div className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
|
||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||
<input
|
||||
type="date"
|
||||
className="bg-transparent outline-none text-xs w-24"
|
||||
value={targetDate}
|
||||
onChange={(e) => setTargetDate(e.target.value)}
|
||||
placeholder="Target date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end px-4 py-2.5 border-t border-border">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!name.trim() || submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? "Creating..." : "Create project"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user