refactor: update UI components and styles for improved consistency and functionality
- Refactored Skeleton component to include data-slot attribute and updated class names. - Enhanced Switch component with size prop and improved styling. - Updated Tooltip components to include data-slot attributes and improved styling. - Refactored global CSS to use custom properties for theming and improved dark mode support. - Added useIsMobile hook for responsive design handling. - Updated IPC link implementation for tRPC in Electron. - Adjusted ProjectsPage layout for better responsiveness. - Removed outdated Tailwind configuration file and integrated Tailwind CSS with Vite.
This commit is contained in:
@@ -80,13 +80,54 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
const [editDialog, setEditDialog] = useState<{ id: string; name: string; clientId: string | null } | null>(null);
|
||||
const [editClientValue, setEditClientValue] = useState<string>(NO_CLIENT_KEY);
|
||||
|
||||
// New-project dialog state
|
||||
const [newProjectOpen, setNewProjectOpen] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState('');
|
||||
const [newProjectClientId, setNewProjectClientId] = useState<string>(NO_CLIENT_KEY);
|
||||
const [newProjectSubClientId, setNewProjectSubClientId] = useState<string>(NO_CLIENT_KEY);
|
||||
// Inline client creation
|
||||
const [creatingClient, setCreatingClient] = useState(false);
|
||||
const [newClientName, setNewClientName] = useState('');
|
||||
// Inline sub-client creation
|
||||
const [creatingSubClient, setCreatingSubClient] = useState(false);
|
||||
const [newSubClientName, setNewSubClientName] = useState('');
|
||||
|
||||
const { data: projectList = [] } = trpc.projects.list.useQuery(
|
||||
{ includeArchived: showArchived },
|
||||
);
|
||||
const { data: clientList = [] } = trpc.clients.list.useQuery();
|
||||
|
||||
// Derived: top-level clients and sub-clients grouped by parentId
|
||||
const topLevelClients = useMemo(
|
||||
() => clientList.filter((c) => !c.parentId),
|
||||
[clientList],
|
||||
);
|
||||
const subClientsByParent = useMemo(() => {
|
||||
const m = new Map<string, typeof clientList>();
|
||||
for (const c of clientList) {
|
||||
if (c.parentId) {
|
||||
const arr = m.get(c.parentId);
|
||||
if (arr) arr.push(c);
|
||||
else m.set(c.parentId, [c]);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}, [clientList]);
|
||||
|
||||
const createClientMutation = trpc.clients.create.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.clients.list.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const createMutation = trpc.projects.create.useMutation({
|
||||
onSuccess: () => { void utils.projects.list.invalidate(); },
|
||||
onSuccess: (data, variables) => {
|
||||
// Auto-expand the matching client group
|
||||
const groupKey = variables.clientId ?? NO_CLIENT_KEY;
|
||||
setExpanded((prev) => new Set([...prev, groupKey]));
|
||||
onSelectProject(data.id);
|
||||
void utils.projects.list.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = trpc.projects.update.useMutation({
|
||||
@@ -140,8 +181,58 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
});
|
||||
}
|
||||
|
||||
function handleNewProject() {
|
||||
createMutation.mutate({ name: 'New Project' });
|
||||
function handleOpenNewProject() {
|
||||
setNewProjectName('');
|
||||
setNewProjectClientId(NO_CLIENT_KEY);
|
||||
setNewProjectSubClientId(NO_CLIENT_KEY);
|
||||
setCreatingClient(false);
|
||||
setNewClientName('');
|
||||
setCreatingSubClient(false);
|
||||
setNewSubClientName('');
|
||||
setNewProjectOpen(true);
|
||||
}
|
||||
|
||||
async function handleCreateProject() {
|
||||
const name = newProjectName.trim();
|
||||
if (!name) return;
|
||||
|
||||
let resolvedClientId: string | undefined;
|
||||
|
||||
// If creating a brand-new client first
|
||||
if (creatingClient && newClientName.trim()) {
|
||||
const result = await createClientMutation.mutateAsync({ name: newClientName.trim() });
|
||||
resolvedClientId = result.id;
|
||||
|
||||
// If also creating a sub-client under the new client
|
||||
if (creatingSubClient && newSubClientName.trim()) {
|
||||
const subResult = await createClientMutation.mutateAsync({
|
||||
name: newSubClientName.trim(),
|
||||
parentId: resolvedClientId,
|
||||
});
|
||||
resolvedClientId = subResult.id;
|
||||
}
|
||||
} else if (newProjectClientId !== NO_CLIENT_KEY) {
|
||||
// User picked an existing client
|
||||
if (creatingSubClient && newSubClientName.trim()) {
|
||||
// Create a new sub-client under the selected client
|
||||
const subResult = await createClientMutation.mutateAsync({
|
||||
name: newSubClientName.trim(),
|
||||
parentId: newProjectClientId,
|
||||
});
|
||||
resolvedClientId = subResult.id;
|
||||
} else if (newProjectSubClientId !== NO_CLIENT_KEY) {
|
||||
// User picked an existing sub-client
|
||||
resolvedClientId = newProjectSubClientId;
|
||||
} else {
|
||||
// Just the parent client, no sub-client
|
||||
resolvedClientId = newProjectClientId;
|
||||
}
|
||||
}
|
||||
|
||||
createMutation.mutate(
|
||||
{ name, clientId: resolvedClientId },
|
||||
{ onSuccess: () => setNewProjectOpen(false) },
|
||||
);
|
||||
}
|
||||
|
||||
function handleArchiveToggle(id: string, currentStatus: string | null) {
|
||||
@@ -186,7 +277,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleNewProject}
|
||||
onClick={handleOpenNewProject}
|
||||
disabled={createMutation.isPending}
|
||||
aria-label="New Project"
|
||||
>
|
||||
@@ -237,7 +328,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleNewProject}
|
||||
onClick={handleOpenNewProject}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
@@ -286,7 +377,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
<div
|
||||
key={project.id}
|
||||
className={cn(
|
||||
'group relative flex items-center h-7 rounded-md text-sm cursor-pointer hover:bg-accent transition-colors',
|
||||
'group flex items-center h-7 rounded-md text-sm cursor-pointer hover:bg-accent transition-colors',
|
||||
selectedProjectId === project.id && 'bg-sidebar-accent',
|
||||
project.status === 'archived' && 'opacity-60',
|
||||
)}
|
||||
@@ -303,9 +394,8 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'size-5 p-0 ml-1 text-muted-foreground hover:bg-muted shrink-0',
|
||||
'relative z-10 h-5 w-5 min-h-0 min-w-0 p-0 ml-1 text-muted-foreground hover:bg-muted shrink-0',
|
||||
'opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
@@ -348,7 +438,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteProjectId({ id: project.id, name: project.name });
|
||||
@@ -399,6 +489,159 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* New Project dialog */}
|
||||
<Dialog open={newProjectOpen} onOpenChange={setNewProjectOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Give your project a name and optionally assign it to a client.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
{/* Project name */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="new-project-name" className="text-sm font-medium">Project Name</label>
|
||||
<Input
|
||||
id="new-project-name"
|
||||
placeholder="e.g. Website Redesign"
|
||||
value={newProjectName}
|
||||
onChange={(e) => setNewProjectName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Client selection */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">Client <span className="text-muted-foreground font-normal">(optional)</span></label>
|
||||
{creatingClient ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="New client name"
|
||||
value={newClientName}
|
||||
onChange={(e) => setNewClientName(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCreatingClient(false);
|
||||
setNewClientName('');
|
||||
setCreatingSubClient(false);
|
||||
setNewSubClientName('');
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={newProjectClientId}
|
||||
onValueChange={(v) => {
|
||||
setNewProjectClientId(v);
|
||||
setNewProjectSubClientId(NO_CLIENT_KEY);
|
||||
setCreatingSubClient(false);
|
||||
setNewSubClientName('');
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Select a client" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NO_CLIENT_KEY}>None (Internal)</SelectItem>
|
||||
{topLevelClients.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCreatingClient(true)}
|
||||
>
|
||||
<Plus className="size-3.5 mr-1" />New
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sub-client selection — only when a client is selected or being created */}
|
||||
{(newProjectClientId !== NO_CLIENT_KEY || (creatingClient && newClientName.trim())) && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">Sub-client <span className="text-muted-foreground font-normal">(optional)</span></label>
|
||||
{creatingSubClient ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="New sub-client name"
|
||||
value={newSubClientName}
|
||||
onChange={(e) => setNewSubClientName(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCreatingSubClient(false);
|
||||
setNewSubClientName('');
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
) : creatingClient ? (
|
||||
/* When creating a new client there are no existing sub-clients to pick */
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={() => setCreatingSubClient(true)}
|
||||
>
|
||||
<Plus className="size-3.5 mr-1" />New Sub-client
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={newProjectSubClientId}
|
||||
onValueChange={setNewProjectSubClientId}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Select a sub-client" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NO_CLIENT_KEY}>None</SelectItem>
|
||||
{(subClientsByParent.get(newProjectClientId) ?? []).map((sc) => (
|
||||
<SelectItem key={sc.id} value={sc.id}>{sc.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCreatingSubClient(true)}
|
||||
>
|
||||
<Plus className="size-3.5 mr-1" />New
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setNewProjectOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleCreateProject}
|
||||
disabled={!newProjectName.trim() || createMutation.isPending || createClientMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending || createClientMutation.isPending ? 'Creating…' : 'Create Project'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit client dialog */}
|
||||
<Dialog
|
||||
open={!!editDialog}
|
||||
|
||||
Reference in New Issue
Block a user