Refactor and enhance UI components across multiple pages
- Improved import organization and formatting in PlaylistsPage, RegisterPage, SoundsPage, SettingsPage, and UsersPage for better readability. - Added error handling and user feedback with toast notifications in SoundsPage and SettingsPage. - Enhanced user experience by implementing debounced search functionality in PlaylistsPage and SoundsPage. - Updated the layout and structure of forms in SettingsPage and UsersPage for better usability. - Improved accessibility and semantics by ensuring proper labeling and descriptions in forms. - Fixed minor bugs related to state management and API calls in various components.
This commit is contained in:
@@ -1,16 +1,41 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Plus, Download, ExternalLink, Calendar, Clock, AlertCircle, CheckCircle, Loader2 } from 'lucide-react'
|
||||
import { extractionsService, type ExtractionInfo } from '@/lib/api/services/extractions'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
type ExtractionInfo,
|
||||
extractionsService,
|
||||
} from '@/lib/api/services/extractions'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import {
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
Plus,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function ExtractionsPage() {
|
||||
const [extractions, setExtractions] = useState<ExtractionInfo[]>([])
|
||||
@@ -63,29 +88,53 @@ export function ExtractionsPage() {
|
||||
const getStatusBadge = (status: ExtractionInfo['status']) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge variant="secondary" className="gap-1"><Clock className="h-3 w-3" />Pending</Badge>
|
||||
return (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
Pending
|
||||
</Badge>
|
||||
)
|
||||
case 'processing':
|
||||
return <Badge variant="outline" className="gap-1"><Loader2 className="h-3 w-3 animate-spin" />Processing</Badge>
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Processing
|
||||
</Badge>
|
||||
)
|
||||
case 'completed':
|
||||
return <Badge variant="default" className="gap-1"><CheckCircle className="h-3 w-3" />Completed</Badge>
|
||||
return (
|
||||
<Badge variant="default" className="gap-1">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
Completed
|
||||
</Badge>
|
||||
)
|
||||
case 'failed':
|
||||
return <Badge variant="destructive" className="gap-1"><AlertCircle className="h-3 w-3" />Failed</Badge>
|
||||
return (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
Failed
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getServiceBadge = (service: string | undefined) => {
|
||||
if (!service) return null
|
||||
|
||||
|
||||
const serviceColors: Record<string, string> = {
|
||||
youtube: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
soundcloud: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
soundcloud:
|
||||
'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
vimeo: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
tiktok: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300',
|
||||
twitter: 'bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-300',
|
||||
instagram: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
|
||||
instagram:
|
||||
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
|
||||
}
|
||||
|
||||
const colorClass = serviceColors[service.toLowerCase()] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'
|
||||
const colorClass =
|
||||
serviceColors[service.toLowerCase()] ||
|
||||
'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={colorClass}>
|
||||
@@ -95,12 +144,9 @@ export function ExtractionsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Extractions' }
|
||||
]
|
||||
items: [{ label: 'Dashboard', href: '/' }, { label: 'Extractions' }],
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
@@ -111,7 +157,7 @@ export function ExtractionsPage() {
|
||||
Extract audio from YouTube, SoundCloud, and other platforms
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
@@ -130,22 +176,29 @@ export function ExtractionsPage() {
|
||||
id="url"
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !isCreating) {
|
||||
handleCreateExtraction()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Supports YouTube, SoundCloud, Vimeo, TikTok, Twitter, Instagram, and more
|
||||
Supports YouTube, SoundCloud, Vimeo, TikTok, Twitter,
|
||||
Instagram, and more
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateExtraction} disabled={isCreating}>
|
||||
<Button
|
||||
onClick={handleCreateExtraction}
|
||||
disabled={isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
@@ -171,9 +224,12 @@ export function ExtractionsPage() {
|
||||
<CardContent className="py-8">
|
||||
<div className="text-center">
|
||||
<Download className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No extractions yet</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
No extractions yet
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Start by adding a URL to extract audio from your favorite platforms
|
||||
Start by adding a URL to extract audio from your favorite
|
||||
platforms
|
||||
</p>
|
||||
<Button onClick={() => setIsDialogOpen(true)} className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -199,7 +255,7 @@ export function ExtractionsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{extractions.map((extraction) => (
|
||||
{extractions.map(extraction => (
|
||||
<TableRow key={extraction.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
@@ -217,7 +273,10 @@ export function ExtractionsPage() {
|
||||
<TableCell>
|
||||
{getStatusBadge(extraction.status)}
|
||||
{extraction.error && (
|
||||
<div className="text-xs text-destructive mt-1 max-w-48 truncate" title={extraction.error}>
|
||||
<div
|
||||
className="text-xs text-destructive mt-1 max-w-48 truncate"
|
||||
title={extraction.error}
|
||||
>
|
||||
{extraction.error}
|
||||
</div>
|
||||
)}
|
||||
@@ -231,7 +290,9 @@ export function ExtractionsPage() {
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Invalid date'
|
||||
}
|
||||
return formatDistanceToNow(date, { addSuffix: true })
|
||||
return formatDistanceToNow(date, {
|
||||
addSuffix: true,
|
||||
})
|
||||
} catch {
|
||||
return 'Invalid date'
|
||||
}
|
||||
@@ -241,15 +302,24 @@ export function ExtractionsPage() {
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<a href={extraction.url} target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href={extraction.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
{extraction.status === 'completed' && extraction.sound_id && (
|
||||
<Button variant="ghost" size="sm" title="View in Sounds">
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{extraction.status === 'completed' &&
|
||||
extraction.sound_id && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="View in Sounds"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -262,4 +332,4 @@ export function ExtractionsPage() {
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user