mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 14:20:42 -07:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user