mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 09:05:17 -07:00
Enhance UI components and styles for improved user experience
- Added global styles to hide scrollbars across all browsers for a cleaner interface. - Refactored HistoryTable component to highlight currently playing audio with conditional styling. - Implemented a confirmation dialog for deleting voice profiles in ProfileCard, enhancing user feedback and preventing accidental deletions. - Updated ProfileList to rename the section from 'Voice Profiles' to 'Voicebox' for better branding and clarity.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { apiClient } from '@/lib/api/client';
|
||||||
import { useDeleteGeneration, useHistory } from '@/lib/hooks/useHistory';
|
import { useDeleteGeneration, useHistory } from '@/lib/hooks/useHistory';
|
||||||
import { formatDate, formatDuration } from '@/lib/utils/format';
|
import { formatDate, formatDuration } from '@/lib/utils/format';
|
||||||
@@ -60,8 +61,6 @@ export function HistoryTable() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full min-h-0">
|
<div className="flex flex-col h-full min-h-0">
|
||||||
<h2 className="text-2xl font-bold mb-4 shrink-0">Generation History</h2>
|
|
||||||
|
|
||||||
{history.length === 0 ? (
|
{history.length === 0 ? (
|
||||||
<div className="text-center py-12 text-muted-foreground flex-1 flex items-center justify-center">
|
<div className="text-center py-12 text-muted-foreground flex-1 flex items-center justify-center">
|
||||||
No generation history yet. Generate your first audio to see it here.
|
No generation history yet. Generate your first audio to see it here.
|
||||||
@@ -81,8 +80,13 @@ export function HistoryTable() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{history.map((gen) => (
|
{history.map((gen) => {
|
||||||
<TableRow key={gen.id}>
|
const isCurrentlyPlaying = currentAudioId === gen.id && isPlaying;
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={gen.id}
|
||||||
|
className={cn(isCurrentlyPlaying && 'bg-muted/50')}
|
||||||
|
>
|
||||||
<TableCell className="max-w-[200px] truncate">{gen.text}</TableCell>
|
<TableCell className="max-w-[200px] truncate">{gen.text}</TableCell>
|
||||||
<TableCell>{gen.profile_name}</TableCell>
|
<TableCell>{gen.profile_name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -123,7 +127,8 @@ export function HistoryTable() {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { Edit, Eye, Mic, Trash2 } from 'lucide-react';
|
import { Edit, Eye, Mic, Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { CircleButton } from '@/components/ui/circle-button';
|
import { CircleButton } from '@/components/ui/circle-button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import type { VoiceProfileResponse } from '@/lib/api/types';
|
import type { VoiceProfileResponse } from '@/lib/api/types';
|
||||||
@@ -16,6 +25,7 @@ interface ProfileCardProps {
|
|||||||
|
|
||||||
export function ProfileCard({ profile }: ProfileCardProps) {
|
export function ProfileCard({ profile }: ProfileCardProps) {
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const deleteProfile = useDeleteProfile();
|
const deleteProfile = useDeleteProfile();
|
||||||
const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);
|
const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);
|
||||||
const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen);
|
const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen);
|
||||||
@@ -33,12 +43,14 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
setProfileDialogOpen(true);
|
setProfileDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||||
if (
|
e.stopPropagation();
|
||||||
confirm(`Are you sure you want to delete "${profile.name}"? This action cannot be undone.`)
|
setDeleteDialogOpen(true);
|
||||||
) {
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
deleteProfile.mutate(profile.id);
|
deleteProfile.mutate(profile.id);
|
||||||
}
|
setDeleteDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,12 +79,15 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
/>
|
/>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
icon={Edit}
|
icon={Edit}
|
||||||
onClick={handleEdit}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEdit();
|
||||||
|
}}
|
||||||
aria-label="Edit profile"
|
aria-label="Edit profile"
|
||||||
/>
|
/>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
onClick={handleDelete}
|
onClick={handleDeleteClick}
|
||||||
disabled={deleteProfile.isPending}
|
disabled={deleteProfile.isPending}
|
||||||
aria-label="Delete profile"
|
aria-label="Delete profile"
|
||||||
/>
|
/>
|
||||||
@@ -93,6 +108,25 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<ProfileDetail profileId={profile.id} open={detailOpen} onOpenChange={setDetailOpen} />
|
<ProfileDetail profileId={profile.id} open={detailOpen} onOpenChange={setDetailOpen} />
|
||||||
|
|
||||||
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Profile</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete "{profile.name}"? This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeleteConfirm} disabled={deleteProfile.isPending}>
|
||||||
|
{deleteProfile.isPending ? 'Deleting...' : 'Delete'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function ProfileList() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-4 shrink-0">
|
<div className="flex items-center justify-between mb-4 shrink-0">
|
||||||
<h2 className="text-2xl font-bold">Voice Profiles</h2>
|
<h2 className="text-2xl font-bold">Voicebox</h2>
|
||||||
<Button onClick={() => setDialogOpen(true)}>
|
<Button onClick={() => setDialogOpen(true)}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
New Profile
|
New Profile
|
||||||
@@ -53,7 +53,7 @@ export function ProfileList() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 grid-cols-3 auto-rows-fr">
|
<div className="grid gap-4 grid-cols-3 auto-rows-fr p-1">
|
||||||
{allProfiles.map((profile) => (
|
{allProfiles.map((profile) => (
|
||||||
<ProfileCard key={profile.id} profile={profile} />
|
<ProfileCard key={profile.id} profile={profile} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -109,6 +109,14 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
/* Hide scrollbars globally */
|
||||||
|
* {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|||||||
Reference in New Issue
Block a user