- Vibe & Tags
+ Etiquetas
{beat.tags && beat.tags.length > 0 ? (
@@ -172,7 +192,7 @@ const BeatDetailPage = () => {
))
) : (
- No tags added.
+ Sin etiquetas.
)}
@@ -185,7 +205,7 @@ const BeatDetailPage = () => {
-
Actions & License
+ Acciones y Licencia
@@ -194,9 +214,8 @@ const BeatDetailPage = () => {
{isOwner && (
{beat.isPublic ? (
@@ -204,7 +223,7 @@ const BeatDetailPage = () => {
)}
- {beat.isPublic ? "Public Beat" : "Private Beat"}
+ {beat.isPublic ? "Beat Público" : "Beat Privado"}
@@ -212,14 +231,36 @@ const BeatDetailPage = () => {
{/* Botón Principal */}
{beat.isDownloadable && !isOwner && (
-
+
+
+
+
+
+
+
+
+ Comprobando tu plan...
+
+
+ Error al verificar tu plan
+
+
)}
@@ -227,7 +268,7 @@ const BeatDetailPage = () => {
MP3 + WAV
- Unlimited
+ Ilimitado
@@ -237,7 +278,49 @@ const BeatDetailPage = () => {
{/* 2. ADMIN CONTROLS (Intacto) */}
{isOwner && (
-
Owner Controls
+
Controles del Propietario
+
+ {/* Promote Beat Button */}
+
+
+
+
+
+
+
+
+ Comprobando add-on...
+
+
+ Error al verificar add-on
+
+
+
@@ -275,10 +358,10 @@ const BeatDetailPage = () => {
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
onConfirm={handleDeleteBeat}
- title="Delete Beat"
- message={`Delete "${beat.title}"? This cannot be undone.`}
- confirmText="Yes, Delete"
- cancelText="Cancel"
+ title="Eliminar Beat"
+ message={`¿Eliminar "${beat.title}"? Esta acción no se puede deshacer.`}
+ confirmText="Sí, Eliminar"
+ cancelText="Cancelar"
variant="danger"
/>
diff --git a/src/pages/app/beats/BeatFormPage.jsx b/src/pages/app/beats/BeatFormPage.jsx
index fca1b90..0bf1d05 100644
--- a/src/pages/app/beats/BeatFormPage.jsx
+++ b/src/pages/app/beats/BeatFormPage.jsx
@@ -28,8 +28,8 @@ const BeatFormPage = () => {
const beat = await getBeatById(id);
setBeatData(beat);
} catch (err) {
- setError('Error loading beat data');
- console.error(err);
+ const errorMessage = err.response?.data?.message || 'Error al cargar los datos del beat';
+ setError(errorMessage);
} finally {
setLoading(false);
}
@@ -114,8 +114,8 @@ const BeatFormPage = () => {
setShowCreatedModal(true);
}
} catch (err) {
- setError(`Error ${isEditing ? 'updating' : 'creating'} beat. Please try again.`);
- console.error(err);
+ const errorMessage = err.response?.data?.message || `Error al ${isEditing ? 'actualizar' : 'crear'} el beat. Por favor, inténtalo de nuevo.`;
+ setError(errorMessage);
} finally {
setSaving(false);
}
@@ -124,7 +124,7 @@ const BeatFormPage = () => {
if (loading) {
return (
-
Loading beat data...
+
Cargando datos del beat...
);
}
@@ -135,11 +135,11 @@ const BeatFormPage = () => {
- {isEditing ? 'Edit Beat' : 'Create New Beat'}
+ {isEditing ? 'Editar Beat' : 'Crear Nuevo Beat'}
diff --git a/src/pages/app/beats/BeatsListPage.jsx b/src/pages/app/beats/BeatsListPage.jsx
index cffc09e..3af8427 100644
--- a/src/pages/app/beats/BeatsListPage.jsx
+++ b/src/pages/app/beats/BeatsListPage.jsx
@@ -10,18 +10,18 @@ import { getBeats } from "../../../services/beatsService";
import "./BeatsTable.css";
const allColumns = [
- { key: "artist", label: "Artist" },
- { key: "genre", label: "Genre" },
+ { key: "artist", label: "Artista" },
+ { key: "genre", label: "Género" },
{ key: "bpm", label: "BPM" },
- { key: "key", label: "Key" },
- { key: "duration", label: "Duration" },
- { key: "plays", label: "Plays" },
- { key: "downloads", label: "Downloads" },
- { key: "likes", label: "Likes" },
- { key: "comments", label: "Comments" },
- { key: "mood", label: "Mood" },
- { key: "license", label: "License" },
- { key: "createdAt", label: "Upload Date" },
+ { key: "key", label: "Tonalidad" },
+ { key: "duration", label: "Duración" },
+ { key: "plays", label: "Reproducciones" },
+ { key: "downloads", label: "Descargas" },
+ { key: "likes", label: "Me gusta" },
+ { key: "comments", label: "Comentarios" },
+ { key: "mood", label: "Estado de ánimo" },
+ { key: "license", label: "Licencia" },
+ { key: "createdAt", label: "Fecha de subida" },
];
const BeatsListPage = () => {
@@ -90,7 +90,7 @@ const BeatsListPage = () => {
: "-",
formattedLikes: (beat.stats?.likes || 0).toLocaleString(),
formattedComments: (beat.stats?.comments || 0).toLocaleString(),
- formattedDate: beat.createdAt ? new Intl.DateTimeFormat("en-US", {
+ formattedDate: beat.createdAt ? new Intl.DateTimeFormat("es-ES", {
year: "numeric",
month: "short",
day: "numeric",
@@ -98,8 +98,8 @@ const BeatsListPage = () => {
}));
setBeats(formattedBeats);
} catch (err) {
- setError("Error fetching beats. Please try again later.");
- console.error(err);
+ const errorMessage = err.response?.data?.message || "Error al cargar los beats. Por favor, inténtalo más tarde.";
+ setError(errorMessage);
} finally {
setLoading(false);
}
@@ -117,7 +117,7 @@ const BeatsListPage = () => {
if (loading) {
return (
-
Loading beats...
+
Cargando beats...
);
}
@@ -190,31 +190,31 @@ const BeatsListPage = () => {
#
-
Title
+
Título
{visibleColumns.artist && (
-
Artist
+
Artista
)}
- {visibleColumns.genre &&
Genre
}
+ {visibleColumns.genre &&
Género
}
{visibleColumns.bpm &&
BPM
}
- {visibleColumns.key &&
Key
}
+ {visibleColumns.key &&
Tonalidad
}
{visibleColumns.duration && (
-
Duration
+
Duración
)}
- {visibleColumns.price &&
Price
}
- {visibleColumns.plays &&
Plays
}
+ {visibleColumns.price &&
Precio
}
+ {visibleColumns.plays &&
Reproducciones
}
{visibleColumns.downloads && (
-
Downloads
+
Descargas
)}
- {visibleColumns.likes &&
Likes
}
+ {visibleColumns.likes &&
Me gusta
}
{visibleColumns.comments && (
-
Comments
+
Comentarios
)}
- {visibleColumns.mood &&
Mood
}
+ {visibleColumns.mood &&
Estado
}
{visibleColumns.license && (
-
License
+
Licencia
)}
{visibleColumns.createdAt && (
-
Upload Date
+
Fecha de subida
)}
{/*
Cover
*/}
@@ -252,12 +252,12 @@ const BeatsListPage = () => {
{beat.title}
- {beat.createdBy?.username || "Unknown"}
+ {beat.createdBy?.username || "Desconocido"}
{visibleColumns.artist && (
-
{beat.createdBy?.username || "Unknown"}
+
{beat.createdBy?.username || "Desconocido"}
)}
{visibleColumns.genre && (
{beat.genre}
diff --git a/src/pages/app/beats/ColumnSelector.jsx b/src/pages/app/beats/ColumnSelector.jsx
index 6ae1041..ced4842 100644
--- a/src/pages/app/beats/ColumnSelector.jsx
+++ b/src/pages/app/beats/ColumnSelector.jsx
@@ -4,7 +4,7 @@ import './ColumnSelector.css';
const ColumnSelector = ({ columns, visibleColumns, onColumnChange }) => {
return (
-
Customize Columns
+
Personalizar Columnas
{columns.map((column) => (
);
diff --git a/src/pages/app/explore/components/BeatCard.css b/src/pages/app/explore/components/BeatCard.css
index 65f0ecc..abfd816 100644
--- a/src/pages/app/explore/components/BeatCard.css
+++ b/src/pages/app/explore/components/BeatCard.css
@@ -28,10 +28,13 @@
background: rgba(139, 92, 246, 0.3);
}
-/* Carousel variant */
+/* Carousel variant - dentro de explore-beats-grid */
.beat-card-carousel {
- flex-shrink: 0;
- width: 200px;
+ flex: 0 0 200px !important;
+ min-width: 200px !important;
+ max-width: 200px !important;
+ width: 200px !important;
+ flex-shrink: 0 !important;
scroll-snap-align: start;
}
diff --git a/src/pages/app/explore/components/BeatsExploreSection.css b/src/pages/app/explore/components/BeatsExploreSection.css
index 21c03f2..991c32f 100644
--- a/src/pages/app/explore/components/BeatsExploreSection.css
+++ b/src/pages/app/explore/components/BeatsExploreSection.css
@@ -4,6 +4,12 @@
display: flex;
flex-direction: column;
gap: 2rem;
+ /* Limitar al ancho del contenedor padre */
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ box-sizing: border-box;
+ overflow: hidden; /* Contener hijos */
}
/* Section Header */
@@ -30,12 +36,7 @@
font-size: 1rem;
}
-/* Subsections */
-.beats-subsection {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
+/* Subsections - eliminado el overflow:hidden para permitir scroll en la regla más abajo */
.subsection-header {
display: flex;
@@ -78,11 +79,63 @@
display: none;
}
-/* Grid Layout */
-.beats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: 1.5rem;
+/* Grid Layout - Horizontal Scrollable */
+.beats-subsection {
+ /* Layout de columna para header + grid */
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ /* Contener al ancho del padre */
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ box-sizing: border-box;
+ overflow: hidden; /* Contener el grid */
+}
+
+/* Wrapper para el scroll horizontal */
+.explore-beats-grid {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 1.25rem;
+ padding: 0.5rem 0 1rem 0;
+ -webkit-overflow-scrolling: touch;
+ /* Scroll horizontal aquí */
+ overflow-x: scroll;
+ overflow-y: visible;
+
+ /* Styled scrollbar */
+ scrollbar-width: thin;
+ scrollbar-color: var(--accent-primary) rgba(255, 255, 255, 0.05);
+}
+
+.explore-beats-grid::-webkit-scrollbar {
+ height: 8px;
+}
+
+.explore-beats-grid::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 4px;
+}
+
+.explore-beats-grid::-webkit-scrollbar-thumb {
+ background: var(--accent-primary);
+ border-radius: 4px;
+}
+
+.explore-beats-grid::-webkit-scrollbar-thumb:hover {
+ background: var(--accent-secondary);
+}
+
+/* Asegurar que las cards tengan tamaño fijo - selector más específico */
+.explore-beats-grid > .beat-card,
+.explore-beats-grid > .beat-card-carousel,
+.explore-beats-grid .beat-card {
+ flex: 0 0 200px !important;
+ min-width: 200px !important;
+ max-width: 200px !important;
+ width: 200px !important;
+ flex-shrink: 0 !important;
}
/* Filtered Results */
@@ -217,8 +270,7 @@
/* Responsive */
@media (max-width: 768px) {
- .beats-grid {
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ .explore-beats-grid {
gap: 1rem;
}
@@ -232,8 +284,7 @@
}
@media (max-width: 480px) {
- .beats-grid {
- grid-template-columns: repeat(2, 1fr);
+ .explore-beats-grid {
gap: 0.75rem;
}
}
diff --git a/src/pages/app/explore/components/BeatsExploreSection.jsx b/src/pages/app/explore/components/BeatsExploreSection.jsx
index 7134011..e7e9989 100644
--- a/src/pages/app/explore/components/BeatsExploreSection.jsx
+++ b/src/pages/app/explore/components/BeatsExploreSection.jsx
@@ -236,11 +236,11 @@ export default function BeatsExploreSection({ searchTerm = '', onClearSearch })
{loadingFiltered ? (
-
+
{renderSkeletons(8)}
) : filteredBeats.length > 0 ? (
-
+
{filteredBeats.map((beat) => (
) : mostPlayedBeats.length > 0 ? (
-
+
{mostPlayedBeats.map((beat) => (
{loadingRecent ? (
-
+
{renderSkeletons(8)}
) : recentBeats.length > 0 ? (
-
+
{recentBeats.map((beat) => (
{
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.profiles)) return data.profiles;
+ if (Array.isArray(data?.results)) return data.results;
+ if (Array.isArray(data?.data)) return data.data;
+ return [];
+};
+
+export default function UsersExploreSection({ searchTerm = '', onClearSearch }) {
+ const navigate = useNavigate();
+ const { profile } = useProfileData();
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [friends, setFriends] = useState(new Set());
+ const [incomingRequests, setIncomingRequests] = useState(new Set());
+ const [sentRequests, setSentRequests] = useState(new Set());
+
+ const trimmedTerm = useMemo(() => searchTerm.trim(), [searchTerm]);
+ const currentUserId = useMemo(
+ () => profile?.userId || profile?._id || profile?.id,
+ [profile]
+ );
+ const currentUsername = useMemo(
+ () => (profile?.username || '').toLowerCase(),
+ [profile]
+ );
+
+ // Cargar estado social (amigos, solicitudes entrantes y enviadas)
+ useEffect(() => {
+ const controller = new AbortController();
+
+ const loadFriendsAndRequests = async () => {
+ try {
+ const [friendsRes, incomingRes, sentRes] = await Promise.all([
+ listFriends({ signal: controller.signal }),
+ listReceivedRequests({ signal: controller.signal }),
+ listSentRequests({ signal: controller.signal })
+ ]);
+
+ const friendIds = new Set(
+ (friendsRes?.friends || []).map((f) => f?.id || f?._id || f?.userId).filter(Boolean)
+ );
+ const incomingIds = new Set(
+ (incomingRes?.requests || []).map((r) => r?.sender?.id || r?.sender?._id || r?.sender?.userId).filter(Boolean)
+ );
+ const sentIds = new Set(sentRes || []);
+
+ setFriends(friendIds);
+ setIncomingRequests(incomingIds);
+ setSentRequests(sentIds);
+ } catch (err) {
+ console.warn('No se pudieron cargar amigos/solicitudes', err?.message || err);
+ }
+ };
+
+ loadFriendsAndRequests();
+
+ return () => controller.abort();
+ }, []);
+
+ const isSelf = useCallback(
+ (u) => {
+ const userId = u?.userId || u?._id || u?.id;
+ const username = (u?.username || u?.userName || u?.handle || '').toLowerCase();
+ if (currentUserId && userId && currentUserId === userId) return true;
+ if (currentUsername && username && currentUsername === username) return true;
+ return false;
+ },
+ [currentUserId, currentUsername]
+ );
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const runSearch = async () => {
+ if (!trimmedTerm || trimmedTerm.length < MIN_SEARCH_LENGTH) {
+ setUsers([]);
+ setError('');
+ setLoading(false);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError('');
+ const response = await searchProfiles(trimmedTerm);
+ const normalized = normalizeProfiles(response)
+ .filter((u) => !isSelf(u))
+ .slice(0, 24);
+ if (!isMounted) return;
+ setUsers(normalized);
+ } catch (err) {
+ console.error('Error searching profiles:', err);
+ if (!isMounted) return;
+ setError('No se pudieron cargar usuarios ahora mismo');
+ setUsers([]);
+ } finally {
+ if (isMounted) setLoading(false);
+ }
+ };
+
+ runSearch();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [trimmedTerm, isSelf]);
+
+ const visibleUsers = useMemo(
+ () => users.filter((u) => !isSelf(u)),
+ [users, isSelf]
+ );
+
+ const handleSendRequest = async (userId) => {
+ if (!userId) return;
+ try {
+ await sendFriendRequest(userId);
+ setSentRequests((prev) => new Set([...prev, userId]));
+ } catch (err) {
+ console.error('Error al enviar solicitud:', err);
+ }
+ };
+
+ const handleViewProfile = (username) => {
+ if (!username) return;
+ navigate(`/app/profile/${username}`);
+ };
+
+ const renderSkeletons = (count) =>
+ Array.from({ length: count }).map((_, idx) => (
+
+ ));
+
+ const hasShortTerm = trimmedTerm && trimmedTerm.length < MIN_SEARCH_LENGTH;
+
+ return (
+
+
+
+ 👥
+ Usuarios
+
+
+ Busca creadores y conéctate con ellos
+
+
+
+ {!trimmedTerm && (
+
+ Escribe al menos 2 caracteres para buscar usuarios.
+
+ )}
+
+ {hasShortTerm && (
+
+ Sigue escribiendo... necesitamos 2 o más caracteres.
+
+ )}
+
+ {loading && (
+ {renderSkeletons(6)}
+ )}
+
+ {!loading && error && (
+
+ 🔌 {error}
+ {onClearSearch && (
+
+ )}
+
+ )}
+
+ {!loading && !error && trimmedTerm && visibleUsers.length === 0 && !hasShortTerm && (
+
+
🔍
+
No encontramos usuarios para "{trimmedTerm}"
+ {onClearSearch && (
+
+ )}
+
+ )}
+
+ {!loading && visibleUsers.length > 0 && (
+
+ {visibleUsers.map((user) => {
+ const username = user?.username || user?.userName || user?.handle;
+ const fullName =
+ user?.full_name || user?.fullName || user?.name || user?.displayName;
+ const bio = user?.about || user?.bio || user?.description;
+ const avatar = user?.avatar || user?.profilePhoto || user?.photo;
+ const userId = user?.userId || user?.id || user?._id;
+ const isFriend = friends.has(userId);
+ const hasIncoming = incomingRequests.has(userId);
+ const hasSent = sentRequests.has(userId);
+
+ return (
+
+
+
+
+
+ {fullName && {fullName}}
+ {isFriend && ✓ Amigo}
+
+
+
+ {bio &&
{bio}
}
+
+
+
+ {isFriend ? (
+
+ ) : hasIncoming ? (
+ <>
+
+ >
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/src/pages/app/profile/Profile.css b/src/pages/app/profile/Profile.css
index 04015d5..12c950a 100644
--- a/src/pages/app/profile/Profile.css
+++ b/src/pages/app/profile/Profile.css
@@ -11,6 +11,13 @@
background: #0f172a;
}
+.profile-actions-row {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin: 1rem;
+}
+
/* =============================================
BLOQUE HERO PRINCIPAL
3 columnas: Avatar | Content+Beats | Certs
diff --git a/src/pages/app/profile/ProfileView.jsx b/src/pages/app/profile/ProfileView.jsx
index 2cacc44..3037b33 100644
--- a/src/pages/app/profile/ProfileView.jsx
+++ b/src/pages/app/profile/ProfileView.jsx
@@ -245,6 +245,14 @@ export default function ProfileView() {
/>
)}
+
+
+
{/* Hero Section */}
request?.id || request?._id || request;
+
+const getSenderName = (request) => {
+ const sender = request?.sender || request?.requester || request?.from;
+ // Priorizar username, luego name, luego alias
+ // Solo usar ID como último recurso si todo lo anterior es vacío/nulo
+ return (sender?.username?.trim() || sender?.name?.trim() || sender?.alias?.trim() || 'Usuario desconocido');
+};
+
+const getSender = (request) => request?.sender || request?.requester || request?.from;
+
+function RequestRow({ request, onAccept, onReject, isProcessing }) {
+ const requestId = getRequestId(request);
+ const sender = getSender(request);
+ const senderName = getSenderName(request);
+
+ return (
+
+
+
+
+ {senderName}
+ {/* {sender?.full_name && {sender.full_name}} */}
+ {request?.createdAt && (
+
+ {new Date(request.createdAt).toLocaleDateString('es-ES', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric'
+ })}
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+function RequestSkeleton() {
+ return (
+
+
+
+
+ );
+}
+
+export default function FriendRequests() {
+ const navigate = useNavigate();
+ const [requests, setRequests] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [search, setSearch] = useState('');
+ const [processingId, setProcessingId] = useState(null);
+ const [processingAction, setProcessingAction] = useState(null);
+ const [toast, setToast] = useState(null);
+ const toastTimerRef = useRef(null);
+
+ const showToast = (type, message) => {
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
+ setToast({ type, message });
+ toastTimerRef.current = setTimeout(() => setToast(null), 3200);
+ };
+
+ const loadRequests = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const data = await listReceivedRequests();
+ // API puede devolver: { value: [...], Count: n } o directamente [...] o { requests: [...] }
+ setRequests(data?.requests || data?.value || data || []);
+ } catch (err) {
+ setError('No se pudieron cargar las solicitudes de amistad.');
+ showToast('error', 'No se pudieron cargar las solicitudes.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadRequests();
+ }, []);
+
+ const filtered = useMemo(() => {
+ const term = search.trim().toLowerCase();
+ if (!term) return requests;
+ return requests.filter((r) => getSenderName(r)?.toLowerCase().includes(term));
+ }, [requests, search]);
+
+ const handleAccept = async (request) => {
+ const requestId = getRequestId(request);
+ const senderName = getSenderName(request);
+
+ const snapshot = [...requests];
+ setProcessingId(requestId);
+ setProcessingAction('accept');
+ setRequests((prev) => prev.filter((r) => getRequestId(r) !== requestId));
+
+ try {
+ await respondRequest(requestId, 'accept');
+ showToast('success', `¡Ahora eres amigo de ${senderName}!`);
+ } catch (err) {
+ setRequests(snapshot);
+ setError('No se pudo aceptar la solicitud.');
+ showToast('error', 'No se pudo aceptar la solicitud.');
+ } finally {
+ setProcessingId(null);
+ setProcessingAction(null);
+ }
+ };
+
+ const handleReject = async (request) => {
+ const requestId = getRequestId(request);
+ const senderName = getSenderName(request);
+
+ const snapshot = [...requests];
+ setProcessingId(requestId);
+ setProcessingAction('reject');
+ setRequests((prev) => prev.filter((r) => getRequestId(r) !== requestId));
+
+ try {
+ await respondRequest(requestId, 'reject');
+ showToast('success', `Solicitud de ${senderName} rechazada.`);
+ } catch (err) {
+ setRequests(snapshot);
+ setError('No se pudo rechazar la solicitud.');
+ showToast('error', 'No se pudo rechazar la solicitud.');
+ } finally {
+ setProcessingId(null);
+ setProcessingAction(null);
+ }
+ };
+
+ const renderContent = () => {
+ if (isLoading) {
+ return (
+
+ {Array.from({ length: 3 }).map((_, idx) => (
+
+ ))}
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+
+ );
+ }
+
+ if (!filtered.length) {
+ return (
+
+ {requests.length === 0
+ ? 'No tienes solicitudes de amistad pendientes.'
+ : 'No se encontraron solicitudes con ese nombre.'}
+
+
+
+ );
+ }
+
+ return (
+
+ {filtered.map((request) => {
+ const reqId = getRequestId(request);
+ return (
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+
+
+
+ {renderContent()}
+
+
+ {toast && (
+
+ {toast.message}
+
+ )}
+
+ );
+}
diff --git a/src/pages/app/social/Friends.css b/src/pages/app/social/Friends.css
new file mode 100644
index 0000000..50513c4
--- /dev/null
+++ b/src/pages/app/social/Friends.css
@@ -0,0 +1,213 @@
+.friends-page {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 16px;
+}
+
+.friends-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.friends-sub {
+ color: #6b7280;
+ margin: 4px 0 0;
+}
+
+.friends-actions {
+ min-width: 240px;
+ flex: 1;
+ max-width: 320px;
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.friends-card {
+ width: 100%;
+}
+
+.friends-toast {
+ position: fixed;
+ right: 16px;
+ bottom: 16px;
+ padding: 12px 14px;
+ border-radius: 10px;
+ color: #111827;
+ background: #ecfeff;
+ border: 1px solid #a5f3fc;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.08);
+ z-index: 30;
+ animation: fadeIn 150ms ease-out;
+}
+
+.friends-toast-success {
+ background: #ecfdf3;
+ border-color: #bbf7d0;
+}
+
+.friends-toast-error {
+ background: #fef2f2;
+ border-color: #fecdd3;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.friends-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+
+.friend-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 16px 18px;
+ border: 1px solid #e0e7ff;
+ border-radius: 14px;
+ background: linear-gradient(135deg, #f3f4f6 0%, #e0e7ff 60%, #f5d0fe 100%);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.08);
+}
+
+.friend-row:hover {
+ border-color: #d1d5db;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ transform: translateY(-2px);
+}
+
+.friend-row-left {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ min-width: 0;
+ flex: 1;
+}
+
+.friend-row-text {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.friend-row-name {
+ font-weight: 800;
+ font-size: 1.08rem;
+ color: #312e81;
+ text-shadow: 0 1px 6px #fff8, 0 1px 0 #fff8;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ letter-spacing: -0.3px;
+}
+
+.friend-row-sub {
+ color: #6b7280;
+ font-size: 0.875rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.friend-row-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+.state-card {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 16px;
+ text-align: center;
+}
+
+.state-card.error {
+ border: 1px solid #fca5a5;
+ background: #fef2f2;
+}
+
+.state-card.empty {
+ border: 1px solid #e5e7eb;
+ background: #f9fafb;
+}
+
+.skeleton {
+ background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.2s ease-in-out infinite;
+ border-radius: 8px;
+}
+
+.skeleton.avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 9999px;
+}
+
+.skeleton.line {
+ height: 12px;
+ width: 120px;
+}
+
+.skeleton.line.short {
+ width: 80px;
+}
+
+.skeleton.btn {
+ height: 36px;
+ width: 96px;
+ border-radius: 8px;
+}
+
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+
+/* Badge de notificación */
+.friends-badge {
+ position: absolute;
+ top: -6px;
+ right: -6px;
+ background: #ef4444;
+ color: white;
+ font-size: 0.7rem;
+ font-weight: 700;
+ padding: 2px 6px;
+ border-radius: 10px;
+ min-width: 18px;
+ height: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+@media (max-width: 640px) {
+ .friend-row {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ .friend-row-actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+}
diff --git a/src/pages/app/social/Friends.jsx b/src/pages/app/social/Friends.jsx
new file mode 100644
index 0000000..e5123ee
--- /dev/null
+++ b/src/pages/app/social/Friends.jsx
@@ -0,0 +1,255 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import Avatar from '@/components/ui/Avatar';
+import Button from '@/components/ui/Button';
+import Card from '@/components/ui/Card';
+import Input from '@/components/ui/Input';
+import ConfirmModal from '@/components/ui/ConfirmModal';
+import {
+ listFriends,
+ removeFriend,
+ listReceivedRequests,
+} from '@/services/social/friendshipsService';
+import './Friends.css';
+
+// Obtiene un id estable del objeto amigo cualquiera sea su forma
+const getFriendId = (friend) => friend?.id || friend?._id || friend;
+
+const getFriendName = (friend) =>
+ friend?.username || friend?.name || friend?.alias || getFriendId(friend);
+
+function FriendRow({ friend, onRemove, isRemoving }) {
+ const friendId = getFriendId(friend);
+ const name = getFriendName(friend);
+
+ return (
+
+
+
+
+ {name}
+ {/* Si quieres mostrar el nombre completo debajo, descomenta la siguiente línea */}
+ {/* {friend?.full_name && {friend.full_name}} */}
+
+
+
+
+
+
+ );
+}
+
+function FriendSkeleton() {
+ return (
+
+
+
+
+ );
+}
+
+export default function Friends() {
+ const navigate = useNavigate();
+ const [friends, setFriends] = useState([]);
+ const [pendingCount, setPendingCount] = useState(0);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [search, setSearch] = useState('');
+ const [removingId, setRemovingId] = useState(null);
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [confirmFriend, setConfirmFriend] = useState(null);
+ const [toast, setToast] = useState(null);
+ const toastTimerRef = useRef(null);
+
+ const showToast = (type, message) => {
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
+ setToast({ type, message });
+ toastTimerRef.current = setTimeout(() => setToast(null), 3200);
+ };
+
+ const closeConfirm = () => {
+ setConfirmOpen(false);
+ setConfirmFriend(null);
+ };
+
+ const loadFriends = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const data = await listFriends();
+ setFriends(data?.friends || []);
+ } catch (err) {
+ setError('No se pudo cargar la lista de amigos.');
+ showToast('error', 'No se pudo cargar la lista de amigos.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const loadPendingCount = async () => {
+ try {
+ const data = await listReceivedRequests();
+ const requests = data?.requests || data || [];
+ setPendingCount(requests.length);
+ } catch (err) {
+ // Silently fail, just don't show the badge
+ setPendingCount(0);
+ }
+ };
+
+ useEffect(() => {
+ loadFriends();
+ loadPendingCount();
+ }, []);
+
+ const filtered = useMemo(() => {
+ const term = search.trim().toLowerCase();
+ if (!term) return friends;
+ return friends.filter((f) => getFriendName(f)?.toLowerCase().includes(term));
+ }, [friends, search]);
+
+ const handleRemove = (friend) => {
+ setConfirmFriend(friend);
+ setConfirmOpen(true);
+ };
+
+ const confirmRemoveFriend = async () => {
+ if (!confirmFriend) return;
+ const friendId = getFriendId(confirmFriend);
+ const name = getFriendName(confirmFriend);
+
+ const snapshot = [...friends];
+ setRemovingId(friendId);
+ setFriends((prev) => prev.filter((f) => getFriendId(f) !== friendId));
+
+ try {
+ await removeFriend(friendId);
+ showToast('success', `${name} fue eliminado de tus amigos.`);
+ } catch (err) {
+ setFriends(snapshot);
+ setError('No se pudo quitar al amigo.');
+ showToast('error', 'No se pudo quitar al amigo.');
+ } finally {
+ setRemovingId(null);
+ closeConfirm();
+ }
+ };
+
+ const renderContent = () => {
+ if (isLoading) {
+ return (
+
+ {Array.from({ length: 4 }).map((_, idx) => (
+
+ ))}
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+
+ );
+ }
+
+ if (!filtered.length) {
+ return (
+
+ No tienes amigos aún.
+
+
+ );
+ }
+
+ return (
+
+ {filtered.map((friend) => (
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
+
+
Amigos
+
Gestiona tus amistades y conversa rápido.
+
+
+
+ setSearch(e.target.value)}
+ fullWidth
+ />
+
+
+
+ {renderContent()}
+
+
+ {toast && (
+
+ {toast.message}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/app/social/UserSearch.css b/src/pages/app/social/UserSearch.css
new file mode 100644
index 0000000..03b252b
--- /dev/null
+++ b/src/pages/app/social/UserSearch.css
@@ -0,0 +1,150 @@
+.user-search-container {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 2rem 1rem;
+}
+
+.user-search-header {
+ margin-bottom: 1.5rem;
+}
+
+.user-search-header h2 {
+ font-size: 1.875rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0 0 0.5rem 0;
+}
+
+.user-search-description {
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ margin: 0;
+}
+
+.user-search-input-wrapper {
+ margin-bottom: 1.5rem;
+}
+
+.user-search-input {
+ width: 100%;
+ font-size: 1rem;
+}
+
+.user-search-error {
+ padding: 1rem;
+ background-color: var(--error-bg, #fee);
+ color: var(--error-text, #c00);
+ border-radius: 8px;
+ margin-bottom: 1rem;
+ text-align: center;
+}
+
+.user-search-loading {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-secondary);
+}
+
+.user-search-empty {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-secondary);
+ font-style: italic;
+}
+
+.user-search-results {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.user-search-result-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color, #e5e7eb);
+ transition: background-color 0.2s;
+}
+
+.user-search-result-item:last-child {
+ border-bottom: none;
+}
+
+.user-search-result-item:hover {
+ background-color: var(--hover-bg, #f9fafb);
+}
+
+.user-search-result-left {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ flex: 1;
+ min-width: 0;
+}
+
+.user-search-result-info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ min-width: 0;
+}
+
+.user-search-result-username {
+ font-weight: 600;
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: color 0.2s;
+}
+
+.user-search-result-username:hover {
+ color: var(--primary-color, #3b82f6);
+ text-decoration: underline;
+}
+
+.user-search-result-fullname {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.user-search-friend-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0.125rem 0.5rem;
+ background-color: var(--success-bg, #dcfce7);
+ color: var(--success-text, #16a34a);
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.user-search-result-actions {
+ flex-shrink: 0;
+ margin-left: 1rem;
+}
+
+@media (max-width: 640px) {
+ .user-search-container {
+ padding: 1rem 0.5rem;
+ }
+
+ .user-search-header h2 {
+ font-size: 1.5rem;
+ }
+
+ .user-search-result-item {
+ padding: 0.75rem;
+ }
+
+ .user-search-result-left {
+ gap: 0.75rem;
+ }
+
+ .user-search-result-fullname {
+ display: none;
+ }
+}
diff --git a/src/pages/app/social/UserSearch.jsx b/src/pages/app/social/UserSearch.jsx
new file mode 100644
index 0000000..50b01ee
--- /dev/null
+++ b/src/pages/app/social/UserSearch.jsx
@@ -0,0 +1,222 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import Avatar from '@/components/ui/Avatar';
+import Button from '@/components/ui/Button';
+import Card from '@/components/ui/Card';
+import Input from '@/components/ui/Input';
+import { searchProfiles } from '@/services/profileService';
+import { sendFriendRequest, listFriends, listReceivedRequests } from '@/services/social/friendshipsService';
+import { useProfileData } from '@/hooks/use-profile-data';
+import './UserSearch.css';
+
+function UserSearch() {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+ const [isSearching, setIsSearching] = useState(false);
+ const [error, setError] = useState(null);
+ const [sentRequests, setSentRequests] = useState(new Set());
+ const [incomingRequests, setIncomingRequests] = useState(new Set());
+ const [friends, setFriends] = useState(new Set());
+ const navigate = useNavigate();
+ const { profile } = useProfileData();
+
+ // Extract current user ID - use userId (from users table) not _id (from profiles table)
+ // This is important because social-service uses userId for friend relationships
+ const currentUserId = profile?.userId || profile?._id || profile?.id;
+
+ // Load friends list on mount
+ useEffect(() => {
+ const loadFriends = async () => {
+ try {
+ const result = await listFriends();
+ const friendIds = new Set(
+ result.friends.map(f => f.id || f._id || f.userId)
+ );
+ setFriends(friendIds);
+ } catch (err) {
+ console.error('Error loading friends:', err);
+ }
+ };
+
+ const loadIncoming = async () => {
+ try {
+ const res = await listReceivedRequests();
+ const pendingSenders = new Set(
+ (res.requests || []).map(r => r?.sender?.id || r?.sender?._id || r?.sender?.userId).filter(Boolean)
+ );
+ setIncomingRequests(pendingSenders);
+ } catch (err) {
+ console.error('Error loading incoming requests:', err);
+ }
+ };
+
+ loadFriends();
+ loadIncoming();
+ }, []);
+
+ // Debounce search
+ useEffect(() => {
+ if (!searchQuery.trim() || searchQuery.trim().length < 2) {
+ setSearchResults([]);
+ return;
+ }
+
+ const timeoutId = setTimeout(async () => {
+ setIsSearching(true);
+ setError(null);
+
+ try {
+ const users = await searchProfiles(searchQuery.trim());
+
+ // Filtra el usuario actual de los resultados
+ const filteredUsers = (Array.isArray(users) ? users : []).filter(user => {
+ const userId = user?.userId || user?.id || user?._id;
+ return userId !== currentUserId;
+ });
+ setSearchResults(filteredUsers);
+ } catch (err) {
+ console.error('Error searching users:', err);
+ setError('Error al buscar usuarios. Por favor, intenta de nuevo.');
+ setSearchResults([]);
+ } finally {
+ setIsSearching(false);
+ }
+ }, 1500);
+
+ return () => clearTimeout(timeoutId);
+ }, [searchQuery, currentUserId]);
+
+ const handleSendRequest = async (userId, username) => {
+ try {
+ await sendFriendRequest(userId);
+ setSentRequests(prev => new Set([...prev, userId]));
+ } catch (err) {
+ console.error('Error sending friend request:', err);
+ alert(`Error al enviar solicitud a ${username}`);
+ }
+ };
+
+ const handleViewProfile = (username) => {
+ navigate(`/app/profile/${username}`);
+ };
+
+ return (
+
+
+
+
Buscar Usuarios
+
+ Encuentra y conecta con otros usuarios
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="user-search-input"
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {isSearching && (
+
+ Buscando...
+
+ )}
+
+ {!isSearching && searchQuery && searchResults.length === 0 && !error && (
+
+ No se encontraron usuarios con "{searchQuery}"
+
+ )}
+
+ {searchResults.length > 0 && (
+
+ )}
+
+
+ );
+}
+
+export default UserSearch;
diff --git a/src/services/beatsService.js b/src/services/beatsService.js
index b92c2b3..40ded46 100644
--- a/src/services/beatsService.js
+++ b/src/services/beatsService.js
@@ -54,7 +54,6 @@ const setCachedSignedUrls = (urlsMap) => {
*/
export const clearSignedUrlCache = () => {
signedUrlCache.clear();
- console.log('🧹 Signed URL cache cleared');
};
// ============================================================
@@ -63,30 +62,15 @@ export const clearSignedUrlCache = () => {
export const getBeats = async (filters = {}) => {
try {
- console.log('📡 Making API request to /beats with filters:', filters);
- console.log('🔗 Base URL:', client.defaults.baseURL);
-
const { data } = await client.get('/beats', { params: filters });
- console.log('📦 Raw API response:', data);
// Extract the beats array from the API response structure
if (data.success && data.data) {
- console.log('✅ Returning beats array:', data.data);
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data; // fallback to raw data
}
} catch (error) {
- console.error('🚨 Service error details:', {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- data: error.response?.data,
- url: error.config?.url,
- baseURL: error.config?.baseURL
- });
throw error;
}
};
@@ -134,75 +118,59 @@ export const getBeatById = async (id) => {
const { data } = await client.get(`/beats/${id}`);
// Extract the beats array from the API response structure
if (data.success && data.data) {
- console.log('✅ Returning beats array:', data.data);
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data; // fallback to raw data
}
} catch (error) {
- console.error(`Error fetching beat with id ${id}:`, error);
throw error;
}
};
export const createBeat = async (beatData) => {
try {
- console.log('📝 Creating new beat:', beatData);
const { data } = await client.post('/beats', beatData);
if (data.success && data.data) {
- console.log('✅ Beat created successfully:', data.data);
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data;
}
} catch (error) {
- console.error('🚨 Error creating beat:', error);
throw error;
}
};
export const updateBeat = async (id, beatData) => {
try {
- console.log('✏️ Updating beat:', id, beatData);
const { data } = await client.put(`/beats/${id}`, beatData);
if (data.success && data.data) {
- console.log('✅ Beat updated successfully:', data.data);
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data;
}
} catch (error) {
- console.error('🚨 Error updating beat:', error);
throw error;
}
};
export const deleteBeat = async (id) => {
try {
- console.log('🗑️ Deleting beat:', id);
const { data } = await client.delete(`/beats/${id}`);
if (data.success) {
- console.log('✅ Beat deleted successfully');
return data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data;
}
} catch (error) {
- console.error('🚨 Error deleting beat:', error);
throw error;
}
};
export const getPresignedUrl = async ({ extension, mimetype, size }) => {
try {
- console.log('🔑 Requesting presigned URL:', { extension, mimetype, size });
const { data } = await client.post('/beats/upload-url', {
extension,
mimetype,
@@ -210,13 +178,11 @@ export const getPresignedUrl = async ({ extension, mimetype, size }) => {
});
if (data.success && data.data) {
- console.log('✅ Presigned URL received:', data.data);
return data.data;
} else {
- throw new Error(data.message || 'Failed to get upload URL');
+ throw new Error(data.message || 'Error al obtener la URL de subida');
}
} catch (error) {
- console.error('🚨 Error getting presigned URL:', error);
throw error;
}
};
@@ -229,7 +195,6 @@ export const getPresignedUrl = async ({ extension, mimetype, size }) => {
*/
export const uploadFileToS3 = async (presignedData, file) => {
try {
- console.log('📤 Uploading file to S3 via Presigned POST...');
const { url, fields } = presignedData;
// Build FormData with all presigned policy fields
@@ -254,14 +219,11 @@ export const uploadFileToS3 = async (presignedData, file) => {
if (!response.ok) {
const errorText = await response.text();
- console.error('🚨 S3 Upload error response:', errorText);
- throw new Error(`S3 Upload failed: ${response.status} - ${errorText}`);
+ throw new Error(`Error al subir a S3: ${response.status} - ${errorText}`);
}
- console.log('✅ File uploaded to S3 successfully via POST');
return true;
} catch (error) {
- console.error('🚨 Error uploading to S3:', error);
throw error;
}
};
@@ -273,10 +235,7 @@ export const uploadFileToS3 = async (presignedData, file) => {
*/
export const searchBeats = async (query) => {
try {
- console.log('🔍 Searching beats with query:', query);
-
if (!query || query.length < 2) {
- console.warn('⚠️ Search query must be at least 2 characters');
return [];
}
@@ -285,14 +244,11 @@ export const searchBeats = async (query) => {
});
if (data.success && data.data) {
- console.log('✅ Search results:', data.data);
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return Array.isArray(data) ? data : [];
}
} catch (error) {
- console.error('🚨 Error searching beats:', error);
throw error;
}
};
@@ -303,18 +259,14 @@ export const searchBeats = async (query) => {
*/
export const getBeatsStats = async () => {
try {
- console.log('📊 Fetching beats statistics...');
const { data } = await client.get('/beats/stats');
if (data.success && data.data) {
- console.log('✅ Beats stats:', data.data);
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data;
}
} catch (error) {
- console.error('🚨 Error fetching beats stats:', error);
throw error;
}
};
@@ -326,18 +278,14 @@ export const getBeatsStats = async () => {
*/
export const incrementPlayCount = async (id) => {
try {
- console.log('▶️ Incrementing play count for beat:', id);
const { data } = await client.post(`/beats/${id}/play`);
if (data.success) {
- console.log('✅ Play count incremented');
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data;
}
} catch (error) {
- console.error('🚨 Error incrementing play count:', error);
throw error;
}
};
@@ -349,18 +297,14 @@ export const incrementPlayCount = async (id) => {
*/
export const downloadBeat = async (id) => {
try {
- console.log('⬇️ Downloading beat:', id);
const { data } = await client.get(`/beats/${id}/download`);
if (data.success && data.data) {
- console.log('✅ Download initiated', data.data);
return data.data;
} else {
- console.warn('⚠️ Unexpected API response structure:', data);
return data;
}
} catch (error) {
- console.error('🚨 Error downloading beat:', error);
throw error;
}
};
@@ -377,18 +321,15 @@ export const getAudioStreamUrl = async (id, skipCache = false) => {
if (!skipCache) {
const cached = getCachedSignedUrl(id);
if (cached) {
- console.log('📦 Using cached signed URL for beat:', id);
return cached;
}
}
try {
- console.log('🎵 Fetching signed stream URL for beat:', id);
const { data } = await client.get(`/beats/${id}/audio`);
// Backend returns { streamUrl: "...", coverUrl: "..." }
if (data.streamUrl) {
- console.log('✅ Stream URL received', data.coverUrl ? '(with cover)' : '(no cover)');
const urls = {
streamUrl: data.streamUrl,
coverUrl: data.coverUrl || null
@@ -405,23 +346,19 @@ export const getAudioStreamUrl = async (id, skipCache = false) => {
return { streamUrl: data, coverUrl: null };
}
- throw new Error('No stream URL in response');
+ throw new Error('No se recibió URL de streaming');
} catch (error) {
// Handle specific HTTP error codes
if (error.response?.status === 403) {
- console.error('🚫 Not authorized to stream this beat');
throw new Error('No autorizado para reproducir este beat');
}
if (error.response?.status === 429) {
- console.error('⏳ Rate limit exceeded for streaming');
throw new Error('Límite de reproducciones excedido. Intenta más tarde.');
}
if (error.response?.status === 404) {
- console.error('🔍 Beat audio not found');
throw new Error('Audio no encontrado');
}
- console.error('🚨 Error fetching stream URL:', error);
throw error;
}
};
@@ -453,35 +390,46 @@ export const getBatchSignedUrls = async (beatIds) => {
// If all are cached, return immediately
if (uncachedIds.length === 0) {
- console.log('📦 All', beatIds.length, 'signed URLs from cache');
return result;
}
- console.log('📦 Cache hit:', beatIds.length - uncachedIds.length, '| Fetching:', uncachedIds.length);
-
try {
// Batch fetch only uncached IDs (limit 10 per request)
const batchSize = 10;
for (let i = 0; i < uncachedIds.length; i += batchSize) {
const batch = uncachedIds.slice(i, i + batchSize);
- console.log('🎵 Batch fetching signed URLs for', batch.length, 'beats');
const { data } = await client.post('/beats/batch/signed-urls', { beatIds: batch });
if (data.success && data.data) {
// Cache and add to result
setCachedSignedUrls(data.data);
Object.assign(result, data.data);
-
- console.log('✅ Batch URLs received:', data.meta?.resolved, 'resolved,', data.meta?.errors, 'errors');
}
}
return result;
} catch (error) {
- console.error('🚨 Error fetching batch signed URLs:', error);
-
// Fallback: return what we have from cache, don't break the UI
return result;
}
};
+
+/**
+ * Toggle beat promotion status
+ * @param {string} id - Beat ID
+ * @returns {Promise