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:
@@ -2,4 +2,4 @@
|
||||
export * from './api/index'
|
||||
|
||||
// Export the main API object as default
|
||||
export { default as api } from './api/index'
|
||||
export { default as api } from './api/index'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AUTH_EVENTS, authEvents } from '../events'
|
||||
import { API_CONFIG } from './config'
|
||||
import { createApiError, NetworkError, TimeoutError } from './errors'
|
||||
import { NetworkError, TimeoutError, createApiError } from './errors'
|
||||
import type { ApiClient, ApiRequestConfig, HttpMethod } from './types'
|
||||
import { authEvents, AUTH_EVENTS } from '../events'
|
||||
|
||||
export class BaseApiClient implements ApiClient {
|
||||
private refreshPromise: Promise<void> | null = null
|
||||
@@ -11,9 +11,12 @@ export class BaseApiClient implements ApiClient {
|
||||
this.baseURL = baseURL
|
||||
}
|
||||
|
||||
private buildURL(endpoint: string, params?: Record<string, string | number | boolean | undefined>): string {
|
||||
private buildURL(
|
||||
endpoint: string,
|
||||
params?: Record<string, string | number | boolean | undefined>,
|
||||
): string {
|
||||
let url: URL
|
||||
|
||||
|
||||
if (this.baseURL) {
|
||||
// Full base URL provided
|
||||
url = new URL(endpoint, this.baseURL)
|
||||
@@ -21,7 +24,7 @@ export class BaseApiClient implements ApiClient {
|
||||
// Use relative URL (for reverse proxy)
|
||||
url = new URL(endpoint, window.location.origin)
|
||||
}
|
||||
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
@@ -29,7 +32,7 @@ export class BaseApiClient implements ApiClient {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return this.baseURL ? url.toString() : url.pathname + url.search
|
||||
}
|
||||
|
||||
@@ -37,7 +40,7 @@ export class BaseApiClient implements ApiClient {
|
||||
method: HttpMethod,
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
config: ApiRequestConfig = {}
|
||||
config: ApiRequestConfig = {},
|
||||
): Promise<T> {
|
||||
const {
|
||||
params,
|
||||
@@ -84,40 +87,43 @@ export class BaseApiClient implements ApiClient {
|
||||
await this.handleTokenRefresh()
|
||||
// Retry the original request
|
||||
const retryResponse = await fetch(url, requestConfig)
|
||||
|
||||
|
||||
if (!retryResponse.ok) {
|
||||
const errorData = await this.safeParseJSON(retryResponse)
|
||||
throw createApiError(retryResponse, errorData)
|
||||
}
|
||||
|
||||
return await this.safeParseJSON(retryResponse) as T
|
||||
|
||||
return (await this.safeParseJSON(retryResponse)) as T
|
||||
} catch (refreshError) {
|
||||
this.handleAuthenticationFailure()
|
||||
throw refreshError
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const errorData = await this.safeParseJSON(response)
|
||||
throw createApiError(response, errorData)
|
||||
}
|
||||
|
||||
// Handle empty responses (204 No Content, etc.)
|
||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||
if (
|
||||
response.status === 204 ||
|
||||
response.headers.get('content-length') === '0'
|
||||
) {
|
||||
return {} as T
|
||||
}
|
||||
|
||||
return await this.safeParseJSON(response) as T
|
||||
return (await this.safeParseJSON(response)) as T
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
|
||||
if ((error as Error).name === 'AbortError') {
|
||||
throw new TimeoutError()
|
||||
}
|
||||
|
||||
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
throw new NetworkError()
|
||||
}
|
||||
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -138,7 +144,7 @@ export class BaseApiClient implements ApiClient {
|
||||
}
|
||||
|
||||
this.refreshPromise = this.performTokenRefresh()
|
||||
|
||||
|
||||
try {
|
||||
await this.refreshPromise
|
||||
} finally {
|
||||
@@ -147,11 +153,14 @@ export class BaseApiClient implements ApiClient {
|
||||
}
|
||||
|
||||
private async performTokenRefresh(): Promise<void> {
|
||||
const response = await fetch(`${this.baseURL}${API_CONFIG.ENDPOINTS.AUTH.REFRESH}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
})
|
||||
const response = await fetch(
|
||||
`${this.baseURL}${API_CONFIG.ENDPOINTS.AUTH.REFRESH}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw createApiError(response, await this.safeParseJSON(response))
|
||||
@@ -165,7 +174,7 @@ export class BaseApiClient implements ApiClient {
|
||||
// Only redirect if we're not already on auth pages to prevent infinite loops
|
||||
const currentPath = window.location.pathname
|
||||
const authPaths = ['/login', '/register', '/auth/callback']
|
||||
|
||||
|
||||
if (!authPaths.includes(currentPath)) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
@@ -176,15 +185,27 @@ export class BaseApiClient implements ApiClient {
|
||||
return this.request<T>('GET', endpoint, undefined, config)
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, data?: unknown, config?: ApiRequestConfig): Promise<T> {
|
||||
async post<T>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
config?: ApiRequestConfig,
|
||||
): Promise<T> {
|
||||
return this.request<T>('POST', endpoint, data, config)
|
||||
}
|
||||
|
||||
async put<T>(endpoint: string, data?: unknown, config?: ApiRequestConfig): Promise<T> {
|
||||
async put<T>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
config?: ApiRequestConfig,
|
||||
): Promise<T> {
|
||||
return this.request<T>('PUT', endpoint, data, config)
|
||||
}
|
||||
|
||||
async patch<T>(endpoint: string, data?: unknown, config?: ApiRequestConfig): Promise<T> {
|
||||
async patch<T>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
config?: ApiRequestConfig,
|
||||
): Promise<T> {
|
||||
return this.request<T>('PATCH', endpoint, data, config)
|
||||
}
|
||||
|
||||
@@ -203,4 +224,4 @@ export class BaseApiClient implements ApiClient {
|
||||
}
|
||||
|
||||
// Default API client instance
|
||||
export const apiClient = new BaseApiClient()
|
||||
export const apiClient = new BaseApiClient()
|
||||
|
||||
@@ -16,7 +16,7 @@ export const API_CONFIG = {
|
||||
BASE_URL: getApiBaseUrl(),
|
||||
TIMEOUT: 30000, // 30 seconds
|
||||
RETRY_ATTEMPTS: 1,
|
||||
|
||||
|
||||
// API Endpoints
|
||||
ENDPOINTS: {
|
||||
AUTH: {
|
||||
@@ -26,7 +26,8 @@ export const API_CONFIG = {
|
||||
REFRESH: '/api/v1/auth/refresh',
|
||||
ME: '/api/v1/auth/me',
|
||||
PROVIDERS: '/api/v1/auth/providers',
|
||||
OAUTH_AUTHORIZE: (provider: string) => `/api/v1/auth/${provider}/authorize`,
|
||||
OAUTH_AUTHORIZE: (provider: string) =>
|
||||
`/api/v1/auth/${provider}/authorize`,
|
||||
OAUTH_CALLBACK: (provider: string) => `/api/v1/auth/${provider}/callback`,
|
||||
EXCHANGE_OAUTH_TOKEN: '/api/v1/auth/exchange-oauth-token',
|
||||
API_TOKEN: '/api/v1/auth/api-token',
|
||||
@@ -35,4 +36,4 @@ export const API_CONFIG = {
|
||||
},
|
||||
} as const
|
||||
|
||||
export type ApiEndpoints = typeof API_CONFIG.ENDPOINTS
|
||||
export type ApiEndpoints = typeof API_CONFIG.ENDPOINTS
|
||||
|
||||
@@ -4,7 +4,12 @@ export class ApiError extends Error {
|
||||
public response?: unknown
|
||||
public detail?: string
|
||||
|
||||
constructor(message: string, status: number, response?: unknown, detail?: string) {
|
||||
constructor(
|
||||
message: string,
|
||||
status: number,
|
||||
response?: unknown,
|
||||
detail?: string,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
@@ -14,8 +19,16 @@ export class ApiError extends Error {
|
||||
|
||||
static fromResponse(response: Response, data?: unknown): ApiError {
|
||||
const errorData = data as Record<string, unknown>
|
||||
const message = errorData?.detail as string || errorData?.message as string || `HTTP ${response.status}: ${response.statusText}`
|
||||
return new ApiError(message, response.status, data, errorData?.detail as string)
|
||||
const message =
|
||||
(errorData?.detail as string) ||
|
||||
(errorData?.message as string) ||
|
||||
`HTTP ${response.status}: ${response.statusText}`
|
||||
return new ApiError(
|
||||
message,
|
||||
response.status,
|
||||
data,
|
||||
errorData?.detail as string,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +88,10 @@ export class ServerError extends ApiError {
|
||||
export function createApiError(response: Response, data?: unknown): ApiError {
|
||||
const status = response.status
|
||||
const errorData = data as Record<string, unknown>
|
||||
const message = errorData?.detail as string || errorData?.message as string || `HTTP ${status}: ${response.statusText}`
|
||||
const message =
|
||||
(errorData?.detail as string) ||
|
||||
(errorData?.message as string) ||
|
||||
`HTTP ${status}: ${response.statusText}`
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
@@ -85,7 +101,10 @@ export function createApiError(response: Response, data?: unknown): ApiError {
|
||||
case 404:
|
||||
return new NotFoundError(message)
|
||||
case 422:
|
||||
return new ValidationError(message, errorData?.fields as Record<string, string[]>)
|
||||
return new ValidationError(
|
||||
message,
|
||||
errorData?.fields as Record<string, string[]>,
|
||||
)
|
||||
case 500:
|
||||
case 501:
|
||||
case 502:
|
||||
@@ -95,4 +114,4 @@ export function createApiError(response: Response, data?: unknown): ApiError {
|
||||
default:
|
||||
return new ApiError(message, status, data, errorData?.detail as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Main API object for convenient access
|
||||
import { apiClient } from './client'
|
||||
import { authService } from './services/auth'
|
||||
|
||||
// Re-export all API services and utilities
|
||||
export * from './client'
|
||||
export * from './config'
|
||||
@@ -7,14 +11,10 @@ export * from './errors'
|
||||
// Services
|
||||
export * from './services/auth'
|
||||
|
||||
// Main API object for convenient access
|
||||
import { authService } from './services/auth'
|
||||
import { apiClient } from './client'
|
||||
|
||||
export const api = {
|
||||
auth: authService,
|
||||
client: apiClient,
|
||||
} as const
|
||||
|
||||
// Default export for convenience
|
||||
export default api
|
||||
export default api
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -2,4 +2,4 @@ export * from './auth'
|
||||
export * from './sounds'
|
||||
export * from './player'
|
||||
export * from './files'
|
||||
export * from './extractions'
|
||||
export * from './extractions'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -20,15 +20,26 @@ export interface ApiRequestConfig extends RequestInit {
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
|
||||
// HTTP Methods
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
|
||||
// Generic API client interface
|
||||
export interface ApiClient {
|
||||
get<T>(endpoint: string, config?: ApiRequestConfig): Promise<T>
|
||||
post<T>(endpoint: string, data?: unknown, config?: ApiRequestConfig): Promise<T>
|
||||
put<T>(endpoint: string, data?: unknown, config?: ApiRequestConfig): Promise<T>
|
||||
patch<T>(endpoint: string, data?: unknown, config?: ApiRequestConfig): Promise<T>
|
||||
post<T>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
config?: ApiRequestConfig,
|
||||
): Promise<T>
|
||||
put<T>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
config?: ApiRequestConfig,
|
||||
): Promise<T>
|
||||
patch<T>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
config?: ApiRequestConfig,
|
||||
): Promise<T>
|
||||
delete<T>(endpoint: string, config?: ApiRequestConfig): Promise<T>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +58,4 @@ export const SOUND_EVENTS = {
|
||||
// User event types
|
||||
export const USER_EVENTS = {
|
||||
USER_CREDITS_CHANGED: 'user_credits_changed',
|
||||
} as const
|
||||
} as const
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Token refresh manager for proactive token refresh
|
||||
*/
|
||||
|
||||
import { authEvents, AUTH_EVENTS } from './events'
|
||||
import { api } from './api'
|
||||
import { AUTH_EVENTS, authEvents } from './events'
|
||||
|
||||
export class TokenRefreshManager {
|
||||
private refreshTimer: NodeJS.Timeout | null = null
|
||||
@@ -22,10 +21,10 @@ export class TokenRefreshManager {
|
||||
|
||||
this.isEnabled = true
|
||||
this.scheduleNextRefresh()
|
||||
|
||||
|
||||
// Listen for visibility changes to handle tab switching
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange)
|
||||
|
||||
|
||||
// Listen for successful auth events to reschedule
|
||||
authEvents.on(AUTH_EVENTS.LOGIN_SUCCESS, this.handleAuthSuccess)
|
||||
authEvents.on(AUTH_EVENTS.TOKEN_REFRESHED, this.handleTokenRefreshed)
|
||||
@@ -41,8 +40,11 @@ export class TokenRefreshManager {
|
||||
|
||||
this.isEnabled = false
|
||||
this.clearRefreshTimer()
|
||||
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange)
|
||||
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
this.handleVisibilityChange,
|
||||
)
|
||||
authEvents.off(AUTH_EVENTS.LOGIN_SUCCESS, this.handleAuthSuccess)
|
||||
authEvents.off(AUTH_EVENTS.TOKEN_REFRESHED, this.handleTokenRefreshed)
|
||||
}
|
||||
@@ -73,10 +75,9 @@ export class TokenRefreshManager {
|
||||
await api.auth.refreshToken()
|
||||
|
||||
authEvents.emit(AUTH_EVENTS.TOKEN_REFRESHED)
|
||||
|
||||
|
||||
// Schedule next refresh immediately since we just completed one
|
||||
this.scheduleNextRefresh()
|
||||
|
||||
} catch {
|
||||
// If refresh fails, try again in 1 minute
|
||||
this.refreshTimer = setTimeout(() => {
|
||||
@@ -87,7 +88,6 @@ export class TokenRefreshManager {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle tab visibility changes
|
||||
*/
|
||||
@@ -105,7 +105,7 @@ export class TokenRefreshManager {
|
||||
try {
|
||||
// Try to make an API call to see if token is still valid
|
||||
await api.auth.getMe()
|
||||
|
||||
|
||||
// Token is still valid, reschedule based on remaining time
|
||||
this.scheduleNextRefresh()
|
||||
} catch (error: unknown) {
|
||||
@@ -146,4 +146,4 @@ export class TokenRefreshManager {
|
||||
}
|
||||
|
||||
// Global token refresh manager instance
|
||||
export const tokenRefreshManager = new TokenRefreshManager()
|
||||
export const tokenRefreshManager = new TokenRefreshManager()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
|
||||
Reference in New Issue
Block a user