From 6018a5c8c5458f5ad2459ebe703df6297ee59d95 Mon Sep 17 00:00:00 2001 From: JSC Date: Sat, 26 Jul 2025 19:49:00 +0200 Subject: [PATCH] feat: update API client and remove unused services; enhance error handling and configuration --- .gitignore | 5 + README.md | 69 ---------- src/components/ui/sidebar.tsx | 2 +- src/components/ui/sonner.tsx | 2 +- src/contexts/AuthContext.tsx | 19 +-- src/lib/api/README.md | 214 ------------------------------ src/lib/api/client.ts | 4 +- src/lib/api/config.ts | 23 +--- src/lib/api/index.ts | 9 -- src/lib/api/services/playlists.ts | 94 ------------- src/lib/api/services/sounds.ts | 100 -------------- src/lib/api/services/users.ts | 74 ----------- src/pages/AuthCallbackPage.tsx | 4 - src/types/auth.ts | 1 - 14 files changed, 14 insertions(+), 606 deletions(-) delete mode 100644 README.md delete mode 100644 src/lib/api/README.md delete mode 100644 src/lib/api/services/playlists.ts delete mode 100644 src/lib/api/services/sounds.ts delete mode 100644 src/lib/api/services/users.ts diff --git a/.gitignore b/.gitignore index a547bf3..73c16c6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ dist dist-ssr *.local +# Environment variables +.env.local +.env.development.local +.env.production.local + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/README.md b/README.md deleted file mode 100644 index 7959ce4..0000000 --- a/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 1ee5a45..30638ac 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" -import { cva, VariantProps } from "class-variance-authority" +import { cva, type VariantProps } from "class-variance-authority" import { PanelLeftIcon } from "lucide-react" import { useIsMobile } from "@/hooks/use-mobile" diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index cd62aff..33d2d2a 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,5 +1,5 @@ import { useTheme } from "next-themes" -import { Toaster as Sonner, ToasterProps } from "sonner" +import { Toaster as Sonner, type ToasterProps } from "sonner" const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 9fad5d6..e797b6b 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -36,23 +36,13 @@ export function AuthProvider({ children }: AuthProviderProps) { }, []) const login = async (credentials: LoginRequest) => { - try { - const user = await api.auth.login(credentials) - setUser(user) - } catch (error) { - console.error('Login failed:', error) - throw error - } + const user = await api.auth.login(credentials) + setUser(user) } const register = async (data: RegisterRequest) => { - try { - const user = await api.auth.register(data) - setUser(user) - } catch (error) { - console.error('Registration failed:', error) - throw error - } + const user = await api.auth.register(data) + setUser(user) } const logout = async () => { @@ -62,7 +52,6 @@ export function AuthProvider({ children }: AuthProviderProps) { const value: AuthContextType = { user, - token: user ? 'cookie-based' : null, login, register, logout, diff --git a/src/lib/api/README.md b/src/lib/api/README.md deleted file mode 100644 index e6af9c7..0000000 --- a/src/lib/api/README.md +++ /dev/null @@ -1,214 +0,0 @@ -# API Library - -A generic, maintainable API client library for the Soundboard application. - -## Features - -- **Generic HTTP Client**: Supports all REST methods with proper TypeScript typing -- **Automatic Token Refresh**: Handles JWT token refresh automatically -- **Error Handling**: Comprehensive error classes for different scenarios -- **Modular Services**: Separate services for different API domains -- **Configuration Management**: Centralized API configuration -- **Backward Compatibility**: Legacy API service for existing code - -## Usage - -### New API (Recommended) - -```typescript -import { api } from '@/lib/api' - -// Authentication -const user = await api.auth.login({ email: 'user@example.com', password: 'password' }) -const currentUser = await api.auth.getMe() -await api.auth.logout() - -// Sounds -const sounds = await api.sounds.list({ page: 1, size: 20 }) -const sound = await api.sounds.get(1) -const newSound = await api.sounds.create({ title: 'My Sound', file: audioFile }) - -// Playlists -const playlists = await api.playlists.list() -const playlist = await api.playlists.create({ name: 'My Playlist' }) - -// Users (admin only) -const users = await api.users.list() -``` - -### Alternative Import Style - -```typescript -import { authService, soundsService } from '@/lib/api' - -// Direct service imports -const user = await authService.login(credentials) -const sounds = await soundsService.list() -``` - -### Direct Client Usage - -```typescript -import { apiClient } from '@/lib/api' - -// Generic HTTP requests -const data = await apiClient.get('/api/v1/custom-endpoint') -const result = await apiClient.post('/api/v1/data', requestData) -``` - -## Services - -### AuthService (`api.auth`) - -- `login(credentials)` - Authenticate user -- `register(userData)` - Register new user -- `getMe()` - Get current user -- `logout()` - Sign out user -- `getOAuthUrl(provider)` - Get OAuth authorization URL -- `getOAuthProviders()` - Get available OAuth providers -- `exchangeOAuthToken(request)` - Exchange OAuth code for auth cookies - -### SoundsService (`api.sounds`) - -- `list(params?)` - Get paginated sounds -- `get(id)` - Get specific sound -- `create(data)` - Create new sound -- `update(id, data)` - Update sound -- `delete(id)` - Delete sound -- `upload(file, metadata?)` - Upload sound file - -### PlaylistsService (`api.playlists`) - -- `list(params?)` - Get paginated playlists -- `get(id)` - Get specific playlist -- `create(data)` - Create new playlist -- `update(id, data)` - Update playlist -- `delete(id)` - Delete playlist -- `addSound(playlistId, soundData)` - Add sound to playlist -- `removeSound(playlistId, soundId)` - Remove sound from playlist - -### UsersService (`api.users`) - -- `list(params?)` - Get paginated users (admin only) -- `get(id)` - Get specific user -- `update(id, data)` - Update user -- `delete(id)` - Delete user (admin only) -- `changePassword(userId, data)` - Change user password -- `uploadAvatar(userId, file)` - Upload user avatar - -## Error Handling - -The library provides specific error classes for different scenarios: - -```typescript -import { - ApiError, - NetworkError, - TimeoutError, - ValidationError, - AuthenticationError, - AuthorizationError, - NotFoundError, - ServerError -} from '@/lib/api' - -try { - await api.auth.login(credentials) -} catch (error) { - if (error instanceof AuthenticationError) { - // Handle login failure - } else if (error instanceof ValidationError) { - // Handle validation errors - console.log(error.fields) // Field-specific errors - } else if (error instanceof NetworkError) { - // Handle network issues - } -} -``` - -## Configuration - -API configuration is centralized in `config.ts`: - -```typescript -export const API_CONFIG = { - BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000', - TIMEOUT: 30000, - RETRY_ATTEMPTS: 1, - ENDPOINTS: { - AUTH: { /* ... */ }, - SOUNDS: { /* ... */ }, - // ... - } -} -``` - -## Request Configuration - -All API methods accept optional configuration: - -```typescript -// Custom timeout -await api.sounds.list({}, { timeout: 60000 }) - -// Skip authentication -await api.auth.getOAuthProviders({}, { skipAuth: true }) - -// Custom headers -await api.sounds.create(data, { - headers: { 'X-Custom-Header': 'value' } -}) - -// Query parameters -await api.sounds.list({}, { - params: { search: 'query', page: 1 } -}) -``` - -## File Structure - -``` -src/lib/api/ -├── index.ts # Main exports -├── client.ts # Core HTTP client -├── config.ts # API configuration -├── types.ts # TypeScript types -├── errors.ts # Error classes -├── services/ -│ ├── auth.ts # Authentication service -│ ├── sounds.ts # Sounds service -│ ├── playlists.ts # Playlists service -│ └── users.ts # Users service -└── README.md # This file -``` - -## Migration Guide - -If migrating from an older API structure: - -1. **Use the main API object:** - ```typescript - import { api } from '@/lib/api' - - // Authentication - const user = await api.auth.login(credentials) - - // Resources - const sounds = await api.sounds.list() - const playlists = await api.playlists.list() - ``` - -2. **Or import services directly:** - ```typescript - import { authService, soundsService } from '@/lib/api' - - const user = await authService.login(credentials) - const sounds = await soundsService.list() - ``` - -3. **For custom requests, use the client:** - ```typescript - import { apiClient } from '@/lib/api' - - const data = await apiClient.get('/api/v1/custom-endpoint') - ``` \ No newline at end of file diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index 5910a76..c07356e 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -81,7 +81,7 @@ export class BaseApiClient implements ApiClient { throw createApiError(retryResponse, errorData) } - return await this.safeParseJSON(retryResponse) + return await this.safeParseJSON(retryResponse) as T } catch (refreshError) { this.handleAuthenticationFailure() throw refreshError @@ -97,7 +97,7 @@ export class BaseApiClient implements ApiClient { return {} as T } - return await this.safeParseJSON(response) + return await this.safeParseJSON(response) as T } catch (error) { clearTimeout(timeoutId) diff --git a/src/lib/api/config.ts b/src/lib/api/config.ts index 1a45836..7a2bde0 100644 --- a/src/lib/api/config.ts +++ b/src/lib/api/config.ts @@ -1,6 +1,6 @@ // API Configuration export const API_CONFIG = { - BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000', + BASE_URL: 'http://localhost:8000', TIMEOUT: 30000, // 30 seconds RETRY_ATTEMPTS: 1, @@ -17,27 +17,6 @@ export const API_CONFIG = { OAUTH_CALLBACK: (provider: string) => `/api/v1/auth/${provider}/callback`, EXCHANGE_OAUTH_TOKEN: '/api/v1/auth/exchange-oauth-token', }, - SOUNDS: { - LIST: '/api/v1/sounds', - CREATE: '/api/v1/sounds', - GET: (id: string | number) => `/api/v1/sounds/${id}`, - UPDATE: (id: string | number) => `/api/v1/sounds/${id}`, - DELETE: (id: string | number) => `/api/v1/sounds/${id}`, - UPLOAD: '/api/v1/sounds/upload', - }, - PLAYLISTS: { - LIST: '/api/v1/playlists', - CREATE: '/api/v1/playlists', - GET: (id: string | number) => `/api/v1/playlists/${id}`, - UPDATE: (id: string | number) => `/api/v1/playlists/${id}`, - DELETE: (id: string | number) => `/api/v1/playlists/${id}`, - }, - USERS: { - LIST: '/api/v1/users', - GET: (id: string | number) => `/api/v1/users/${id}`, - UPDATE: (id: string | number) => `/api/v1/users/${id}`, - DELETE: (id: string | number) => `/api/v1/users/${id}`, - }, }, } as const diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index e83efcd..002d8b1 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -6,22 +6,13 @@ export * from './errors' // Services export * from './services/auth' -export * from './services/sounds' -export * from './services/playlists' -export * from './services/users' // Main API object for convenient access import { authService } from './services/auth' -import { soundsService } from './services/sounds' -import { playlistsService } from './services/playlists' -import { usersService } from './services/users' import { apiClient } from './client' export const api = { auth: authService, - sounds: soundsService, - playlists: playlistsService, - users: usersService, client: apiClient, } as const diff --git a/src/lib/api/services/playlists.ts b/src/lib/api/services/playlists.ts deleted file mode 100644 index 0df8383..0000000 --- a/src/lib/api/services/playlists.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { apiClient } from '../client' -import { API_CONFIG } from '../config' -import type { PaginatedResponse } from '../types' -import type { Sound } from './sounds' - -export interface Playlist { - id: number - name: string - description?: string - user_id: number - is_public: boolean - sounds: Sound[] - created_at: string - updated_at: string -} - -export interface CreatePlaylistRequest { - name: string - description?: string - is_public?: boolean -} - -export interface UpdatePlaylistRequest { - name?: string - description?: string - is_public?: boolean -} - -export interface PlaylistsListParams { - page?: number - size?: number - search?: string - user_id?: number - is_public?: boolean -} - -export interface AddSoundToPlaylistRequest { - sound_id: number -} - -export class PlaylistsService { - /** - * Get list of playlists with pagination - */ - async list(params?: PlaylistsListParams): Promise> { - return apiClient.get>(API_CONFIG.ENDPOINTS.PLAYLISTS.LIST, { - params: params as Record - }) - } - - /** - * Get a specific playlist by ID - */ - async get(id: string | number): Promise { - return apiClient.get(API_CONFIG.ENDPOINTS.PLAYLISTS.GET(id)) - } - - /** - * Create a new playlist - */ - async create(data: CreatePlaylistRequest): Promise { - return apiClient.post(API_CONFIG.ENDPOINTS.PLAYLISTS.CREATE, data) - } - - /** - * Update an existing playlist - */ - async update(id: string | number, data: UpdatePlaylistRequest): Promise { - return apiClient.patch(API_CONFIG.ENDPOINTS.PLAYLISTS.UPDATE(id), data) - } - - /** - * Delete a playlist - */ - async delete(id: string | number): Promise { - return apiClient.delete(API_CONFIG.ENDPOINTS.PLAYLISTS.DELETE(id)) - } - - /** - * Add a sound to a playlist - */ - async addSound(playlistId: string | number, data: AddSoundToPlaylistRequest): Promise { - return apiClient.post(`${API_CONFIG.ENDPOINTS.PLAYLISTS.GET(playlistId)}/sounds`, data) - } - - /** - * Remove a sound from a playlist - */ - async removeSound(playlistId: string | number, soundId: string | number): Promise { - return apiClient.delete(`${API_CONFIG.ENDPOINTS.PLAYLISTS.GET(playlistId)}/sounds/${soundId}`) - } -} - -export const playlistsService = new PlaylistsService() \ No newline at end of file diff --git a/src/lib/api/services/sounds.ts b/src/lib/api/services/sounds.ts deleted file mode 100644 index 604bec6..0000000 --- a/src/lib/api/services/sounds.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { apiClient } from '../client' -import { API_CONFIG } from '../config' -import type { PaginatedResponse } from '../types' - -export interface Sound { - id: number - title: string - description?: string - file_url: string - duration: number - file_size: number - mime_type: string - play_count: number - user_id: number - created_at: string - updated_at: string -} - -export interface CreateSoundRequest { - title: string - description?: string - file: File -} - -export interface UpdateSoundRequest { - title?: string - description?: string -} - -export interface SoundsListParams { - page?: number - size?: number - search?: string - user_id?: number -} - -export class SoundsService { - /** - * Get list of sounds with pagination - */ - async list(params?: SoundsListParams): Promise> { - return apiClient.get>(API_CONFIG.ENDPOINTS.SOUNDS.LIST, { - params: params as Record - }) - } - - /** - * Get a specific sound by ID - */ - async get(id: string | number): Promise { - return apiClient.get(API_CONFIG.ENDPOINTS.SOUNDS.GET(id)) - } - - /** - * Create a new sound - */ - async create(data: CreateSoundRequest): Promise { - const formData = new FormData() - formData.append('title', data.title) - if (data.description) { - formData.append('description', data.description) - } - formData.append('file', data.file) - - return apiClient.post(API_CONFIG.ENDPOINTS.SOUNDS.CREATE, formData) - } - - /** - * Update an existing sound - */ - async update(id: string | number, data: UpdateSoundRequest): Promise { - return apiClient.patch(API_CONFIG.ENDPOINTS.SOUNDS.UPDATE(id), data) - } - - /** - * Delete a sound - */ - async delete(id: string | number): Promise { - return apiClient.delete(API_CONFIG.ENDPOINTS.SOUNDS.DELETE(id)) - } - - /** - * Upload a sound file - */ - async upload(file: File, metadata?: { title?: string; description?: string }): Promise { - const formData = new FormData() - formData.append('file', file) - - if (metadata?.title) { - formData.append('title', metadata.title) - } - if (metadata?.description) { - formData.append('description', metadata.description) - } - - return apiClient.post(API_CONFIG.ENDPOINTS.SOUNDS.UPLOAD, formData) - } -} - -export const soundsService = new SoundsService() \ No newline at end of file diff --git a/src/lib/api/services/users.ts b/src/lib/api/services/users.ts deleted file mode 100644 index 9339115..0000000 --- a/src/lib/api/services/users.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { User } from '@/types/auth' -import { apiClient } from '../client' -import { API_CONFIG } from '../config' -import type { PaginatedResponse } from '../types' - -export interface UpdateUserRequest { - name?: string - email?: string - picture?: string -} - -export interface UsersListParams { - page?: number - size?: number - search?: string - role?: string - is_active?: boolean -} - -export interface ChangePasswordRequest { - current_password: string - new_password: string -} - -export class UsersService { - /** - * Get list of users with pagination (admin only) - */ - async list(params?: UsersListParams): Promise> { - return apiClient.get>(API_CONFIG.ENDPOINTS.USERS.LIST, { - params: params as Record - }) - } - - /** - * Get a specific user by ID - */ - async get(id: string | number): Promise { - return apiClient.get(API_CONFIG.ENDPOINTS.USERS.GET(id)) - } - - /** - * Update user profile - */ - async update(id: string | number, data: UpdateUserRequest): Promise { - return apiClient.patch(API_CONFIG.ENDPOINTS.USERS.UPDATE(id), data) - } - - /** - * Delete a user (admin only) - */ - async delete(id: string | number): Promise { - return apiClient.delete(API_CONFIG.ENDPOINTS.USERS.DELETE(id)) - } - - /** - * Change user password - */ - async changePassword(userId: string | number, data: ChangePasswordRequest): Promise { - return apiClient.post(`${API_CONFIG.ENDPOINTS.USERS.GET(userId)}/change-password`, data) - } - - /** - * Upload user avatar - */ - async uploadAvatar(userId: string | number, file: File): Promise { - const formData = new FormData() - formData.append('avatar', file) - - return apiClient.post(`${API_CONFIG.ENDPOINTS.USERS.GET(userId)}/avatar`, formData) - } -} - -export const usersService = new UsersService() \ No newline at end of file diff --git a/src/pages/AuthCallbackPage.tsx b/src/pages/AuthCallbackPage.tsx index 202e12d..acdbe05 100644 --- a/src/pages/AuthCallbackPage.tsx +++ b/src/pages/AuthCallbackPage.tsx @@ -20,15 +20,11 @@ export function AuthCallbackPage() { throw new Error('No authorization code received') } - console.log('Exchanging OAuth code for tokens...') - // Exchange the temporary code for proper auth cookies const result = await api.auth.exchangeOAuthToken({ code }) - console.log('Token exchange successful:', result) // Now get the user info const user = await api.auth.getMe() - console.log('User info retrieved:', user) // Update auth context if (setUser) setUser(user) diff --git a/src/types/auth.ts b/src/types/auth.ts index 37cd99e..96d31e0 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -40,7 +40,6 @@ export interface RegisterRequest { export interface AuthContextType { user: User | null - token: string | null login: (credentials: LoginRequest) => Promise register: (data: RegisterRequest) => Promise logout: () => Promise