geoaccess app
This commit is contained in:
parent
02fa10c2d5
commit
51f507811c
18 changed files with 577 additions and 129 deletions
19
app/package-lock.json
generated
19
app/package-lock.json
generated
|
@ -19,10 +19,12 @@
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"ionicons": "^7.4.0",
|
"ionicons": "^7.4.0",
|
||||||
"jotai": "^2.12.5",
|
"jotai": "^2.12.5",
|
||||||
|
"lucide-react": "^0.536.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-router": "^5.3.4",
|
"react-router": "^5.3.4",
|
||||||
"react-router-dom": "^5.3.4"
|
"react-router-dom": "^5.3.4",
|
||||||
|
"tinycolor2": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "7.4.2",
|
"@capacitor/cli": "7.4.2",
|
||||||
|
@ -8484,6 +8486,15 @@
|
||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.536.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.536.0.tgz",
|
||||||
|
"integrity": "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
@ -10814,6 +10825,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinycolor2": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinypool": {
|
"node_modules/tinypool": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz",
|
||||||
|
|
|
@ -23,10 +23,12 @@
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"ionicons": "^7.4.0",
|
"ionicons": "^7.4.0",
|
||||||
"jotai": "^2.12.5",
|
"jotai": "^2.12.5",
|
||||||
|
"lucide-react": "^0.536.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-router": "^5.3.4",
|
"react-router": "^5.3.4",
|
||||||
"react-router-dom": "^5.3.4"
|
"react-router-dom": "^5.3.4",
|
||||||
|
"tinycolor2": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "7.4.2",
|
"@capacitor/cli": "7.4.2",
|
||||||
|
|
|
@ -1,30 +1,26 @@
|
||||||
import './App.css';
|
import "./App.css";
|
||||||
import './theme/variables.css';
|
import "./theme/variables.css";
|
||||||
import '@ionic/react/css/core.css';
|
import "@ionic/react/css/core.css";
|
||||||
import '@ionic/react/css/display.css';
|
import "@ionic/react/css/display.css";
|
||||||
import '@ionic/react/css/flex-utils.css';
|
import "@ionic/react/css/flex-utils.css";
|
||||||
import '@ionic/react/css/float-elements.css';
|
import "@ionic/react/css/float-elements.css";
|
||||||
import '@ionic/react/css/normalize.css';
|
import "@ionic/react/css/normalize.css";
|
||||||
import '@ionic/react/css/padding.css';
|
import "@ionic/react/css/padding.css";
|
||||||
import '@ionic/react/css/palettes/dark.system.css';
|
import "@ionic/react/css/palettes/dark.system.css";
|
||||||
import '@ionic/react/css/structure.css';
|
import "@ionic/react/css/structure.css";
|
||||||
import '@ionic/react/css/text-alignment.css';
|
import "@ionic/react/css/text-alignment.css";
|
||||||
import '@ionic/react/css/text-transformation.css';
|
import "@ionic/react/css/text-transformation.css";
|
||||||
import '@ionic/react/css/typography.css';
|
import "@ionic/react/css/typography.css";
|
||||||
|
|
||||||
import { IonApp, IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, setupIonicReact } from '@ionic/react';
|
import { IonApp, setupIonicReact } from "@ionic/react";
|
||||||
import { IonReactRouter } from '@ionic/react-router';
|
import { IonReactRouter } from "@ionic/react-router";
|
||||||
import { list, musicalNote, search } from 'ionicons/icons';
|
import { useAtom } from "jotai";
|
||||||
import { useAtom } from 'jotai';
|
import { useEffect, useState } from "react";
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { currentTrackAtom } from './atoms';
|
import { currentTrackAtom } from "./atoms";
|
||||||
import { mocks } from './mocks';
|
import { mocks } from "./mocks";
|
||||||
import Tab1 from './pages/Tab1';
|
import Tab from "./pages/Tab3";
|
||||||
import Tab2 from './pages/Tab2';
|
import LocationAccessChecker from "./hooks/GeoAccess";
|
||||||
import Tab3 from './pages/Tab3';
|
|
||||||
import Tab from './pages/Tab3';
|
|
||||||
|
|
||||||
/* Core CSS required for Ionic components to work properly */
|
/* Core CSS required for Ionic components to work properly */
|
||||||
/* Basic CSS for apps built with Ionic */
|
/* Basic CSS for apps built with Ionic */
|
||||||
|
@ -35,15 +31,21 @@ import Tab from './pages/Tab3';
|
||||||
setupIonicReact();
|
setupIonicReact();
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [current, setCurrent] = useAtom(currentTrackAtom)
|
const [current, setCurrent] = useAtom(currentTrackAtom);
|
||||||
|
const [accessGranted, setAccessGranted] = useState<boolean | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrent(mocks.nowPlaying)
|
setCurrent(mocks.nowPlaying);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
// if (accessGranted === false) {
|
||||||
|
// console.log("access denied.");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IonApp>
|
<IonApp>
|
||||||
{/* {current && <Background imageUrl={current.coverUrl} color={current.color}/>} */}
|
<LocationAccessChecker onAccessChecked={setAccessGranted} />
|
||||||
<IonReactRouter>
|
<IonReactRouter>
|
||||||
<Tab />
|
<Tab />
|
||||||
</IonReactRouter>
|
</IonReactRouter>
|
||||||
|
|
3
app/src/api_endpoints.ts
Normal file
3
app/src/api_endpoints.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const API_BASE_URL = "https://fbbb261497e3.ngrok-free.app";
|
||||||
|
export const GEO_ACCESS_API = `${API_BASE_URL}/geo-access/real`;
|
||||||
|
export const MONGO_DB_CODA_API = `${API_BASE_URL}/queue`
|
|
@ -1,5 +1,4 @@
|
||||||
.now-playing-cover {
|
.now-playing-cover {
|
||||||
margin-top: 25px;
|
|
||||||
margin-left: 25px;
|
margin-left: 25px;
|
||||||
width: calc(100% - 50px);
|
width: calc(100% - 50px);
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -15,7 +14,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.4));
|
/* background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.4)); */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +34,7 @@
|
||||||
.now-playing-artist {
|
.now-playing-artist {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-shy);
|
color: var(--text-supporting);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { IonContent } from "@ionic/react";
|
import './NowPlayingTab.css';
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import "./NowPlayingTab.css";
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export interface NowPlayingData {
|
export interface NowPlayingData {
|
||||||
coverUrl: string;
|
coverUrl: string;
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.queue {
|
h1.queue {
|
||||||
|
|
|
@ -1,13 +1,25 @@
|
||||||
import './Queue.css';
|
import "./Queue.css";
|
||||||
|
|
||||||
import { IonButton, IonContent, IonHeader, IonIcon, IonModal, IonSearchbar, IonTitle, IonToolbar } from '@ionic/react';
|
import {
|
||||||
import { add } from 'ionicons/icons';
|
IonButton,
|
||||||
import React, { useState } from 'react';
|
IonContent,
|
||||||
|
IonHeader,
|
||||||
|
IonModal,
|
||||||
|
IonSearchbar,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
} from "@ionic/react";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { ArrowBigUp, Plus } from "lucide-react";
|
||||||
|
|
||||||
interface NowPlayingDiscProps {
|
interface NowPlayingDiscProps {
|
||||||
coverUrl: string;
|
coverUrl: string;
|
||||||
title: string;
|
title: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
|
upvoted?: boolean;
|
||||||
|
upvotes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueProps {
|
interface QueueProps {
|
||||||
|
@ -18,20 +30,27 @@ const Queue: React.FC<QueueProps> = ({ songs }) => {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const handleUpVote = () => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="queue-container">
|
<div className="queue-container">
|
||||||
<div className="queue-header">
|
<div className="queue-header">
|
||||||
<h1 className="queue">Up next</h1>
|
<h1 className="queue">Up next</h1>
|
||||||
<IonButton
|
<Plus
|
||||||
fill="clear"
|
size={24}
|
||||||
onClick={() => setShowModal(true)}
|
color="white"
|
||||||
className="icon-button"
|
strokeWidth={2}
|
||||||
>
|
onClick={() => {
|
||||||
<IonIcon icon={add} />
|
setShowModal(true);
|
||||||
</IonButton>
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{songs.map((song, index) => (
|
{songs.map((song, index) => {
|
||||||
|
const isUpvoted = song.upvoted ?? false;
|
||||||
|
const votes = song.upvotes ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="song-item" key={index}>
|
<div className="song-item" key={index}>
|
||||||
<img
|
<img
|
||||||
className="cover"
|
className="cover"
|
||||||
|
@ -42,8 +61,33 @@ const Queue: React.FC<QueueProps> = ({ songs }) => {
|
||||||
<div className="title">{song.title}</div>
|
<div className="title">{song.title}</div>
|
||||||
<div className="artist">{song.artist}</div>
|
<div className="artist">{song.artist}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="upvote-container"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "6px",
|
||||||
|
marginLeft: "auto",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={handleUpVote}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{ fontWeight: "600", color: "#444", userSelect: "none" }}
|
||||||
|
>
|
||||||
|
{votes}
|
||||||
|
</span>
|
||||||
|
<ArrowBigUp
|
||||||
|
size={24}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
color={isUpvoted ? "#ff6600" : "#999"}
|
||||||
|
fill={isUpvoted ? "#ff6600" : "none"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<IonModal isOpen={showModal} onDidDismiss={() => setShowModal(false)}>
|
<IonModal isOpen={showModal} onDidDismiss={() => setShowModal(false)}>
|
||||||
<IonHeader>
|
<IonHeader>
|
||||||
|
|
119
app/src/components/testCodaDbMongo.tsx
Normal file
119
app/src/components/testCodaDbMongo.tsx
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
IonPage,
|
||||||
|
IonContent,
|
||||||
|
IonList,
|
||||||
|
IonItem,
|
||||||
|
IonLabel,
|
||||||
|
IonInput,
|
||||||
|
IonButton,
|
||||||
|
IonSpinner,
|
||||||
|
IonText,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { useCoda } from '../hooks/useCoda';
|
||||||
|
|
||||||
|
const TestCoda: React.FC = () => {
|
||||||
|
const {
|
||||||
|
records,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchRecords,
|
||||||
|
addRecord,
|
||||||
|
deleteRecord,
|
||||||
|
updateVoti,
|
||||||
|
} = useCoda();
|
||||||
|
|
||||||
|
// Stati per i nuovi record da aggiungere
|
||||||
|
const [title, setTitle] = 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('');
|
||||||
|
setArtista('');
|
||||||
|
setCoverUrl('');
|
||||||
|
setColor('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonContent className="ion-padding">
|
||||||
|
<h1>Test UseCoda</h1>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 10 }}>
|
||||||
|
<IonSpinner name="crescent" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<IonText color="danger">
|
||||||
|
<p>{error}</p>
|
||||||
|
</IonText>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IonButton expand="block" onClick={fetchRecords} disabled={loading}>
|
||||||
|
Ricarica Record
|
||||||
|
</IonButton>
|
||||||
|
|
||||||
|
<h2>Aggiungi Nuovo Record</h2>
|
||||||
|
<IonInput
|
||||||
|
placeholder="Title"
|
||||||
|
value={title}
|
||||||
|
onIonChange={(e) => setTitle(e.detail.value!)}
|
||||||
|
/>
|
||||||
|
<IonInput
|
||||||
|
placeholder="Artista"
|
||||||
|
value={artista}
|
||||||
|
onIonChange={(e) => setArtista(e.detail.value!)}
|
||||||
|
/>
|
||||||
|
<IonInput
|
||||||
|
placeholder="Cover URL"
|
||||||
|
value={coverUrl}
|
||||||
|
onIonChange={(e) => setCoverUrl(e.detail.value!)}
|
||||||
|
/>
|
||||||
|
<IonInput
|
||||||
|
placeholder="Color (es. #ff0000)"
|
||||||
|
value={color}
|
||||||
|
onIonChange={(e) => setColor(e.detail.value!)}
|
||||||
|
/>
|
||||||
|
<IonButton expand="block" onClick={handleAdd} disabled={loading}>
|
||||||
|
Aggiungi
|
||||||
|
</IonButton>
|
||||||
|
|
||||||
|
<h2>Lista Record</h2>
|
||||||
|
<IonList>
|
||||||
|
{records.length === 0 && !loading && <p>Nessun record disponibile</p>}
|
||||||
|
{records.map((r) => (
|
||||||
|
<IonItem key={r.id} style={{ borderLeft: `5px solid ${r.color}` }}>
|
||||||
|
<IonLabel>
|
||||||
|
<h3>{r.title}</h3>
|
||||||
|
<p>Artista: {r.artista}</p>
|
||||||
|
<img
|
||||||
|
src={r.coverUrl}
|
||||||
|
alt={r.title}
|
||||||
|
style={{ width: 50, height: 50, objectFit: 'cover', marginBottom: 5 }}
|
||||||
|
/>
|
||||||
|
<p>Voti: {r.voti}</p>
|
||||||
|
</IonLabel>
|
||||||
|
<IonButton size="small" onClick={() => updateVoti(r.id, 1)} disabled={loading}>
|
||||||
|
+1
|
||||||
|
</IonButton>
|
||||||
|
<IonButton size="small" onClick={() => updateVoti(r.id, -1)} disabled={loading}>
|
||||||
|
-1
|
||||||
|
</IonButton>
|
||||||
|
<IonButton color="danger" size="small" onClick={() => deleteRecord(r.id)} disabled={loading}>
|
||||||
|
Elimina
|
||||||
|
</IonButton>
|
||||||
|
</IonItem>
|
||||||
|
))}
|
||||||
|
</IonList>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestCoda;
|
135
app/src/hooks/GeoAccess.tsx
Normal file
135
app/src/hooks/GeoAccess.tsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
IonPage,
|
||||||
|
IonContent,
|
||||||
|
IonSpinner,
|
||||||
|
IonText,
|
||||||
|
IonButton,
|
||||||
|
} from "@ionic/react";
|
||||||
|
import { GEO_ACCESS_API } from "../api_endpoints";
|
||||||
|
|
||||||
|
const MAX_ACCEPTABLE_ACCURACY = 100; // metri
|
||||||
|
|
||||||
|
const LocationAccessChecker: React.FC<{
|
||||||
|
onAccessChecked?: (granted: boolean) => void;
|
||||||
|
}> = ({ onAccessChecked }) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [accessGranted, setAccessGranted] = useState<boolean | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [accuracy, setAccuracy] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkLocationAccess = async () => {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
setError("Geolocation non supportata dal browser");
|
||||||
|
setAccessGranted(false);
|
||||||
|
setLoading(false);
|
||||||
|
onAccessChecked?.(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
async (position) => {
|
||||||
|
const latitude = position.coords.latitude;
|
||||||
|
const longitude = position.coords.longitude;
|
||||||
|
const positionAccuracy = position.coords.accuracy;
|
||||||
|
|
||||||
|
console.log("Lat:", latitude);
|
||||||
|
console.log("Lng:", longitude);
|
||||||
|
console.log("Accuracy (m):", positionAccuracy);
|
||||||
|
setAccuracy(positionAccuracy);
|
||||||
|
|
||||||
|
if (positionAccuracy > MAX_ACCEPTABLE_ACCURACY) {
|
||||||
|
setError(
|
||||||
|
`Precisione troppo bassa: ${positionAccuracy.toFixed(1)} metri`
|
||||||
|
);
|
||||||
|
setAccessGranted(false);
|
||||||
|
setLoading(false);
|
||||||
|
onAccessChecked?.(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coordinates = {
|
||||||
|
coords: [latitude, longitude],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(GEO_ACCESS_API, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(coordinates),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Errore nella risposta dal server");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success === true) {
|
||||||
|
setAccessGranted(true);
|
||||||
|
onAccessChecked?.(true);
|
||||||
|
} else {
|
||||||
|
setAccessGranted(false);
|
||||||
|
onAccessChecked?.(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError("Errore di rete o server");
|
||||||
|
setAccessGranted(false);
|
||||||
|
onAccessChecked?.(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
setError(`Errore geolocalizzazione: ${err.message}`);
|
||||||
|
setAccessGranted(false);
|
||||||
|
setLoading(false);
|
||||||
|
onAccessChecked?.(false);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 10000,
|
||||||
|
maximumAge: 60000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkLocationAccess();
|
||||||
|
}, [onAccessChecked]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonContent className="ion-padding" fullscreen>
|
||||||
|
<IonSpinner name="crescent" />
|
||||||
|
<IonText>Verifica posizione in corso...</IonText>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessGranted === false) {
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonContent className="ion-padding" fullscreen>
|
||||||
|
<IonText color="danger">
|
||||||
|
<h2>Accesso non consentito</h2>
|
||||||
|
<p>La tua posizione non permette l’uso dell’app.</p>
|
||||||
|
{error && <p>Errore: {error}</p>}
|
||||||
|
{accuracy !== null && (
|
||||||
|
<p>Precisione posizione: {accuracy.toFixed(1)} metri</p>
|
||||||
|
)}
|
||||||
|
</IonText>
|
||||||
|
<IonButton onClick={() => window.location.reload()}>
|
||||||
|
Riprova
|
||||||
|
</IonButton>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // accesso consentito, niente da mostrare
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationAccessChecker;
|
113
app/src/hooks/useCoda.tsx
Normal file
113
app/src/hooks/useCoda.tsx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { MONGO_DB_CODA_API } from "../api_endpoints";
|
||||||
|
|
||||||
|
const API_URL = MONGO_DB_CODA_API;
|
||||||
|
|
||||||
|
export interface Record {
|
||||||
|
id: string;
|
||||||
|
titolo: string;
|
||||||
|
coverUrl: string;
|
||||||
|
artista: string;
|
||||||
|
color: string;
|
||||||
|
voti: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCoda = () => {
|
||||||
|
const [records, setRecords] = useState<Record[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRecords = async () => {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || "Errore fetch");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRecord = async (record: Omit<Record, "id">) => {
|
||||||
|
try {
|
||||||
|
const body = JSON.stringify(record);
|
||||||
|
const response = await fetchJson(`${API_URL}/add`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (response.success) {
|
||||||
|
setRecords((prev) => [...prev, response.data]);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
throw new Error("Errore aggiunta record");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || "Errore add");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || "Errore delete");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVoti = async (id: string, delta: 1 | -1) => {
|
||||||
|
try {
|
||||||
|
const response = await fetchJson(`${API_URL}/vote/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ increment: delta === 1 }),
|
||||||
|
});
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || "Errore update voti");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRecords();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
records,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchRecords,
|
||||||
|
addRecord,
|
||||||
|
deleteRecord,
|
||||||
|
updateVoti,
|
||||||
|
};
|
||||||
|
};
|
6
app/src/pages/Tab.css
Normal file
6
app/src/pages/Tab.css
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.gradient-background {
|
||||||
|
--background: none !important;
|
||||||
|
/* height: 100%; */
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
.tab-1-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import "./Tab1.css";
|
|
||||||
|
|
||||||
import { IonContent, IonPage } from "@ionic/react";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
|
|
||||||
import { currentTrackAtom } from "../atoms";
|
|
||||||
import NowPlayingDisc from "../components/NowPlayingDisc";
|
|
||||||
|
|
||||||
const Tab1: React.FC = () => {
|
|
||||||
const [current] = useAtom(currentTrackAtom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonPage>
|
|
||||||
<IonContent fullscreen className="transparent-content">
|
|
||||||
<div className="tab-1-container">
|
|
||||||
{current && (
|
|
||||||
<NowPlayingDisc
|
|
||||||
coverUrl={current.coverUrl}
|
|
||||||
title={current.title}
|
|
||||||
artist={current.artist}
|
|
||||||
color={current.color}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</IonContent>
|
|
||||||
</IonPage>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tab1;
|
|
|
@ -1,17 +0,0 @@
|
||||||
import Queue from '../components/Queue';
|
|
||||||
import { mocks } from '../mocks';
|
|
||||||
import './Tab2.css';
|
|
||||||
|
|
||||||
import { IonContent, IonPage } from '@ionic/react';
|
|
||||||
|
|
||||||
const Tab2: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<IonPage>
|
|
||||||
<IonContent fullscreen className="transparent-content">
|
|
||||||
<Queue songs={mocks.queue} />
|
|
||||||
</IonContent>
|
|
||||||
</IonPage>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tab2;
|
|
|
@ -1,27 +1,41 @@
|
||||||
import { IonContent, IonPage } from '@ionic/react';
|
import { IonContent, IonPage } from "@ionic/react";
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from "jotai";
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { currentTrackAtom } from '../atoms';
|
import { currentTrackAtom } from "../atoms";
|
||||||
import NowPlayingTab from '../components/NowPlayingTab';
|
import NowPlayingTab from "../components/NowPlayingTab";
|
||||||
import Queue from '../components/Queue';
|
import Queue from "../components/Queue";
|
||||||
import { mocks } from '../mocks';
|
import { mocks } from "../mocks";
|
||||||
|
|
||||||
|
import "./Tab.css";
|
||||||
|
import { darkenAndSaturate } from "../utils";
|
||||||
|
import TestCoda from "../components/testCodaDbMongo";
|
||||||
const Tab: React.FC = () => {
|
const Tab: React.FC = () => {
|
||||||
const [current] = useAtom(currentTrackAtom);
|
const [current] = useAtom(currentTrackAtom);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!current) return null;
|
||||||
|
|
||||||
|
const darkerColor = darkenAndSaturate(current.color, 40, 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IonPage>
|
<IonPage>
|
||||||
<IonContent fullscreen>
|
<IonContent fullscreen className="">
|
||||||
|
<div
|
||||||
|
className="gradient-background"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to bottom, ${current.color}, ${darkerColor})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{current && (
|
{current && (
|
||||||
<NowPlayingTab
|
<NowPlayingTab
|
||||||
data={current}
|
data={current}
|
||||||
startTime={now - 15000} // es. iniziata 15 sec fa
|
startTime={now - 15000}
|
||||||
duration={180000} // 3 minuti
|
duration={180000}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<TestCoda />
|
||||||
<Queue songs={mocks.queue} />
|
<Queue songs={mocks.queue} />
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
|
|
46
app/src/utils.tsx
Normal file
46
app/src/utils.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
export function darkenAndSaturate(color: string, darkenPercent = 20, saturatePercent = 20): string {
|
||||||
|
const ctx = document.createElement("canvas").getContext("2d");
|
||||||
|
if (!ctx) return color;
|
||||||
|
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
const rgb = ctx.fillStyle; // browser normalizza il colore
|
||||||
|
ctx.fillStyle = rgb;
|
||||||
|
const computed = ctx.fillStyle;
|
||||||
|
|
||||||
|
const temp = document.createElement("div");
|
||||||
|
temp.style.color = computed;
|
||||||
|
document.body.appendChild(temp);
|
||||||
|
const rgbStr = getComputedStyle(temp).color;
|
||||||
|
document.body.removeChild(temp);
|
||||||
|
|
||||||
|
const match = rgbStr.match(/rgba?\((\d+), (\d+), (\d+)/);
|
||||||
|
if (!match) return color;
|
||||||
|
|
||||||
|
let [r, g, b] = match.slice(1).map(Number);
|
||||||
|
|
||||||
|
// Convert to HSL
|
||||||
|
const rNorm = r / 255, gNorm = g / 255, bNorm = b / 255;
|
||||||
|
const max = Math.max(rNorm, gNorm, bNorm), min = Math.min(rNorm, gNorm, bNorm);
|
||||||
|
let h = 0, s = 0, l = (max + min) / 2;
|
||||||
|
|
||||||
|
if (max !== min) {
|
||||||
|
const d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch (max) {
|
||||||
|
case rNorm: h = (gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0); break;
|
||||||
|
case gNorm: h = (bNorm - rNorm) / d + 2; break;
|
||||||
|
case bNorm: h = (rNorm - gNorm) / d + 4; break;
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify saturation and lightness
|
||||||
|
s = Math.min(1, s + saturatePercent / 100);
|
||||||
|
l = Math.max(0, l - darkenPercent / 100);
|
||||||
|
|
||||||
|
const hDeg = Math.round(h * 360);
|
||||||
|
const sPerc = Math.round(s * 100);
|
||||||
|
const lPerc = Math.round(l * 100);
|
||||||
|
|
||||||
|
return `hsl(${hDeg}, ${sPerc}%, ${lPerc}%)`;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue