From 8a02d7c366a071156644599bd8a9851646cd5f0e Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 7 May 2025 23:03:18 -0700 Subject: [PATCH 1/3] chore: UI Fixes --- .../[search_space_id]/chats/page.tsx | 4 +- .../[search_space_id]/podcasts/page.tsx | 2 - .../podcasts/podcasts-client.tsx | 617 +++++++++++------- 3 files changed, 400 insertions(+), 223 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx index 58c89f4..4d71dac 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx @@ -8,8 +8,8 @@ interface PageProps { } export default async function ChatsPage({ params }: PageProps) { - // Await params to properly access dynamic route parameters - const searchSpaceId = params.search_space_id; + // Get search space ID from the route parameter + const { search_space_id: searchSpaceId } = params; return ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx index 394177c..4292607 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx @@ -8,8 +8,6 @@ interface PageProps { } export default async function PodcastsPage({ params }: PageProps) { - // Access dynamic route parameters - // Need to await params before accessing its properties in an async component const { search_space_id: searchSpaceId } = await Promise.resolve(params); return ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx index 72e50ab..16527fb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx @@ -1,25 +1,24 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; import { format } from 'date-fns'; -import { - Search, Calendar, Trash2, MoreHorizontal, Podcast, - Play, Pause, SkipForward, SkipBack, Volume2, VolumeX +import { AnimatePresence, motion } from 'framer-motion'; +import { + Calendar, + MoreHorizontal, + Pause, + Play, + Podcast, + Search, + SkipBack, + SkipForward, + Trash2, + Volume2, VolumeX } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; // UI Components -import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import { Slider } from '@/components/ui/slider'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator -} from '@/components/ui/dropdown-menu'; +import { Card } from '@/components/ui/card'; import { Dialog, DialogContent, @@ -28,6 +27,13 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; import { Select, SelectContent, @@ -36,6 +42,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Slider } from '@/components/ui/slider'; import { toast } from "sonner"; interface PodcastItem { @@ -53,14 +60,15 @@ interface PodcastsPageClientProps { const pageVariants = { initial: { opacity: 0 }, - enter: { opacity: 1, transition: { duration: 0.3, ease: 'easeInOut' } }, + enter: { opacity: 1, transition: { duration: 0.4, ease: 'easeInOut', staggerChildren: 0.1 } }, exit: { opacity: 0, transition: { duration: 0.3, ease: 'easeInOut' } } }; const podcastCardVariants = { - initial: { y: 20, opacity: 0 }, - animate: { y: 0, opacity: 1 }, - exit: { y: -20, opacity: 0 } + initial: { scale: 0.95, y: 20, opacity: 0 }, + animate: { scale: 1, y: 0, opacity: 1, transition: { type: "spring", stiffness: 300, damping: 25 } }, + exit: { scale: 0.95, y: -20, opacity: 0 }, + hover: { y: -5, scale: 1.02, transition: { duration: 0.2 } } }; const MotionCard = motion(Card); @@ -216,12 +224,17 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient const handleVolumeChange = (value: number[]) => { if (audioRef.current) { const newVolume = value[0]; + + // Set volume audioRef.current.volume = newVolume; setVolume(newVolume); + // Handle mute state based on volume if (newVolume === 0) { + audioRef.current.muted = true; setIsMuted(true); - } else if (isMuted) { + } else { + audioRef.current.muted = false; setIsMuted(false); } } @@ -230,8 +243,16 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient // Toggle mute const toggleMute = () => { if (audioRef.current) { - audioRef.current.muted = !isMuted; - setIsMuted(!isMuted); + const newMutedState = !isMuted; + audioRef.current.muted = newMutedState; + setIsMuted(newMutedState); + + // If unmuting, restore previous volume if it was 0 + if (!newMutedState && volume === 0) { + const restoredVolume = 0.5; + audioRef.current.volume = restoredVolume; + setVolume(restoredVolume); + } } }; @@ -264,68 +285,73 @@ export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClient return; } - // Revoke previous object URL if exists - if (currentObjectUrlRef.current) { - URL.revokeObjectURL(currentObjectUrlRef.current); - currentObjectUrlRef.current = null; + // Prevent multiple simultaneous loading requests + if (isAudioLoading) { + return; } - // Reset player state and show loading - setCurrentPodcast(podcast); - setAudioSrc(undefined); - setCurrentTime(0); - setDuration(0); - setIsPlaying(false); - setIsAudioLoading(true); - try { + // Reset player state and show loading + setCurrentPodcast(podcast); + setAudioSrc(undefined); + setCurrentTime(0); + setDuration(0); + setIsPlaying(false); + setIsAudioLoading(true); + const token = localStorage.getItem('surfsense_bearer_token'); if (!token) { - toast.error('Authentication token not found.'); - setIsAudioLoading(false); - return; + throw new Error('Authentication token not found.'); } - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, - { - headers: { - 'Authorization': `Bearer ${token}`, - }, - } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch audio stream: ${response.statusText}`); + // Revoke previous object URL if exists (only after we've started the new request) + if (currentObjectUrlRef.current) { + URL.revokeObjectURL(currentObjectUrlRef.current); + currentObjectUrlRef.current = null; } - const blob = await response.blob(); - const objectUrl = URL.createObjectURL(blob); - currentObjectUrlRef.current = objectUrl; - - // Wait for React to commit the new `src` - setAudioSrc(objectUrl); - - // Use requestAnimationFrame instead of setTimeout for more reliable DOM updates - requestAnimationFrame(() => { - if (audioRef.current) { - // The