diff --git a/apps/web/src/components/admin/AdminAuditLogsView.tsx b/apps/web/src/components/admin/AdminAuditLogsView.tsx index 87b5dba32..24b92c995 100644 --- a/apps/web/src/components/admin/AdminAuditLogsView.tsx +++ b/apps/web/src/components/admin/AdminAuditLogsView.tsx @@ -32,7 +32,7 @@ export const AdminAuditLogsView: React.FC = () => { try { const data = await adminService.getAuditLogs({ page, limit: 20 }); setLogs(data.logs || []); - setTotal(data.pagination?.total_items || 0); + setTotal(data.pagination?.total || 0); } catch (e) { logger.error('Failed to fetch audit logs', { error: e }); } finally { diff --git a/apps/web/src/components/admin/AdminUsersView.tsx b/apps/web/src/components/admin/AdminUsersView.tsx index a5f0a6ecf..e758f773b 100644 --- a/apps/web/src/components/admin/AdminUsersView.tsx +++ b/apps/web/src/components/admin/AdminUsersView.tsx @@ -7,31 +7,27 @@ import { BanUserModal } from './modals/BanUserModal'; import { User } from '../../types'; import { Search, Shield, Activity, Users, Download, UserPlus, Loader2 } from 'lucide-react'; import { useToast } from '../../components/feedback/ToastProvider'; -import { userService } from '../../services/userService'; -import { logger } from '@/utils/logger'; +import { useGetUsers } from '@/services/generated/user/user'; export const AdminUsersView: React.FC = () => { const { addToast } = useToast(); const [search, setSearch] = useState(''); - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); const [selectedUser, setSelectedUser] = useState(null); + const [users, setUsers] = useState([]); + + // Use generated hook. The orval-generated response type wraps in a + // {data, status, headers} discriminated union, but the apiClient response + // interceptor (services/api/interceptors/response.ts) unwraps the + // {success, data} envelope before the mutator returns. So at runtime + // `usersData` IS the payload — cast accordingly. + const { data: usersData, isLoading: loading } = useGetUsers(); useEffect(() => { - const loadUsers = async () => { - setLoading(true); - try { - const res = await userService.list(); - setUsers(res.users); - } catch (e) { - logger.error('Failed to load users', { error: e }); - addToast('Failed to load users', 'error'); - } finally { - setLoading(false); - } - }; - loadUsers(); - }, []); + const payload = usersData as unknown as { users?: User[] } | undefined; + if (payload?.users) { + setUsers(payload.users); + } + }, [usersData]); const handleBan = (duration: string) => { if (!selectedUser) return; diff --git a/apps/web/src/components/feedback/AnnouncementBanner.tsx b/apps/web/src/components/feedback/AnnouncementBanner.tsx index 04ac2da16..23e86bccc 100644 --- a/apps/web/src/components/feedback/AnnouncementBanner.tsx +++ b/apps/web/src/components/feedback/AnnouncementBanner.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; import { X, Info, AlertTriangle, AlertCircle } from 'lucide-react'; -import { adminService } from '@/services/adminService'; +import { useGetApiV1AnnouncementsActive } from '@/services/generated/admin/admin'; import { cn } from '@/lib/utils'; interface Announcement { @@ -36,13 +36,15 @@ const typeConfig: Record }; export function AnnouncementBanner() { - const [announcements, setAnnouncements] = useState([]); const [dismissed, setDismissed] = useState>(loadDismissed); const [showAll, setShowAll] = useState(false); - useEffect(() => { - adminService.getActiveAnnouncements().then(setAnnouncements).catch(() => {}); - }, []); + // Use generated hook. apiClient response interceptor unwraps the + // {success, data} envelope, so at runtime announcementsData is the + // payload directly — see services/api/interceptors/response.ts. + const { data: announcementsData } = useGetApiV1AnnouncementsActive(); + const payload = announcementsData as unknown as { announcements?: Announcement[] } | undefined; + const announcements: Announcement[] = payload?.announcements ?? []; const dismiss = useCallback((id: string) => { setDismissed((prev) => { @@ -52,7 +54,7 @@ export function AnnouncementBanner() { }); }, []); - const visible = announcements.filter((a) => !dismissed.has(a.id)); + const visible = announcements.filter((a: Announcement) => !dismissed.has(a.id)); if (visible.length === 0) return null; const shown = showAll ? visible : visible.slice(0, 1); @@ -60,7 +62,7 @@ export function AnnouncementBanner() { return (
- {shown.map((a) => { + {shown.map((a: Announcement) => { const config = typeConfig[a.type] ?? defaultConfig; const Icon = config.icon; return ( diff --git a/apps/web/src/components/library/playlists/PlaylistDetailView.tsx b/apps/web/src/components/library/playlists/PlaylistDetailView.tsx index 2aadc0c96..dcfb573e4 100644 --- a/apps/web/src/components/library/playlists/PlaylistDetailView.tsx +++ b/apps/web/src/components/library/playlists/PlaylistDetailView.tsx @@ -19,8 +19,20 @@ interface PlaylistDetailViewProps { onBack: () => void; } +/** + * Extended Playlist type for UI-specific fields used in this view + */ +interface ExtendedPlaylist extends Playlist { + creator: string; + userId: string; + likes: number; + isCollaborative: boolean; + duration: string; + followers: number; +} + // Mock Data Fetcher -const getPlaylistById = (id: string): any => ({ +const getPlaylistById = (id: string): ExtendedPlaylist => ({ id, title: 'Cyberpunk 2077 Vibes', creator: 'Cyber_Producer', @@ -35,16 +47,31 @@ const getPlaylistById = (id: string): any => ({ isCollaborative: false, duration: '45 min', followers: 850, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + user_id: 'u1', tracks: Array.from({ length: 12 }).map((_, i) => ({ id: `t${i}`, title: `Neon Track ${i + 1}`, artist: 'Various Artists', album: 'Compilation', cover_url: '', + cover_art_path: '', duration: '3:45', durationSec: 225, plays: 1000 + i * 100, likes: 50 + i, + creator_id: 'u1', + file_path: '', + file_size: 0, + format: 'mp3', + play_count: 1000 + i * 100, + like_count: 50 + i, + is_public: true, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + status: 'completed', + stream_status: 'ready', })), }); @@ -54,14 +81,14 @@ export const PlaylistDetailView: React.FC = ({ }) => { const { addToast } = useToast(); const { playTrack } = useAudio(); - const [playlist, setPlaylist] = useState(getPlaylistById(playlistId)); + const [playlist, setPlaylist] = useState(getPlaylistById(playlistId)); const [isEditing, setIsEditing] = useState(false); const [tracks, setTracks] = useState(playlist.tracks || []); const [draggedIndex, setDraggedIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); const handleUpdate = (data: Partial) => { - setPlaylist((prev: any) => ({ ...prev, ...data })); + setPlaylist((prev) => ({ ...prev, ...data })); addToast('Playlist updated', 'success'); }; @@ -106,7 +133,7 @@ export const PlaylistDetailView: React.FC = ({
diff --git a/apps/web/src/components/settings/appearance/AppearanceSettingsView.tsx b/apps/web/src/components/settings/appearance/AppearanceSettingsView.tsx index 2739cf1b4..7cf4c93b7 100644 --- a/apps/web/src/components/settings/appearance/AppearanceSettingsView.tsx +++ b/apps/web/src/components/settings/appearance/AppearanceSettingsView.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect } from 'react'; import { Card } from '../../ui/card'; import { Button } from '../../ui/button'; import { useTheme } from '../../../components/theme/ThemeProvider'; @@ -15,7 +15,8 @@ import { import { useToast } from '../../../components/feedback/ToastProvider'; import { Switch } from '../../ui/switch'; import { useAuth } from '@/features/auth/hooks/useAuth'; -import { userService } from '@/services/userService'; +import { useGetUsersMePreferences, usePutUsersMePreferences } from '@/services/generated/user/user'; +import type { VezaBackendApiInternalTypesPreferenceSettings } from '@/services/generated/model/vezaBackendApiInternalTypesPreferenceSettings'; import { usePWA } from '@/hooks/usePWA'; import { Download } from 'lucide-react'; @@ -55,10 +56,20 @@ export const AppearanceSettingsView: React.FC = () => { const { canInstall, install, isInstalling } = usePWA(); const [showSidebar, setShowSidebar] = React.useState(true); - const loadPreferences = useCallback(async () => { - if (!isAuthenticated) return; - try { - const prefs = await userService.getPreferences(); + // Use generated hooks. apiClient response interceptor unwraps the + // {success, data} envelope so at runtime prefData IS the payload — + // see services/api/interceptors/response.ts. + const { data: prefData } = useGetUsersMePreferences({ + query: { + enabled: isAuthenticated, + } + }); + + const updatePrefsMutation = usePutUsersMePreferences(); + + useEffect(() => { + const prefs = prefData as unknown as VezaBackendApiInternalTypesPreferenceSettings | undefined; + if (prefs) { const themeVal = prefs.theme as 'dark' | 'light' | 'system'; if (themeVal && ['dark', 'light', 'system'].includes(themeVal)) { setTheme(themeVal); @@ -70,24 +81,20 @@ export const AppearanceSettingsView: React.FC = () => { } setAccentHue(prefs.accentHue ?? 220); setFontSize(Math.min(20, Math.max(14, prefs.fontSize ?? 16))); - } catch { - /* ignore, use local state */ } - }, [isAuthenticated, setTheme, setContrast, setDensity, setAccentHue, setFontSize]); - - useEffect(() => { - loadPreferences(); - }, [loadPreferences]); + }, [prefData, setTheme, setContrast, setDensity, setAccentHue, setFontSize]); const handleSave = async () => { if (isAuthenticated) { try { - await userService.updatePreferences({ - theme, - contrast, - density, - accentHue, - fontSize, + await updatePrefsMutation.mutateAsync({ + data: { + theme, + contrast, + density, + accentHue, + fontSize, + } }); addToast('Appearance settings saved', 'success'); } catch { diff --git a/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts b/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts index c943aa77d..73e1ae580 100644 --- a/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts +++ b/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts @@ -1,8 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@/features/auth/hooks/useAuth'; import { useToast } from '@/components/feedback/ToastProvider'; -import { userService } from '@/services/userService'; -import { logger } from '@/utils/logger'; +import { useGetUsersId, usePutUsersId } from '@/services/generated/user/user'; import { getCroppedImg } from './cropUtils'; import type { EditProfileFormData, PixelCrop } from './types'; @@ -28,49 +27,53 @@ export function useEditProfile() { const [loading, setLoading] = useState(false); const [formData, setFormData] = useState(initialFormData); + // Use generated hooks + const { data: profileData } = useGetUsersId(user?.id || '', { + query: { + enabled: !!user?.id, + } + }); + + const updateProfileMutation = usePutUsersId(); + useEffect(() => { - const fetchProfile = async () => { - if (!user) return; - try { - const res = await userService.getProfile(user.id); - const p = res.profile; - setFormData({ - username: p.username || '', - first_name: p.first_name || '', - last_name: p.last_name || '', - bio: p.bio || '', - banner_url: (p as { banner_url?: string }).banner_url ?? (p as { banner?: string }).banner ?? '', - location: p.location || '', - gender: p.gender || 'Prefer not to say', - birthdate: p.birthdate || '', - }); - if (p.avatar_url) setAvatar(p.avatar_url); - const bannerUrl = (p as { banner_url?: string }).banner_url ?? (p as { banner?: string }).banner; - if (bannerUrl) setBanner(bannerUrl); - } catch (e) { - logger.error('Failed to load profile settings', { - error: e instanceof Error ? e.message : String(e), - stack: e instanceof Error ? e.stack : undefined, - userId: user?.id, - }); - addToast('Failed to load profile settings', 'error'); - } - }; - fetchProfile(); - }, [user, addToast]); + // apiClient response interceptor unwraps the {success, data} envelope, + // so at runtime profileData is the payload directly. See + // services/api/interceptors/response.ts. + const payload = profileData as unknown as { profile?: Record } | undefined; + const p = payload?.profile as Record | undefined; + if (p) { + setFormData({ + username: p.username || '', + first_name: p.first_name || '', + last_name: p.last_name || '', + bio: p.bio || '', + banner_url: p.banner_url ?? p.banner ?? '', + location: p.location || '', + gender: p.gender || 'Prefer not to say', + birthdate: p.birthdate || '', + }); + if (p.avatar_url) setAvatar(p.avatar_url); + const bannerUrl = p.banner_url ?? p.banner; + if (bannerUrl) setBanner(bannerUrl); + } + }, [profileData]); const handleSave = useCallback(async () => { if (!user) return; setLoading(true); try { - await userService.updateProfile(user.id, formData); + await updateProfileMutation.mutateAsync({ + id: user.id, + data: formData as any, + }); addToast('Profile updated successfully', 'success'); } catch (e) { addToast('Failed to update profile', 'error'); } finally { setLoading(false); } - }, [user, formData, addToast]); + }, [user, formData, addToast, updateProfileMutation]); const handleFileChange = useCallback( (e: React.ChangeEvent, type: 'avatar' | 'banner') => {