From 22ad1d25e0e927f3e7fb649e6b688c2cc86ceee8 Mon Sep 17 00:00:00 2001 From: MiguelEncina Date: Mon, 5 Jan 2026 01:59:27 +0100 Subject: [PATCH 1/4] feat: frontend space implementation of beats upload features --- .../features/player/BeatDetailPlayer.jsx | 12 +- src/components/forms/BeatForm.jsx | 75 ++++++++---- src/pages/app/beats/BeatDetailPage.jsx | 107 ++++++++++++++++-- src/pages/app/beats/MyBeatsListPage.jsx | 30 ++++- src/services/beatsService.js | 19 ++++ 5 files changed, 199 insertions(+), 44 deletions(-) diff --git a/src/components/features/player/BeatDetailPlayer.jsx b/src/components/features/player/BeatDetailPlayer.jsx index 75e10da..5ad2a3d 100644 --- a/src/components/features/player/BeatDetailPlayer.jsx +++ b/src/components/features/player/BeatDetailPlayer.jsx @@ -8,6 +8,7 @@ import { incrementPlayCount, downloadBeat, getAudioStreamUrl } from '../../../se import LivingWaveform from './LivingWaveform'; import { usePlayerStore } from '../../../store/usePlayerStore'; import './BeatDetailPlayer.css'; +import { Feature, On, Default} from 'space-react-client'; const BeatDetailPlayer = ({ beat, isOwner }) => { // Global player store - SINGLE SOURCE OF TRUTH @@ -287,9 +288,14 @@ const BeatDetailPlayer = ({ beat, isOwner }) => {
{beat.isDownloadable && ( - + + + + + + )}
diff --git a/src/components/forms/BeatForm.jsx b/src/components/forms/BeatForm.jsx index cbf4d93..75ed272 100644 --- a/src/components/forms/BeatForm.jsx +++ b/src/components/forms/BeatForm.jsx @@ -22,6 +22,8 @@ import Textarea from '../ui/Textarea'; import Toggle from '../ui/Toggle'; import FileUploader from '../ui/FileUploader'; import './BeatForm.css'; +import { useNavigate } from "react-router-dom"; +import { Feature, On, Default, Loading, ErrorFallback } from 'space-react-client'; const BeatForm = ({ initialData = null, @@ -40,6 +42,7 @@ const BeatForm = ({ }); const [audioFile, setAudioFile] = useState(null); const [coverFile, setCoverFile] = useState(null); + const navigate = useNavigate(); // Helper to resolve cover URL safely const getCoverUrl = (data) => { @@ -226,32 +229,54 @@ const BeatForm = ({ )} {/* Cover Art Section */} - -
- -
-

Cover Art

-

Upload an image for your beat

-
-
+ + + +
+ +
+

Cover Art

+

Upload an image for your beat

+
+
-
- -
-
+
+ +
+
+ + + + + + Comprobando tu plan... + + + Error al verificar tu plan + + {/* Right Column: Metadata */} diff --git a/src/pages/app/beats/BeatDetailPage.jsx b/src/pages/app/beats/BeatDetailPage.jsx index 5c9b810..31737bd 100644 --- a/src/pages/app/beats/BeatDetailPage.jsx +++ b/src/pages/app/beats/BeatDetailPage.jsx @@ -10,7 +10,9 @@ import { Tag, Download, CheckCircle2, + Star, } from "lucide-react"; +import { Feature, On, Default, Loading, ErrorFallback } from 'space-react-client'; import Card from "../../../components/ui/Card"; import Button from "../../../components/ui/Button"; @@ -24,6 +26,7 @@ import { getBeatById, deleteBeat, downloadBeat, + togglePromotion, } from "../../../services/beatsService"; import { getCurrentUserId } from "../../../services/authService"; import "./BeatDetailPage.css"; @@ -37,6 +40,7 @@ const BeatDetailPage = () => { const [error, setError] = useState(null); const [showDeleteModal, setShowDeleteModal] = useState(false); const [deleting, setDeleting] = useState(false); + const [promoting, setPromoting] = useState(false); const [isOwner, setIsOwner] = useState(false); // Placeholder for ownership logic // Stats state to update download count locally const [stats, setStats] = useState({ plays: 0, downloads: 0 }); @@ -114,6 +118,24 @@ const BeatDetailPage = () => { } }; + const handleTogglePromotion = async () => { + try { + setPromoting(true); + const data = await togglePromotion(beat._id); + if (data) { + setBeat((prev) => ({ + ...prev, + promoted: data.promoted, + })); + } + } catch (error) { + console.error("Error toggling promotion:", error); + // Could show toast with error.response?.data?.message + } finally { + setPromoting(false); + } + }; + if (loading) return
Loading...
; if (error) return
{error}
; if (!beat) return null; @@ -194,9 +216,8 @@ const BeatDetailPage = () => { {isOwner && (
{beat.isPublic ? ( @@ -212,14 +233,36 @@ const BeatDetailPage = () => { {/* Botón Principal */} {beat.isDownloadable && !isOwner && ( - + + + + + + + + + Comprobando tu plan... + + + Error al verificar tu plan + + )}
@@ -238,6 +281,48 @@ const BeatDetailPage = () => { {isOwner && (
Owner Controls + + {/* Promote Beat Button */} + + + + + + + + + Comprobando add-on... + + + Error al verificar add-on + + +
- - - + + + + + + + + + + + + + Comprobando tu plan... + + + Error al verificar tu plan + +
diff --git a/src/services/beatsService.js b/src/services/beatsService.js index b92c2b3..f55a04f 100644 --- a/src/services/beatsService.js +++ b/src/services/beatsService.js @@ -485,3 +485,22 @@ export const getBatchSignedUrls = async (beatIds) => { return result; } }; + +/** + * Toggle beat promotion status + * @param {string} id - Beat ID + * @returns {Promise} Updated promotion status { beatId, promoted } + */ +export const togglePromotion = async (id) => { + try { + const { data } = await client.patch(`/beats/${id}/promote`); + + if (data.success && data.data) { + return data.data; + } else { + return data; + } + } catch (error) { + throw error; + } +}; From eee254ad1672b68dd6fb5c4abaa73437a27a3faa Mon Sep 17 00:00:00 2001 From: MiguelEncina Date: Mon, 5 Jan 2026 03:17:36 +0100 Subject: [PATCH 2/4] fix: visual upgrades --- src/App.jsx | 1 - src/pages/app/explore/ExplorePage.css | 19 +++++ src/pages/app/explore/ExplorePage.jsx | 4 +- src/pages/app/explore/components/BeatCard.css | 9 ++- .../components/BeatsExploreSection.css | 81 +++++++++++++++---- .../components/BeatsExploreSection.jsx | 10 +-- 6 files changed, 99 insertions(+), 25 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 93281fc..7156354 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -51,7 +51,6 @@ function App() { {/* Public Routes - Accessible to everyone */} }> } /> - } /> } /> } /> diff --git a/src/pages/app/explore/ExplorePage.css b/src/pages/app/explore/ExplorePage.css index 6b7e098..6407d4c 100644 --- a/src/pages/app/explore/ExplorePage.css +++ b/src/pages/app/explore/ExplorePage.css @@ -1,10 +1,17 @@ /* ExplorePage.css - Estilos para la página de exploración */ +/* Fix para contener el desbordamiento solo en esta página */ +.private-content-wrapper:has(.explore-page) { + max-width: calc(100vw - 260px); + overflow-x: hidden; +} + .explore-page { min-height: 100vh; padding: 2rem; max-width: 1400px; margin: 0 auto; + box-sizing: border-box; } /* Header */ @@ -147,6 +154,18 @@ display: flex; flex-direction: column; gap: 3rem; + /* Limitar al ancho disponible */ + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +/* Wrapper que contiene el desbordamiento */ +.explore-content-wrapper { + width: 100%; + max-width: 100%; + overflow: hidden; } /* Responsive */ diff --git a/src/pages/app/explore/ExplorePage.jsx b/src/pages/app/explore/ExplorePage.jsx index 6e124b3..448cbaf 100644 --- a/src/pages/app/explore/ExplorePage.jsx +++ b/src/pages/app/explore/ExplorePage.jsx @@ -124,7 +124,9 @@ export default function ExplorePage() {
- {renderSections()} +
+ {renderSections()} +
); 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) => ( Date: Mon, 5 Jan 2026 13:32:21 +0100 Subject: [PATCH 3/4] fix: translate pages and manage backend errors --- src/components/forms/BeatForm.jsx | 73 +++++++++++------------ src/pages/app/beats/BeatDetailPage.jsx | 52 ++++++++--------- src/pages/app/beats/BeatFormPage.jsx | 14 ++--- src/pages/app/beats/BeatsListPage.jsx | 60 +++++++++---------- src/pages/app/beats/ColumnSelector.jsx | 2 +- src/pages/app/beats/MyBeatsListPage.jsx | 21 ++----- src/services/beatsService.js | 77 +------------------------ 7 files changed, 107 insertions(+), 192 deletions(-) diff --git a/src/components/forms/BeatForm.jsx b/src/components/forms/BeatForm.jsx index 75ed272..1149cb1 100644 --- a/src/components/forms/BeatForm.jsx +++ b/src/components/forms/BeatForm.jsx @@ -63,7 +63,6 @@ const BeatForm = ({ // Sync state with initialData if it changes (async load) useEffect(() => { if (initialData) { - console.log('BeatForm received initialData:', initialData); setFormData({ title: initialData.title || '', genre: initialData.genre || '', @@ -74,7 +73,6 @@ const BeatForm = ({ }); const derivedCoverUrl = getCoverUrl(initialData); - console.log('Derived Cover URL:', derivedCoverUrl); setCoverPreviewUrl(derivedCoverUrl); } }, [initialData]); @@ -126,22 +124,21 @@ const BeatForm = ({ const fileExtension = file.name.split('.').pop().toLowerCase(); if (!validTypes.includes(file.type) && !validExtensions.includes(fileExtension)) { - setError('Invalid file type. Allowed: MP3, WAV, FLAC, AAC'); + setError('Tipo de archivo inválido. Permitidos: MP3, WAV, FLAC, AAC'); return false; } if (file.size > 50 * 1024 * 1024) { - setError('File size too large. Maximum size is 50MB.'); + setError('Archivo demasiado grande. El tamaño máximo es 50MB.'); return false; } try { const metadata = await parseBlob(file); if (!metadata.format.codec) { - setError('Invalid audio file content.'); + setError('Contenido de archivo de audio inválido.'); return false; } } catch (err) { - console.error('Error parsing audio file:', err); - setError('Failed to verify audio file.'); + setError('Error al verificar el archivo de audio.'); return false; } @@ -180,7 +177,7 @@ const BeatForm = ({ const handleSubmit = (e) => { e.preventDefault(); - if (!isEditing && !audioFile) { setError('Please upload an audio file.'); return; } + if (!isEditing && !audioFile) { setError('Por favor, sube un archivo de audio.'); return; } const submitData = { ...formData, }; @@ -207,8 +204,8 @@ const BeatForm = ({
-

Audio File

-

Upload your beat (MP3, WAV, FLAC, AAC)

+

Archivo de Audio

+

Sube tu beat (MP3, WAV, FLAC, AAC)

@@ -219,10 +216,10 @@ const BeatForm = ({ file={audioFile} onChange={handleAudioChange} onClear={handleClearAudio} - title="Drop your beat here" - description="or click to browse files" + title="Arrastra tu beat aquí" + description="o haz clic para buscar archivos" formats="MP3, WAV, FLAC, AAC" - maxSizeText="Max 50MB" + maxSizeText="Máx 50MB" />
@@ -235,8 +232,8 @@ const BeatForm = ({
-

Cover Art

-

Upload an image for your beat

+

Portada

+

Sube una imagen para tu beat

@@ -247,10 +244,10 @@ const BeatForm = ({ file={coverFile} onChange={handleCoverChange} onClear={handleClearCover} - title="Drop your cover here" - description="or click to upload image" + title="Arrastra tu portada aquí" + description="o haz clic para subir imagen" formats="JPG, PNG, WEBP" - maxSizeText="Max 5MB" + maxSizeText="Máx 5MB" isImage={true} previewUrl={coverPreviewUrl} icon={Image} @@ -286,27 +283,27 @@ const BeatForm = ({
-

Beat Information

-

Add details about your beat

+

Información del Beat

+

Añade detalles sobre tu beat

{/* Title - Full Width Directo */} } name="title" value={formData.title} onChange={handleInputChange} - placeholder="Enter beat title" + placeholder="Introduce el título del beat" fullWidth required /> {/* Genre */}