Refactor and enhance UI components across multiple pages
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped

- 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:
JSC
2025-08-14 23:51:47 +02:00
parent 8358aa16aa
commit 4e50e7e79d
53 changed files with 2477 additions and 1520 deletions

View File

@@ -1,5 +1,5 @@
import { apiClient } from '../client'
import type { User } from '@/types/auth'
import { apiClient } from '../client'
export interface Plan {
id: number
@@ -56,7 +56,7 @@ export interface NormalizationResponse {
export class AdminService {
async listUsers(limit = 100, offset = 0): Promise<User[]> {
return apiClient.get<User[]>(`/api/v1/admin/users/`, {
params: { limit, offset }
params: { limit, offset },
})
}
@@ -69,11 +69,15 @@ export class AdminService {
}
async disableUser(userId: number): Promise<MessageResponse> {
return apiClient.post<MessageResponse>(`/api/v1/admin/users/${userId}/disable`)
return apiClient.post<MessageResponse>(
`/api/v1/admin/users/${userId}/disable`,
)
}
async enableUser(userId: number): Promise<MessageResponse> {
return apiClient.post<MessageResponse>(`/api/v1/admin/users/${userId}/enable`)
return apiClient.post<MessageResponse>(
`/api/v1/admin/users/${userId}/enable`,
)
}
async listPlans(): Promise<Plan[]> {
@@ -85,27 +89,38 @@ export class AdminService {
return apiClient.post<ScanResponse>(`/api/v1/admin/sounds/scan`)
}
async normalizeAllSounds(force = false, onePass?: boolean): Promise<NormalizationResponse> {
async normalizeAllSounds(
force = false,
onePass?: boolean,
): Promise<NormalizationResponse> {
const params = new URLSearchParams()
if (force) params.append('force', 'true')
if (onePass !== undefined) params.append('one_pass', onePass.toString())
const queryString = params.toString()
const url = queryString ? `/api/v1/admin/sounds/normalize/all?${queryString}` : `/api/v1/admin/sounds/normalize/all`
const url = queryString
? `/api/v1/admin/sounds/normalize/all?${queryString}`
: `/api/v1/admin/sounds/normalize/all`
return apiClient.post<NormalizationResponse>(url)
}
async normalizeSoundsByType(soundType: 'SDB' | 'TTS' | 'EXT', force = false, onePass?: boolean): Promise<NormalizationResponse> {
async normalizeSoundsByType(
soundType: 'SDB' | 'TTS' | 'EXT',
force = false,
onePass?: boolean,
): Promise<NormalizationResponse> {
const params = new URLSearchParams()
if (force) params.append('force', 'true')
if (onePass !== undefined) params.append('one_pass', onePass.toString())
const queryString = params.toString()
const url = queryString ? `/api/v1/admin/sounds/normalize/type/${soundType}?${queryString}` : `/api/v1/admin/sounds/normalize/type/${soundType}`
const url = queryString
? `/api/v1/admin/sounds/normalize/type/${soundType}?${queryString}`
: `/api/v1/admin/sounds/normalize/type/${soundType}`
return apiClient.post<NormalizationResponse>(url)
}
}
export const adminService = new AdminService()
export const adminService = new AdminService()

View File

@@ -72,12 +72,15 @@ export class AuthService {
*/
async login(credentials: LoginRequest): Promise<User> {
// Using direct fetch for auth endpoints to avoid circular dependency with token refresh
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.LOGIN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
credentials: 'include',
})
const response = await fetch(
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.LOGIN}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
credentials: 'include',
},
)
if (!response.ok) {
const errorData = await response.json().catch(() => null)
@@ -91,12 +94,15 @@ export class AuthService {
* Register a new user account
*/
async register(userData: RegisterRequest): Promise<User> {
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.REGISTER}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
credentials: 'include',
})
const response = await fetch(
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.REGISTER}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
credentials: 'include',
},
)
if (!response.ok) {
const errorData = await response.json().catch(() => null)
@@ -133,7 +139,7 @@ export class AuthService {
async getOAuthUrl(provider: string): Promise<OAuthAuthorizationResponse> {
return apiClient.get<OAuthAuthorizationResponse>(
API_CONFIG.ENDPOINTS.AUTH.OAUTH_AUTHORIZE(provider),
{ skipAuth: true }
{ skipAuth: true },
)
}
@@ -143,21 +149,26 @@ export class AuthService {
async getOAuthProviders(): Promise<OAuthProvidersResponse> {
return apiClient.get<OAuthProvidersResponse>(
API_CONFIG.ENDPOINTS.AUTH.PROVIDERS,
{ skipAuth: true }
{ skipAuth: true },
)
}
/**
* Exchange OAuth temporary code for auth cookies
*/
async exchangeOAuthToken(request: ExchangeOAuthTokenRequest): Promise<ExchangeOAuthTokenResponse> {
async exchangeOAuthToken(
request: ExchangeOAuthTokenRequest,
): Promise<ExchangeOAuthTokenResponse> {
// Use direct fetch for OAuth token exchange to avoid auth loops and ensure cookies are set
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.EXCHANGE_OAUTH_TOKEN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
credentials: 'include', // Essential for receiving auth cookies
})
const response = await fetch(
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.EXCHANGE_OAUTH_TOKEN}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
credentials: 'include', // Essential for receiving auth cookies
},
)
if (!response.ok) {
const errorData = await response.json().catch(() => null)
@@ -171,10 +182,13 @@ export class AuthService {
* Refresh authentication token
*/
async refreshToken(): Promise<void> {
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.REFRESH}`, {
method: 'POST',
credentials: 'include',
})
const response = await fetch(
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.REFRESH}`,
{
method: 'POST',
credentials: 'include',
},
)
if (!response.ok) {
throw new Error('Token refresh failed')
@@ -198,15 +212,22 @@ export class AuthService {
/**
* Generate a new API token
*/
async generateApiToken(request: ApiTokenRequest = {}): Promise<ApiTokenResponse> {
return apiClient.post<ApiTokenResponse>(API_CONFIG.ENDPOINTS.AUTH.API_TOKEN, request)
async generateApiToken(
request: ApiTokenRequest = {},
): Promise<ApiTokenResponse> {
return apiClient.post<ApiTokenResponse>(
API_CONFIG.ENDPOINTS.AUTH.API_TOKEN,
request,
)
}
/**
* Get API token status
*/
async getApiTokenStatus(): Promise<ApiTokenStatusResponse> {
return apiClient.get<ApiTokenStatusResponse>(API_CONFIG.ENDPOINTS.AUTH.API_TOKEN_STATUS)
return apiClient.get<ApiTokenStatusResponse>(
API_CONFIG.ENDPOINTS.AUTH.API_TOKEN_STATUS,
)
}
/**
@@ -224,4 +245,4 @@ export class AuthService {
}
}
export const authService = new AuthService()
export const authService = new AuthService()

View File

@@ -28,7 +28,9 @@ export class ExtractionsService {
* Create a new extraction job
*/
async createExtraction(url: string): Promise<CreateExtractionResponse> {
const response = await apiClient.post<CreateExtractionResponse>(`/api/v1/extractions/?url=${encodeURIComponent(url)}`)
const response = await apiClient.post<CreateExtractionResponse>(
`/api/v1/extractions/?url=${encodeURIComponent(url)}`,
)
return response
}
@@ -36,7 +38,9 @@ export class ExtractionsService {
* Get extraction by ID
*/
async getExtraction(extractionId: number): Promise<ExtractionInfo> {
const response = await apiClient.get<ExtractionInfo>(`/api/v1/extractions/${extractionId}`)
const response = await apiClient.get<ExtractionInfo>(
`/api/v1/extractions/${extractionId}`,
)
return response
}
@@ -44,9 +48,11 @@ export class ExtractionsService {
* Get user's extractions
*/
async getUserExtractions(): Promise<ExtractionInfo[]> {
const response = await apiClient.get<GetExtractionsResponse>('/api/v1/extractions/')
const response = await apiClient.get<GetExtractionsResponse>(
'/api/v1/extractions/',
)
return response.extractions
}
}
export const extractionsService = new ExtractionsService()
export const extractionsService = new ExtractionsService()

View File

@@ -7,10 +7,13 @@ export class FilesService {
async downloadSound(soundId: number): Promise<void> {
try {
// Use fetch directly to handle file download
const response = await fetch(`${API_CONFIG.BASE_URL}/api/v1/files/sounds/${soundId}/download`, {
method: 'GET',
credentials: 'include',
})
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/v1/files/sounds/${soundId}/download`,
{
method: 'GET',
credentials: 'include',
},
)
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`)
@@ -19,7 +22,7 @@ export class FilesService {
// Get filename from Content-Disposition header or use default
const contentDisposition = response.headers.get('Content-Disposition')
let filename = `sound_${soundId}.mp3`
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/)
if (filenameMatch) {
@@ -30,14 +33,14 @@ export class FilesService {
// Create blob and download
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
// Create temporary download link
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
// Cleanup
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
@@ -59,10 +62,13 @@ export class FilesService {
*/
async hasThumbnail(soundId: number): Promise<boolean> {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/v1/files/sounds/${soundId}/thumbnail`, {
method: 'HEAD', // Only check headers, don't download
credentials: 'include',
})
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/v1/files/sounds/${soundId}/thumbnail`,
{
method: 'HEAD', // Only check headers, don't download
credentials: 'include',
},
)
return response.ok
} catch {
return false
@@ -73,7 +79,7 @@ export class FilesService {
* Preload a thumbnail image
*/
async preloadThumbnail(soundId: number): Promise<boolean> {
return new Promise((resolve) => {
return new Promise(resolve => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
@@ -82,4 +88,4 @@ export class FilesService {
}
}
export const filesService = new FilesService()
export const filesService = new FilesService()

View File

@@ -2,4 +2,4 @@ export * from './auth'
export * from './sounds'
export * from './player'
export * from './files'
export * from './extractions'
export * from './extractions'

View File

@@ -1,7 +1,12 @@
import { apiClient } from '../client'
export type PlayerStatus = 'playing' | 'paused' | 'stopped'
export type PlayerMode = 'continuous' | 'loop' | 'loop_one' | 'random' | 'single'
export type PlayerMode =
| 'continuous'
| 'loop'
| 'loop_one'
| 'random'
| 'single'
export interface PlayerSound {
id: number
@@ -144,4 +149,4 @@ export class PlayerService {
}
}
export const playerService = new PlayerService()
export const playerService = new PlayerService()

View File

@@ -1,6 +1,12 @@
import { apiClient } from '../client'
export type PlaylistSortField = 'name' | 'genre' | 'created_at' | 'updated_at' | 'sound_count' | 'total_duration'
export type PlaylistSortField =
| 'name'
| 'genre'
| 'created_at'
| 'updated_at'
| 'sound_count'
| 'total_duration'
export type SortOrder = 'asc' | 'desc'
export interface Playlist {
@@ -47,7 +53,7 @@ export class PlaylistsService {
*/
async getPlaylists(params?: GetPlaylistsParams): Promise<Playlist[]> {
const searchParams = new URLSearchParams()
// Handle parameters
if (params?.search) {
searchParams.append('search', params.search)
@@ -64,8 +70,10 @@ export class PlaylistsService {
if (params?.offset) {
searchParams.append('offset', params.offset.toString())
}
const url = searchParams.toString() ? `/api/v1/playlists/?${searchParams.toString()}` : '/api/v1/playlists/'
const url = searchParams.toString()
? `/api/v1/playlists/?${searchParams.toString()}`
: '/api/v1/playlists/'
return apiClient.get<Playlist[]>(url)
}
@@ -104,11 +112,14 @@ export class PlaylistsService {
/**
* Update a playlist
*/
async updatePlaylist(id: number, data: {
name?: string
description?: string
genre?: string
}): Promise<Playlist> {
async updatePlaylist(
id: number,
data: {
name?: string
description?: string
genre?: string
},
): Promise<Playlist> {
return apiClient.put<Playlist>(`/api/v1/playlists/${id}`, data)
}
@@ -154,28 +165,38 @@ export class PlaylistsService {
/**
* Add sound to playlist
*/
async addSoundToPlaylist(playlistId: number, soundId: number, position?: number): Promise<void> {
async addSoundToPlaylist(
playlistId: number,
soundId: number,
position?: number,
): Promise<void> {
await apiClient.post(`/api/v1/playlists/${playlistId}/sounds`, {
sound_id: soundId,
position
position,
})
}
/**
* Remove sound from playlist
*/
async removeSoundFromPlaylist(playlistId: number, soundId: number): Promise<void> {
async removeSoundFromPlaylist(
playlistId: number,
soundId: number,
): Promise<void> {
await apiClient.delete(`/api/v1/playlists/${playlistId}/sounds/${soundId}`)
}
/**
* Reorder sounds in playlist
*/
async reorderPlaylistSounds(playlistId: number, soundPositions: Array<[number, number]>): Promise<void> {
async reorderPlaylistSounds(
playlistId: number,
soundPositions: Array<[number, number]>,
): Promise<void> {
await apiClient.put(`/api/v1/playlists/${playlistId}/sounds/reorder`, {
sound_positions: soundPositions
sound_positions: soundPositions,
})
}
}
export const playlistsService = new PlaylistsService()
export const playlistsService = new PlaylistsService()

View File

@@ -21,7 +21,15 @@ export interface Sound {
updated_at: string
}
export type SoundSortField = 'name' | 'filename' | 'duration' | 'size' | 'type' | 'play_count' | 'created_at' | 'updated_at'
export type SoundSortField =
| 'name'
| 'filename'
| 'duration'
| 'size'
| 'type'
| 'play_count'
| 'created_at'
| 'updated_at'
export type SortOrder = 'asc' | 'desc'
export interface GetSoundsParams {
@@ -43,14 +51,14 @@ export class SoundsService {
*/
async getSounds(params?: GetSoundsParams): Promise<Sound[]> {
const searchParams = new URLSearchParams()
// Handle multiple types
if (params?.types) {
params.types.forEach(type => {
searchParams.append('types', type)
})
}
// Handle other parameters
if (params?.search) {
searchParams.append('search', params.search)
@@ -67,8 +75,10 @@ export class SoundsService {
if (params?.offset) {
searchParams.append('offset', params.offset.toString())
}
const url = searchParams.toString() ? `/api/v1/sounds/?${searchParams.toString()}` : '/api/v1/sounds/'
const url = searchParams.toString()
? `/api/v1/sounds/?${searchParams.toString()}`
: '/api/v1/sounds/'
const response = await apiClient.get<GetSoundsResponse>(url)
return response.sounds || []
}
@@ -76,14 +86,19 @@ export class SoundsService {
/**
* Get sounds of a specific type
*/
async getSoundsByType(type: string, params?: Omit<GetSoundsParams, 'types'>): Promise<Sound[]> {
async getSoundsByType(
type: string,
params?: Omit<GetSoundsParams, 'types'>,
): Promise<Sound[]> {
return this.getSounds({ ...params, types: [type] })
}
/**
* Get SDB type sounds
*/
async getSDBSounds(params?: Omit<GetSoundsParams, 'types'>): Promise<Sound[]> {
async getSDBSounds(
params?: Omit<GetSoundsParams, 'types'>,
): Promise<Sound[]> {
return this.getSoundsByType('SDB', params)
}
@@ -102,4 +117,4 @@ export class SoundsService {
}
}
export const soundsService = new SoundsService()
export const soundsService = new SoundsService()