diff --git a/app/package-lock.json b/app/package-lock.json index f5b88f8..c635182 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -17,6 +17,7 @@ "@ionic/react-router": "^8.5.0", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", + "framer-motion": "^12.23.12", "ionicons": "^7.4.0", "jotai": "^2.12.5", "lucide-react": "^0.536.0", @@ -6560,6 +6561,33 @@ "node": ">= 6" } }, + "node_modules/framer-motion": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.12", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -8725,6 +8753,21 @@ "dev": true, "license": "MIT" }, + "node_modules/motion-dom": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/app/package.json b/app/package.json index 419b61b..25c8bc0 100644 --- a/app/package.json +++ b/app/package.json @@ -21,6 +21,7 @@ "@ionic/react-router": "^8.5.0", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", + "framer-motion": "^12.23.12", "ionicons": "^7.4.0", "jotai": "^2.12.5", "lucide-react": "^0.536.0", diff --git a/app/src/App.tsx b/app/src/App.tsx index 31fe302..ee6e25b 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -32,7 +32,7 @@ setupIonicReact(); const App: React.FC = () => { const [current, setCurrent] = useAtom(currentTrackAtom); - const [accessGranted, setAccessGranted] = useState(null); + // const [accessGranted, setAccessGranted] = useState(null); useEffect(() => { setCurrent(mocks.nowPlaying); @@ -44,8 +44,9 @@ const App: React.FC = () => { // } return ( + - + {/* */} diff --git a/app/src/atoms/index.tsx b/app/src/atoms/index.tsx index 3127e7a..52ca4c6 100644 --- a/app/src/atoms/index.tsx +++ b/app/src/atoms/index.tsx @@ -1,5 +1,7 @@ import { atom } from 'jotai'; import { NowPlayingData } from '../mocks'; +import { QueueItem } from '../models'; export const themeAtom = atom<'light' | 'dark' | 'red'>('light'); export const currentTrackAtom = atom() +export const codaRecordsAtom = atom([]); \ No newline at end of file diff --git a/app/src/components/NowPlayingTab.css b/app/src/components/NowPlayingTab.css index c2e3860..ca6d4d6 100644 --- a/app/src/components/NowPlayingTab.css +++ b/app/src/components/NowPlayingTab.css @@ -6,6 +6,7 @@ display: block; position: relative; border-radius: 8px; + } .now-playing-overlay { diff --git a/app/src/components/Queue.tsx b/app/src/components/Queue.tsx index 3d60f38..595a808 100644 --- a/app/src/components/Queue.tsx +++ b/app/src/components/Queue.tsx @@ -11,10 +11,11 @@ import { } from "@ionic/react"; import React, { useState } from "react"; - import { ArrowBigUp, Plus } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; interface NowPlayingDiscProps { + id: string; coverUrl: string; title: string; artist: string; @@ -24,13 +25,17 @@ interface NowPlayingDiscProps { interface QueueProps { songs: NowPlayingDiscProps[]; + onUpvote: (id: string) => void; } -const Queue: React.FC = ({ songs }) => { +const Queue: React.FC = ({ songs, onUpvote }) => { const [showModal, setShowModal] = useState(false); const [searchText, setSearchText] = useState(""); - const handleUpVote = () => {}; + // Ordina i brani in ordine decrescente per upvotes + const sortedSongs = [...songs].sort( + (a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0) + ); return (
@@ -40,64 +45,72 @@ const Queue: React.FC = ({ songs }) => { size={24} color="white" strokeWidth={2} - onClick={() => { - setShowModal(true); - }} + onClick={() => setShowModal(true)} />
- {songs.map((song, index) => { - const isUpvoted = song.upvoted ?? false; - const votes = song.upvotes ?? 0; + + {sortedSongs.map((song) => { + const isUpvoted = song.upvoted ?? false; + const votes = song.upvotes ?? 0; - return ( -
- {`${song.title} -
-
{song.title}
-
{song.artist}
-
- -
- - {votes} - - -
-
- ); - })} +
+
{song.title}
+
{song.artist}
+
+ +
onUpvote(song.id)} + > + + {votes} + + +
+ + ); + })} +
setShowModal(false)}> Add Songs - setShowModal(false)} - > + setShowModal(false)}> Close diff --git a/app/src/components/testCodaDbMongo.tsx b/app/src/components/testCodaDbMongo.tsx index 3e2ad3c..c3f0f32 100644 --- a/app/src/components/testCodaDbMongo.tsx +++ b/app/src/components/testCodaDbMongo.tsx @@ -9,6 +9,9 @@ import { IonButton, IonSpinner, IonText, + IonGrid, + IonRow, + IonCol, } from '@ionic/react'; import { useCoda } from '../hooks/useCoda'; @@ -24,15 +27,18 @@ const TestCoda: React.FC = () => { } = useCoda(); // Stati per i nuovi record da aggiungere - const [title, setTitle] = useState(''); + const [titolo, setTitolo] = useState(''); const [artista, setArtista] = useState(''); const [coverUrl, setCoverUrl] = useState(''); const [color, setColor] = useState(''); const handleAdd = () => { - if (!title || !artista || !coverUrl || !color) return alert('Completa tutti i campi'); - addRecord({ title, artista, coverUrl, color, voti: 0 }); - setTitle(''); + if (!titolo || !artista || !coverUrl || !color) { + alert('Compila tutti i campi prima di aggiungere'); + return; + } + addRecord({ titolo, artista, coverUrl, color, voti: 0 }); + setTitolo(''); setArtista(''); setCoverUrl(''); setColor(''); @@ -61,9 +67,9 @@ const TestCoda: React.FC = () => {

Aggiungi Nuovo Record

setTitle(e.detail.value!)} + placeholder="Titolo" + value={titolo} + onIonChange={(e) => setTitolo(e.detail.value!)} /> { {records.length === 0 && !loading &&

Nessun record disponibile

} {records.map((r) => ( - - -

{r.title}

-

Artista: {r.artista}

- {r.title} -

Voti: {r.voti}

-
- updateVoti(r.id, 1)} disabled={loading}> - +1 - - updateVoti(r.id, -1)} disabled={loading}> - -1 - - deleteRecord(r.id)} disabled={loading}> - Elimina - + + + + + {r.titolo} + + + +

{r.titolo}

+

Artista: {r.artista}

+

Voti: {r.voti}

+
+
+ + updateVoti(r.id, 1)} disabled={loading}> + +1 + + updateVoti(r.id, -1)} disabled={loading}> + -1 + + deleteRecord(r.id)} disabled={loading}> + Elimina + + +
+
))}
diff --git a/app/src/hooks/useCoda.tsx b/app/src/hooks/useCoda.tsx index 313a55b..f027108 100644 --- a/app/src/hooks/useCoda.tsx +++ b/app/src/hooks/useCoda.tsx @@ -1,9 +1,11 @@ -import { useState, useEffect } from "react"; +import { useAtom } from "jotai"; +import { useEffect, useState } from "react"; import { MONGO_DB_CODA_API } from "../api_endpoints"; +import { codaRecordsAtom } from "../atoms"; const API_URL = MONGO_DB_CODA_API; -export interface Record { +export interface QueueItem { id: string; titolo: string; coverUrl: string; @@ -13,15 +15,15 @@ export interface Record { } export const useCoda = () => { - const [records, setRecords] = useState([]); + const [records, setRecords] = useAtom(codaRecordsAtom); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchJson = async (input: RequestInfo, init?: RequestInit) => { const res = await fetch(input, init); if (!res.ok) { - const text = await res.text(); - throw new Error(text || "Errore fetch"); + const msg = await res.text(); + throw new Error(msg || "Errore nella chiamata fetch"); } return res.json(); }; @@ -30,12 +32,8 @@ export const useCoda = () => { setLoading(true); try { const response = await fetchJson(`${API_URL}/read`); - if (response.success) { - setRecords(response.data); - setError(null); - } else { - throw new Error("Errore nella risposta del server"); - } + setRecords(response.data); + setError(null); } catch (e: any) { setError(e.message || "Errore fetch"); } finally { @@ -43,20 +41,25 @@ export const useCoda = () => { } }; - const addRecord = async (record: Omit) => { + useEffect(() => { + fetchRecords(); // chiamata iniziale + + const interval = setInterval(() => { + fetchRecords(); // polling ogni 5 secondi + }, 5000); + + return () => clearInterval(interval); // pulizia su unmount + }, []); + + const addRecord = async (record: Omit) => { try { - const body = JSON.stringify(record); const response = await fetchJson(`${API_URL}/add`, { method: "POST", headers: { "Content-Type": "application/json" }, - body, + body: JSON.stringify(record), }); - if (response.success) { - setRecords((prev) => [...prev, response.data]); - setError(null); - } else { - throw new Error("Errore aggiunta record"); - } + setRecords((prev) => [...prev, response.data]); + setError(null); } catch (e: any) { setError(e.message || "Errore add"); } @@ -64,43 +67,28 @@ export const useCoda = () => { const deleteRecord = async (id: string) => { try { - const response = await fetchJson(`${API_URL}/delete/${id}`, { - method: "DELETE", - }); - if (response.success) { - setRecords((prev) => prev.filter((r) => r.id !== id)); - setError(null); - } else { - throw new Error("Errore eliminazione record"); - } + await fetchJson(`${API_URL}/delete/${id}`, { method: "DELETE" }); + setRecords((prev) => prev.filter((r) => r.id !== id)); + setError(null); } catch (e: any) { setError(e.message || "Errore delete"); } }; - const updateVoti = async (id: string, delta: 1 | -1) => { + const updateVoti = async (id: string, increment: boolean) => { try { const response = await fetchJson(`${API_URL}/vote/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ increment: delta === 1 }), + body: JSON.stringify({ increment }), }); - if (response.success) { - const updatedItem: Record = response.data; - setRecords((prev) => prev.map((r) => (r.id === id ? updatedItem : r))); - setError(null); - } else { - throw new Error("Errore aggiornamento voti"); - } + setRecords((prev) => prev.map((r) => (r.id === id ? response.data : r))); + setError(null); } catch (e: any) { setError(e.message || "Errore update voti"); } }; - useEffect(() => { - fetchRecords(); - }, []); - return { records, loading, diff --git a/app/src/models.tsx b/app/src/models.tsx new file mode 100644 index 0000000..82c6e8c --- /dev/null +++ b/app/src/models.tsx @@ -0,0 +1,8 @@ +export interface QueueItem { + id: string; + titolo: string; + coverUrl: string; + artista: string; + color: string; + voti: number; +} diff --git a/app/src/pages/Tab.css b/app/src/pages/Tab.css index defdac7..544e44e 100644 --- a/app/src/pages/Tab.css +++ b/app/src/pages/Tab.css @@ -3,4 +3,5 @@ /* height: 100%; */ padding-top: 25px; padding-bottom: 25px; + border-radius: 0 0 20px 20px; } diff --git a/app/src/pages/Tab3.css b/app/src/pages/Tab3.css deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/pages/Tab3.tsx b/app/src/pages/Tab3.tsx index 73c3f49..e184d09 100644 --- a/app/src/pages/Tab3.tsx +++ b/app/src/pages/Tab3.tsx @@ -1,42 +1,55 @@ -import { IonContent, IonPage } from "@ionic/react"; -import { useAtom } from "jotai"; -import React from "react"; +import './Tab.css'; -import { currentTrackAtom } from "../atoms"; -import NowPlayingTab from "../components/NowPlayingTab"; -import Queue from "../components/Queue"; -import { mocks } from "../mocks"; +import { IonContent, IonPage } from '@ionic/react'; +import { useAtom } from 'jotai'; +import React from 'react'; + +import { currentTrackAtom } from '../atoms'; +import NowPlayingTab from '../components/NowPlayingTab'; +import Queue from '../components/Queue'; +import { useCoda } from '../hooks/useCoda'; +import { darkenAndSaturate } from '../utils'; -import "./Tab.css"; -import { darkenAndSaturate } from "../utils"; -import TestCoda from "../components/testCodaDbMongo"; const Tab: React.FC = () => { + const { records, loading, error, updateVoti } = useCoda(); const [current] = useAtom(currentTrackAtom); const now = Date.now(); + // if (loading) return
Loading...
; + // if (error) return
Error: {error}
; + // if (records.length === 0) return
No songs in queue
; + if (!current) return null; const darkerColor = darkenAndSaturate(current.color, 40, 100); + // Mappa i record nel formato richiesto da Queue + const songs = records.map((r) => ({ + id: r.id, + coverUrl: r.coverUrl, + title: r.titolo, + artist: r.artista, + upvotes: r.voti, + upvoted: false, + })); + return ( - +
- {current && ( - - )} +
- - + { + updateVoti(id, true); // Incrementa voti + }} + />
);