Files
eranos/src/components/MiniAudioPlayer.tsx
T
Chad Curtis 0c29506402 Fix all 50 ESLint warnings by extracting non-component exports and adding missing deps
- Extract utility functions from component files into dedicated modules
  to fix react-refresh/only-export-components warnings:
  - parseBadgeDefinition -> src/lib/parseBadgeDefinition.ts
  - parseProfileBadges -> src/lib/parseProfileBadges.ts
  - getColors, paletteToTheme -> src/lib/colorMomentUtils.ts
  - parseDimToAspectRatio, eventToMediaItem -> src/lib/mediaUtils.ts
  - isAudioUrl, isImageUrl, isVideoUrl -> src/lib/mediaTypeDetection.ts
  - buildKindOptions, parseSelectedKinds -> src/lib/feedFilterUtils.ts
  - useVideoThumbnail -> src/hooks/useVideoThumbnail.ts
  - useEnvelopeDimensions -> src/hooks/useEnvelopeDimensions.ts
  - usePortalContainer -> src/hooks/usePortalContainer.ts
  - useAudioPlayer -> src/contexts/audioPlayerContextDef.ts
  - SubHeaderBar context/hooks -> src/components/SubHeaderBarContext.ts
  - EmotionDev hooks -> src/blobbi/dev/useEmotionDev.ts
  - BlobbiActions context def -> BlobbiActionsContextDef.ts

- Remove export from internal-only functions (useEventComments,
  parseEmojiPack, useScrollCarets, formatEffectSummary, getSortedEffectEntries)

- Fix react-hooks/exhaustive-deps warnings by adding missing dependencies
  to useEffect/useCallback/useMemo hooks across 14 files

- Fix logical expression dependency warnings by wrapping conditional
  values (tasks, pubkeys, authorPubkeys) in useMemo

- Move module-level constants (CORE_TAB_IDS, CORE_TAB_LABELS,
  DEFAULT_TAB_LABELS) out of ProfilePage component body

- Reorder usePushNotifications hooks so syncPreferences is defined
  before enable to fix block-scoped variable error
2026-04-05 11:57:31 -05:00

118 lines
3.9 KiB
TypeScript

import { useRef, useState, useEffect, useCallback } from 'react';
import { Play, Pause } from 'lucide-react';
import { cn } from '@/lib/utils';
import { formatTime } from '@/lib/formatTime';
interface MiniAudioPlayerProps {
src: string;
label?: string;
className?: string;
}
/** Compact inline audio player for profile fields. */
export function MiniAudioPlayer({ src, label, className }: MiniAudioPlayerProps) {
const audioRef = useRef<HTMLAudioElement>(null);
const progressRef = useRef<HTMLDivElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const progress = duration > 0 ? currentTime / duration : 0;
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const onPlay = () => setIsPlaying(true);
const onPause = () => setIsPlaying(false);
const onEnded = () => setIsPlaying(false);
const onTime = () => setCurrentTime(audio.currentTime);
const onDur = () => {
if (audio.duration && isFinite(audio.duration)) {
setDuration(audio.duration);
}
};
audio.addEventListener('play', onPlay);
audio.addEventListener('pause', onPause);
audio.addEventListener('ended', onEnded);
audio.addEventListener('timeupdate', onTime);
audio.addEventListener('durationchange', onDur);
audio.addEventListener('loadedmetadata', onDur);
return () => {
audio.removeEventListener('play', onPlay);
audio.removeEventListener('pause', onPause);
audio.removeEventListener('ended', onEnded);
audio.removeEventListener('timeupdate', onTime);
audio.removeEventListener('durationchange', onDur);
audio.removeEventListener('loadedmetadata', onDur);
};
}, []);
const togglePlay = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const audio = audioRef.current;
if (!audio) return;
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
}, []);
const handleSeek = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const audio = audioRef.current;
const bar = progressRef.current;
if (!audio || !bar || !duration) return;
const rect = bar.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
audio.currentTime = ratio * duration;
}, [duration]);
return (
<div className={cn('flex items-center gap-2.5 rounded-lg border border-border bg-muted/30 px-3 py-2', className)}>
<audio ref={audioRef} preload="metadata" className="hidden">
<source src={src} />
</audio>
{/* Play/Pause */}
<button
onClick={togglePlay}
className={cn(
'shrink-0 size-7 rounded-full flex items-center justify-center transition-colors',
isPlaying
? 'bg-primary text-primary-foreground'
: 'bg-primary/15 text-primary hover:bg-primary/25',
)}
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying
? <Pause className="size-3" fill="currentColor" />
: <Play className="size-3 ml-px" fill="currentColor" />}
</button>
{/* Track info + progress */}
<div className="flex-1 min-w-0 space-y-1">
{label && <span className="block text-xs font-medium truncate">{label}</span>}
{/* Progress bar */}
<div
ref={progressRef}
className="h-1 w-full rounded-full bg-primary/15 cursor-pointer"
onClick={handleSeek}
>
<div
className="h-full rounded-full bg-primary transition-[width] duration-150"
style={{ width: `${progress * 100}%` }}
/>
</div>
</div>
{/* Time */}
<span className="text-[10px] tabular-nums text-muted-foreground shrink-0">
{formatTime(currentTime)}/{duration > 0 ? formatTime(duration) : '--:--'}
</span>
</div>
);
}