Refactor App layout and enhance UI components for better usability

- Changed the default active tab in the App component from 'profiles' to 'main'.
- Improved the layout of the main content area to better accommodate different views, including profiles, generation forms, and history.
- Updated Sidebar component to reflect the new tab structure with a 'main' tab.
- Enhanced the GenerationForm to utilize the selected profile from the UI store, improving user feedback when no profile is selected.
- Added a new CircleButton component for better icon button interactions.
- Adjusted styles in various components for improved responsiveness and visual consistency.
This commit is contained in:
Jamie Pine
2026-01-25 14:44:42 -08:00
parent 05036dfef6
commit 98cfe2dc5a
16 changed files with 352 additions and 224 deletions
@@ -1,8 +1,9 @@
import { Edit, Mic, Trash2 } from 'lucide-react';
import { Edit, Eye, Mic, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { CircleButton } from '@/components/ui/circle-button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils/cn';
import type { VoiceProfileResponse } from '@/lib/api/types';
import { useDeleteProfile } from '@/lib/hooks/useProfiles';
import { formatDate } from '@/lib/utils/format';
@@ -18,6 +19,14 @@ export function ProfileCard({ profile }: ProfileCardProps) {
const deleteProfile = useDeleteProfile();
const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);
const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen);
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId);
const isSelected = selectedProfileId === profile.id;
const handleSelect = () => {
setSelectedProfileId(isSelected ? null : profile.id);
};
const handleEdit = () => {
setEditingProfileId(profile.id);
@@ -35,39 +44,51 @@ export function ProfileCard({ profile }: ProfileCardProps) {
return (
<>
<Card
className="cursor-pointer hover:shadow-lg transition-shadow"
onClick={() => setDetailOpen(true)}
className={cn(
"cursor-pointer hover:shadow-md transition-all",
isSelected && "ring-2 ring-primary shadow-md"
)}
onClick={handleSelect}
>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Mic className="h-5 w-5" />
{profile.name}
<CardHeader className="p-3 pb-2">
<CardTitle className="flex items-center justify-between gap-2 text-base font-medium">
<span className="flex items-center gap-1.5 min-w-0 flex-1">
<Mic className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{profile.name}</span>
</span>
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="icon" onClick={handleEdit} aria-label="Edit profile">
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
<div className="flex gap-0.5 shrink-0">
<CircleButton
icon={Eye}
onClick={(e) => {
e.stopPropagation();
setDetailOpen(true);
}}
aria-label="View details"
/>
<CircleButton
icon={Edit}
onClick={handleEdit}
aria-label="Edit profile"
/>
<CircleButton
icon={Trash2}
onClick={handleDelete}
disabled={deleteProfile.isPending}
aria-label="Delete profile"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
/>
</div>
</CardTitle>
</CardHeader>
<CardContent>
{profile.description && (
<p className="text-sm text-muted-foreground mb-2">{profile.description}</p>
)}
<div className="flex gap-2 mb-2">
<Badge variant="outline">{profile.language}</Badge>
<CardContent className="p-3 pt-0">
<p className="text-xs text-muted-foreground mb-1.5 line-clamp-2 leading-relaxed">
{profile.description || 'No description'}
</p>
<div className="flex items-center justify-between gap-2 mb-1">
<Badge variant="outline" className="text-xs h-5 px-1.5">
{profile.language}
</Badge>
<p className="text-xs text-muted-foreground/60 text-right">{formatDate(profile.created_at)}</p>
</div>
<p className="text-xs text-muted-foreground">Created {formatDate(profile.created_at)}</p>
</CardContent>
</Card>
@@ -26,9 +26,11 @@ export function ProfileList() {
);
}
const allProfiles = profiles || [];
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex items-center justify-between mb-4 shrink-0">
<h2 className="text-2xl font-bold">Voice Profiles</h2>
<Button onClick={() => setDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
@@ -36,26 +38,28 @@ export function ProfileList() {
</Button>
</div>
{profiles && profiles.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Mic className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">
No voice profiles yet. Create your first profile to get started.
</p>
<Button onClick={() => setDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Create Profile
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{profiles?.map((profile) => (
<ProfileCard key={profile.id} profile={profile} />
))}
</div>
)}
<div className="min-h-[280px] shrink-0">
{allProfiles.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Mic className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">
No voice profiles yet. Create your first profile to get started.
</p>
<Button onClick={() => setDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Create Profile
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4 grid-cols-3 auto-rows-fr">
{allProfiles.map((profile) => (
<ProfileCard key={profile.id} profile={profile} />
))}
</div>
)}
</div>
<ProfileForm />
</div>