added rankings but ugly and tired
This commit is contained in:
23
package-lock.json
generated
23
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ink": "^4.1.0",
|
"ink": "^4.1.0",
|
||||||
|
"ink-table": "^3.1.0",
|
||||||
"meow": "^11.0.0",
|
"meow": "^11.0.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
@@ -4773,6 +4774,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ink-table": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ink-table/-/ink-table-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-qxVb4DIaEaJryvF9uZGydnmP9Hkmas3DCKVpEcBYC0E4eJd3qNgNe+PZKuzgCERFe9LfAS1TNWxCr9+AU4v3YA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-hash": "^2.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ink": ">=3.0.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ink-testing-library": {
|
"node_modules/ink-testing-library": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-3.0.0.tgz",
|
||||||
@@ -6437,6 +6451,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-hash": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ink": "^4.1.0",
|
"ink": "^4.1.0",
|
||||||
|
"ink-table": "^3.1.0",
|
||||||
"meow": "^11.0.0",
|
"meow": "^11.0.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
|
|||||||
265
source/app.js
265
source/app.js
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Text, Box } from 'ink';
|
import { Text, Box, useStdout } from 'ink';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
const locations = {
|
const locations = {
|
||||||
@@ -10,8 +10,12 @@ const locations = {
|
|||||||
const INTER_ID = 6684; // SC Internacional team id
|
const INTER_ID = 6684; // SC Internacional team id
|
||||||
const PARIS_ID = 1045; // Paris FC team id
|
const PARIS_ID = 1045; // Paris FC team id
|
||||||
|
|
||||||
|
const COMPETITION_FL1 = "FL1";
|
||||||
|
const COMPETITION_BSA = "BSA";
|
||||||
|
|
||||||
|
|
||||||
const TASKTROVE_API = 'https://tasktrove.couraud.xyz/api/v1/tasks';
|
const TASKTROVE_API = 'https://tasktrove.couraud.xyz/api/v1/tasks';
|
||||||
const TASKTROVE_TOKEN = 'eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoidGFza3Ryb3ZlIiwiaXNzIjoibWVtb3MiLCJzdWIiOiIzIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1OTk5MjI0N30.666jJ97j9a3d8c3a2a2a2a2a2a2a2a2a2a2a2a2a2a2'; // From your memo API
|
const TASKTROVE_TOKEN = 'eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoidGFza3Ryb3ZlIiwiaXNzIjoibWVtb3MiLCJzdWIiOiIzIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1OTk5MjI0N30.666jJ97j9a3d8c3a2a2a2a2a2a2a2a2a2a2a2a2a2a2';
|
||||||
|
|
||||||
const weatherCodeMap = {
|
const weatherCodeMap = {
|
||||||
0: { desc: 'Ciel clair', color: 'yellow' },
|
0: { desc: 'Ciel clair', color: 'yellow' },
|
||||||
@@ -197,7 +201,6 @@ async function fetchLatestMemo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New function to fetch and render TaskTrove tasks
|
|
||||||
async function fetchTaskTroveTasks() {
|
async function fetchTaskTroveTasks() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(TASKTROVE_API, {
|
const response = await fetch(TASKTROVE_API, {
|
||||||
@@ -254,7 +257,152 @@ function renderTaskTroveList(tasks) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App({ name = 'Mathias' }) {
|
function useTerminalSize() {
|
||||||
|
const { stdout } = useStdout();
|
||||||
|
const [size, setSize] = useState({ width: stdout.columns, height: stdout.rows });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stdout) return;
|
||||||
|
function onResize() {
|
||||||
|
setSize({ width: stdout.columns, height: stdout.rows });
|
||||||
|
}
|
||||||
|
stdout.on('resize', onResize);
|
||||||
|
return () => {
|
||||||
|
stdout.off('resize', onResize);
|
||||||
|
};
|
||||||
|
}, [stdout]);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WeatherSection({ weatherNancy, weatherParis, width }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" justifyContent="flex-start" width={width}>
|
||||||
|
{renderWeather('Nancy', weatherNancy)}
|
||||||
|
{renderWeather('Paris', weatherParis)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchesSection({ matches, teamColor, teamName, width }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color={teamColor}>Matches à venir de <Text color={teamColor}>{teamName}</Text> :</Text>
|
||||||
|
{!matches && <Text>Chargement des matchs...</Text>}
|
||||||
|
{matches && matches.length === 0 && <Text>Aucun match trouvé.</Text>}
|
||||||
|
{matches && matches.slice(0, 2).map((m, i) => (
|
||||||
|
<Text key={i} wrap="truncate-end">
|
||||||
|
{m.date} {m.time} - <Text color={teamColor}>{teamName}</Text> vs {m.opponent} ({translateHomeAway(m.homeAway)})
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeviceSection({ deviceInfo, deviceError, width }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color="green">Informations du device :</Text>
|
||||||
|
{renderDeviceInfo(deviceInfo, deviceError)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MemoSection({ latestMemo, width, height }) {
|
||||||
|
const lines = latestMemo.split('\n');
|
||||||
|
const maxLines = Math.max(height - 28, 4);
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color="magenta">Liste de courses</Text>
|
||||||
|
{lines.slice(0, maxLines).map((line, index) => (
|
||||||
|
<Text key={index} wrap="truncate-end">{line}</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskTroveSection({ tasks, width }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color="cyan">To-Do List</Text>
|
||||||
|
{renderTaskTroveList(tasks)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Fonction utilitaire pour fetcher le classement d'une compétition donnée
|
||||||
|
async function fetchLeagueStandings(competitionCode) {
|
||||||
|
const url = `https://api.football-data.org/v4/competitions/${competitionCode}/standings`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { "X-Auth-Token": "1535f68086e542528841b5e276f50b45" },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
// Retourne uniquement le tableau des équipes pour la saison en cours
|
||||||
|
const standings = data.standings?.find(s => s.type === "TOTAL")?.table || [];
|
||||||
|
return standings;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur fetchLeagueStandings:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function LeagueTable({ standings, highlightTeamId, highlightColor }) {
|
||||||
|
const COL_POS = 5, COL_CLUB = 30, COL_J = 5, COL_PTS = 5;
|
||||||
|
const pad = (str, len) => String(str).padEnd(len, " ");
|
||||||
|
|
||||||
|
if (!standings || standings.length === 0) {
|
||||||
|
return <Text>Aucun classement disponible.</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the index of the highlighted team
|
||||||
|
const highlightIndex = standings.findIndex(teamData => teamData.team.id === highlightTeamId);
|
||||||
|
|
||||||
|
// Determine range for slicing: 2 before and 2 after highlighted team with bounds check
|
||||||
|
const start = Math.max(0, highlightIndex - 2);
|
||||||
|
const end = Math.min(standings.length, highlightIndex + 3); // +3 to include 2 after highlighted team
|
||||||
|
|
||||||
|
// Extract subset of teams to show
|
||||||
|
const subsetStandings = (highlightIndex === -1) ? [] : standings.slice(start, end);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} borderStyle="round" borderColor={highlightColor} padding={1}>
|
||||||
|
<Text color="cyan" bold>
|
||||||
|
{pad("Pos", COL_POS)}
|
||||||
|
{pad("Club", COL_CLUB)}
|
||||||
|
{pad("J", COL_J)}
|
||||||
|
{pad("Pts", COL_PTS)}
|
||||||
|
</Text>
|
||||||
|
{subsetStandings.length === 0 && (
|
||||||
|
<Text color="yellow">Équipe non trouvée dans le classement.</Text>
|
||||||
|
)}
|
||||||
|
{subsetStandings.map((teamData, idx) => {
|
||||||
|
const isHighlighted = teamData.team.id === highlightTeamId;
|
||||||
|
const teamKey = teamData.team.id ?? `team-${start + idx}`;
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={teamKey}
|
||||||
|
color={isHighlighted ? "white" : undefined}
|
||||||
|
backgroundColor={isHighlighted ? highlightColor : undefined}
|
||||||
|
>
|
||||||
|
{pad(teamData.position, COL_POS)}
|
||||||
|
{pad(teamData.team.name, COL_CLUB)}
|
||||||
|
{pad(teamData.playedGames, COL_J)}
|
||||||
|
{pad(teamData.points, COL_PTS)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function App({ name = "Mathias" }) {
|
||||||
const [now, setNow] = useState(new Date());
|
const [now, setNow] = useState(new Date());
|
||||||
const [weatherNancy, setWeatherNancy] = useState(null);
|
const [weatherNancy, setWeatherNancy] = useState(null);
|
||||||
const [weatherParis, setWeatherParis] = useState(null);
|
const [weatherParis, setWeatherParis] = useState(null);
|
||||||
@@ -265,123 +413,138 @@ export default function App({ name = 'Mathias' }) {
|
|||||||
const [latestMemo, setLatestMemo] = useState("Chargement de la dernière note...");
|
const [latestMemo, setLatestMemo] = useState("Chargement de la dernière note...");
|
||||||
const [taskTroveTasks, setTaskTroveTasks] = useState(null);
|
const [taskTroveTasks, setTaskTroveTasks] = useState(null);
|
||||||
|
|
||||||
|
// Nouveaux états pour classements
|
||||||
|
const [standingsInter, setStandingsInter] = useState([]);
|
||||||
|
const [standingsParis, setStandingsParis] = useState([]);
|
||||||
|
|
||||||
|
const { width, height } = useTerminalSize();
|
||||||
|
|
||||||
|
// Timer pour date/heure live
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => setNow(new Date()), 1000);
|
const timer = setInterval(() => setNow(new Date()), 1000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Mise à jour météo (implémenter fetchWeather)
|
||||||
async function updateWeatherData() {
|
async function updateWeatherData() {
|
||||||
try {
|
try {
|
||||||
const [nancy, paris] = await Promise.all([
|
const [nancy, paris] = await Promise.all([
|
||||||
fetchWeather(locations.Nancy.latitude, locations.Nancy.longitude),
|
fetchWeather(locations.Nancy.latitude, locations.Nancy.longitude),
|
||||||
fetchWeather(locations.Paris.latitude, locations.Paris.longitude)
|
fetchWeather(locations.Paris.latitude, locations.Paris.longitude),
|
||||||
]);
|
]);
|
||||||
setWeatherNancy(nancy);
|
setWeatherNancy(nancy);
|
||||||
setWeatherParis(paris);
|
setWeatherParis(paris);
|
||||||
} catch (err) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mise à jour matchs programmés
|
||||||
async function updateMatchesData() {
|
async function updateMatchesData() {
|
||||||
try {
|
try {
|
||||||
const [interMatches, parisMatches] = await Promise.all([
|
const [interMatches, parisMatches] = await Promise.all([
|
||||||
fetchScheduledMatches(INTER_ID),
|
fetchScheduledMatches(INTER_ID),
|
||||||
fetchScheduledMatches(PARIS_ID)
|
fetchScheduledMatches(PARIS_ID),
|
||||||
]);
|
]);
|
||||||
setMatchesInter(interMatches);
|
setMatchesInter(interMatches);
|
||||||
setMatchesParis(parisMatches);
|
setMatchesParis(parisMatches);
|
||||||
} catch (err) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mise à jour device info (implémenter fetchDeviceInfo)
|
||||||
async function updateDeviceInfo() {
|
async function updateDeviceInfo() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchDeviceInfo();
|
const data = await fetchDeviceInfo();
|
||||||
setDeviceInfo(data);
|
setDeviceInfo(data);
|
||||||
setDeviceError(null);
|
setDeviceError(null);
|
||||||
} catch (err) {
|
} catch (_) {
|
||||||
setDeviceError("Impossible de récupérer les informations du device");
|
setDeviceError("Impossible de récupérer les informations du device");
|
||||||
setDeviceInfo(null);
|
setDeviceInfo(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mise à jour dernière note (implémenter fetchLatestMemo)
|
||||||
async function updateLatestMemo() {
|
async function updateLatestMemo() {
|
||||||
const memo = await fetchLatestMemo();
|
const memo = await fetchLatestMemo();
|
||||||
setLatestMemo(memo);
|
setLatestMemo(memo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mise à jour tâches TaskTrove (implémenter fetchTaskTroveTasks)
|
||||||
async function updateTaskTroveTasks() {
|
async function updateTaskTroveTasks() {
|
||||||
const tasks = await fetchTaskTroveTasks();
|
const tasks = await fetchTaskTroveTasks();
|
||||||
setTaskTroveTasks(tasks);
|
setTaskTroveTasks(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mise à jour classements
|
||||||
|
async function updateStandings() {
|
||||||
|
try {
|
||||||
|
const [interStandings, parisStandings] = await Promise.all([
|
||||||
|
fetchLeagueStandings(COMPETITION_BSA),
|
||||||
|
fetchLeagueStandings(COMPETITION_FL1),
|
||||||
|
]);
|
||||||
|
setStandingsInter(interStandings);
|
||||||
|
setStandingsParis(parisStandings);
|
||||||
|
} catch (_) {
|
||||||
|
setStandingsInter([]);
|
||||||
|
setStandingsParis([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisation + intervalle rafraîchissement
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateWeatherData();
|
updateWeatherData();
|
||||||
updateMatchesData();
|
updateMatchesData();
|
||||||
updateDeviceInfo();
|
updateDeviceInfo();
|
||||||
updateLatestMemo();
|
updateLatestMemo();
|
||||||
updateTaskTroveTasks();
|
updateTaskTroveTasks();
|
||||||
|
updateStandings();
|
||||||
|
|
||||||
const weatherInterval = setInterval(updateWeatherData, 60000);
|
const weatherInterval = setInterval(updateWeatherData, 60000);
|
||||||
|
const matchesInterval = setInterval(updateMatchesData, 60000);
|
||||||
const deviceInterval = setInterval(updateDeviceInfo, 60000);
|
const deviceInterval = setInterval(updateDeviceInfo, 60000);
|
||||||
const memoInterval = setInterval(updateLatestMemo, 60000);
|
const memoInterval = setInterval(updateLatestMemo, 60000);
|
||||||
const taskTroveInterval = setInterval(updateTaskTroveTasks, 60000);
|
const taskTroveInterval = setInterval(updateTaskTroveTasks, 60000);
|
||||||
|
const standingsInterval = setInterval(updateStandings, 60000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(weatherInterval);
|
clearInterval(weatherInterval);
|
||||||
|
clearInterval(matchesInterval);
|
||||||
clearInterval(deviceInterval);
|
clearInterval(deviceInterval);
|
||||||
clearInterval(memoInterval);
|
clearInterval(memoInterval);
|
||||||
clearInterval(taskTroveInterval);
|
clearInterval(taskTroveInterval);
|
||||||
|
clearInterval(standingsInterval);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" padding={1}>
|
<Box flexDirection="column" padding={1} height={height} width={width} justifyContent="flex-start" alignItems={width >= 80 ? "center" : "flex-start"}>
|
||||||
<Text>Hello, <Text color="green">{name}</Text> !</Text>
|
<Box justifyContent="center" width={width}>
|
||||||
<Text color="yellow">{formatDate(now)}</Text>
|
<Text>Hello, <Text color="green">{name}</Text> !</Text>
|
||||||
<Text color="magenta">{formatTime(now)}</Text>
|
</Box>
|
||||||
|
<Box justifyContent="center" width={width}>
|
||||||
{renderWeather('Nancy', weatherNancy)}
|
<Text color="yellow">{formatDate(now)}</Text>
|
||||||
{renderWeather('Paris', weatherParis)}
|
</Box>
|
||||||
|
<Box justifyContent="center" width={width}>
|
||||||
<Box marginTop={1} flexDirection="column">
|
<Text color="magenta">{formatTime(now)}</Text>
|
||||||
<Text color="red">Matches à venir de <Text color="red">SC Internacional</Text> :</Text>
|
|
||||||
{!matchesInter && <Text>Chargement des matchs...</Text>}
|
|
||||||
{matchesInter && matchesInter.length === 0 && <Text>Aucun match trouvé.</Text>}
|
|
||||||
{matchesInter && matchesInter.slice(0, 2).map((m, i) => (
|
|
||||||
<Text key={i}>
|
|
||||||
{m.date} {m.time} - <Text color="red">SC Internacional</Text> vs {m.opponent} ({translateHomeAway(m.homeAway)})
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
<WeatherSection weatherNancy={weatherNancy} weatherParis={weatherParis} width={width} />
|
||||||
<Text color="blue">Matches à venir de <Text color="blue">Paris FC</Text> :</Text>
|
|
||||||
{!matchesParis && <Text>Chargement des matchs...</Text>}
|
|
||||||
{matchesParis && matchesParis.length === 0 && <Text>Aucun match trouvé.</Text>}
|
|
||||||
{matchesParis && matchesParis.slice(0, 2).map((m, i) => (
|
|
||||||
<Text key={i}>
|
|
||||||
{m.date} {m.time} - <Text color="blue">Paris FC</Text> vs {m.opponent} ({translateHomeAway(m.homeAway)})
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
<MatchesSection matches={matchesInter} teamColor="red" teamName="SC Internacional" width={width} />
|
||||||
<Text color="green">Informations du device :</Text>
|
|
||||||
{renderDeviceInfo(deviceInfo, deviceError)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
<MatchesSection matches={matchesParis} teamColor="blue" teamName="Paris FC" width={width} />
|
||||||
<Text color="magenta">Liste de courses</Text>
|
|
||||||
{latestMemo.split('\n').map((line, index) => (
|
|
||||||
<Text key={index}>{line}</Text>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
{/* Nouveaux tableaux classement */}
|
||||||
<Text color="cyan">To-Do List</Text>
|
<LeagueTable standings={standingsInter} highlightTeamId={INTER_ID} highlightColor="red" />
|
||||||
{renderTaskTroveList(taskTroveTasks)}
|
<LeagueTable standings={standingsParis} highlightTeamId={PARIS_ID} highlightColor="blue" />
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Text dimColor>Press Ctrl+C to exit.</Text>
|
<DeviceSection deviceInfo={deviceInfo} deviceError={deviceError} width={width} />
|
||||||
|
|
||||||
|
<MemoSection latestMemo={latestMemo} width={width} height={height} />
|
||||||
|
|
||||||
|
<TaskTroveSection tasks={taskTroveTasks} width={width} />
|
||||||
|
|
||||||
|
<Box justifyContent="center" width={width}>
|
||||||
|
<Text dimColor>Press Ctrl+C to exit.</Text>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
source/components/DeviceSection.js
Normal file
36
source/components/DeviceSection.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// components/DeviceSection.js
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { LoadingBar } from './LoadingBar.js';
|
||||||
|
|
||||||
|
function renderDeviceInfo(deviceInfo, deviceError) {
|
||||||
|
if (deviceError) {
|
||||||
|
return <Text color="red">{deviceError}</Text>;
|
||||||
|
}
|
||||||
|
if (!deviceInfo) {
|
||||||
|
return <Text>Chargement des informations...</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentage = deviceInfo.percentage;
|
||||||
|
if (percentage == null) {
|
||||||
|
return <Text>Status : {deviceInfo.status || 'N/A'}</Text>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1}>
|
||||||
|
<LoadingBar percentage={percentage} />
|
||||||
|
<Text>Temps restant : {deviceInfo.remainingTime || 'N/A'}</Text>
|
||||||
|
<Text>Status : {deviceInfo.status || 'N/A'}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceSection({ deviceInfo, deviceError, width }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color="green">Informations du device :</Text>
|
||||||
|
{renderDeviceInfo(deviceInfo, deviceError)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
source/components/LoadingBar.js
Normal file
20
source/components/LoadingBar.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// components/LoadingBar.js
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
|
||||||
|
export const LoadingBar = ({ percentage }) => {
|
||||||
|
const width = 30;
|
||||||
|
const filledWidth = Math.round((percentage / 100) * width);
|
||||||
|
const emptyWidth = width - filledWidth;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text color="green">[</Text>
|
||||||
|
<Text color="green">{'='.repeat(filledWidth)}</Text>
|
||||||
|
<Text>{' '.repeat(emptyWidth)}</Text>
|
||||||
|
<Text color="green">]</Text>
|
||||||
|
<Text> {percentage}%</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
25
source/components/MatchesSection.js
Normal file
25
source/components/MatchesSection.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// components/MatchesSection.js
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
|
||||||
|
function translateHomeAway(homeAway) {
|
||||||
|
if (homeAway === 'Home') return 'domicile';
|
||||||
|
if (homeAway === 'Away') return 'extérieur';
|
||||||
|
return homeAway;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MatchesSection({ matches, teamColor, teamName, width }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color={teamColor}>Matches à venir de <Text color={teamColor}>{teamName}</Text> :</Text>
|
||||||
|
{!matches && <Text>Chargement des matchs...</Text>}
|
||||||
|
{matches && matches.length === 0 && <Text>Aucun match trouvé.</Text>}
|
||||||
|
{matches && matches.slice(0, 2).map((m, i) => (
|
||||||
|
<Text key={i} wrap="truncate-end">
|
||||||
|
{m.date} {m.time} - <Text color={teamColor}>{teamName}</Text> vs {m.opponent} ({translateHomeAway(m.homeAway)})
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
source/components/MemoSection.js
Normal file
15
source/components/MemoSection.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
|
||||||
|
export function MemoSection({ latestMemo, width, height }) {
|
||||||
|
const lines = latestMemo.split('\n');
|
||||||
|
const maxLines = Math.max(height - 28, 4);
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color="magenta">Liste de courses</Text>
|
||||||
|
{lines.slice(0, maxLines).map((line, index) => (
|
||||||
|
<Text key={index} wrap="truncate-end">{line}</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
source/components/TaskTroveSection.js
Normal file
36
source/components/TaskTroveSection.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// components/TaskTroveSection.js
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
|
||||||
|
function renderTaskTroveList(tasks) {
|
||||||
|
if (!tasks) {
|
||||||
|
return <Text>Chargement des tâches...</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
return <Text>Aucune tâche à afficher.</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{tasks.map((task, index) => {
|
||||||
|
const color = task.priority === 1 ? 'red' : task.priority === 2 ? 'yellow' : 'white';
|
||||||
|
return (
|
||||||
|
<Text key={index} color={color}>
|
||||||
|
- {task.title}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskTroveSection({ tasks, width }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginTop={1} width={width}>
|
||||||
|
<Text color="cyan">To-Do List</Text>
|
||||||
|
{renderTaskTroveList(tasks)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
source/components/WeatherSection.js
Normal file
28
source/components/WeatherSection.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// components/WeatherSection.js
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { weatherCodeMap } from '../utils/weatherCodes.js';
|
||||||
|
|
||||||
|
function renderWeather(location, weather) {
|
||||||
|
if (!weather) return <Text>{location}: Loading weather...</Text>;
|
||||||
|
|
||||||
|
const weatherInfo = weatherCodeMap[weather.weathercode] || { desc: 'Unknown', color: 'white' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{location}: <Text color={weatherInfo.color}>{weatherInfo.desc}</Text>,{' '}
|
||||||
|
<Text color="cyan">{weather.temperature}°C</Text> (Vent : {weather.windspeed} km/h)
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeatherSection({ weatherNancy, weatherParis, width }) {
|
||||||
|
const flexDirection = width >= 60 ? "row" : "column";
|
||||||
|
return (
|
||||||
|
<Box flexDirection={flexDirection} justifyContent="space-between" width={width}>
|
||||||
|
{renderWeather('Nancy', weatherNancy)}
|
||||||
|
{renderWeather('Paris', weatherParis)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
source/config.js
Normal file
12
source/config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const locations = {
|
||||||
|
Nancy: { latitude: 48.6921, longitude: 6.1844 },
|
||||||
|
Paris: { latitude: 48.8566, longitude: 2.3522 }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INTER_ID = 6684; // SC Internacional team id
|
||||||
|
export const PARIS_ID = 1045; // Paris FC team id
|
||||||
|
|
||||||
|
export const TASKTROVE_API = 'https://tasktrove.couraud.xyz/api/v1/tasks';
|
||||||
|
export const TASKTROVE_TOKEN = 'eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoidGFza3Ryb3ZlIiwiaXNzIjoibWVtb3MiLCJzdWIiOiIzIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1OTk5MjI0N30.666jJ97j9a3d8c3a2a2a2a2a2a2a2a2a2a2a2a2a2a2';
|
||||||
|
|
||||||
|
export const FOOTBALL_API_TOKEN = "1535f68086e542528841b5e276f50b45";
|
||||||
34
source/hooks/useDeviceInfo.js
Normal file
34
source/hooks/useDeviceInfo.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// hooks/useDeviceInfo.js
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
async function fetchDeviceInfo() {
|
||||||
|
const url = 'http://192.168.0.19:19837/device-info';
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error('Erreur HTTP ' + response.status);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeviceInfo() {
|
||||||
|
const [deviceInfo, setDeviceInfo] = useState(null);
|
||||||
|
const [deviceError, setDeviceError] = useState(null);
|
||||||
|
|
||||||
|
async function updateDeviceInfo() {
|
||||||
|
try {
|
||||||
|
const data = await fetchDeviceInfo();
|
||||||
|
setDeviceInfo(data);
|
||||||
|
setDeviceError(null);
|
||||||
|
} catch (_) {
|
||||||
|
setDeviceError("Impossible de récupérer les informations du device");
|
||||||
|
setDeviceInfo(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateDeviceInfo();
|
||||||
|
const interval = setInterval(updateDeviceInfo, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { deviceInfo, deviceError };
|
||||||
|
}
|
||||||
54
source/hooks/useLatestMemo.js
Normal file
54
source/hooks/useLatestMemo.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// hooks/useLatestMemo.js
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
function formatMemoContent(markdownContent) {
|
||||||
|
if (!markdownContent) return "";
|
||||||
|
return markdownContent.split('\n').map(line => {
|
||||||
|
const taskItemMatch = line.match(/^-\s*\[( |x|X)\]\s*/);
|
||||||
|
if (taskItemMatch) {
|
||||||
|
return '- ' + line.slice(taskItemMatch[0].length);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLatestMemo() {
|
||||||
|
const url = "https://memos.couraud.xyz/api/v1/memos?page=1&perPage=1";
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiZ3JvY2VyaWVzIiwiaXNzIjoibWVtb3MiLCJzdWIiOiIyIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc2MDI3NzQwMX0.H8m6LSaav7cuiQgt_rrzB7Fx4UM7Un11M2S0L5JJfPc"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
const memosArray = json.memos;
|
||||||
|
if (memosArray && memosArray.length > 0) {
|
||||||
|
return formatMemoContent(memosArray[0].content);
|
||||||
|
}
|
||||||
|
return "Aucune note trouvée.";
|
||||||
|
} catch (error) {
|
||||||
|
return `Erreur lors de la récupération de la note: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLatestMemo() {
|
||||||
|
const [latestMemo, setLatestMemo] = useState("Chargement de la dernière note...");
|
||||||
|
|
||||||
|
async function updateMemo() {
|
||||||
|
const memo = await fetchLatestMemo();
|
||||||
|
setLatestMemo(memo);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateMemo();
|
||||||
|
const interval = setInterval(updateMemo, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { latestMemo };
|
||||||
|
}
|
||||||
58
source/hooks/useMatches.js
Normal file
58
source/hooks/useMatches.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// hooks/useMatches.js
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatDateTimeUTC } from '../utils/dateUtils.js';
|
||||||
|
import { FOOTBALL_API_TOKEN } from '../config.js';
|
||||||
|
|
||||||
|
async function fetchScheduledMatches(teamId) {
|
||||||
|
const url = `https://api.football-data.org/v4/teams/${teamId}/matches?status=SCHEDULED`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { "X-Auth-Token": FOOTBALL_API_TOKEN }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.matches) return [];
|
||||||
|
return data.matches.map(match => {
|
||||||
|
let opponent, homeAway;
|
||||||
|
if (match.homeTeam.id === teamId) {
|
||||||
|
opponent = match.awayTeam.name;
|
||||||
|
homeAway = 'Home';
|
||||||
|
} else if (match.awayTeam.id === teamId) {
|
||||||
|
opponent = match.homeTeam.name;
|
||||||
|
homeAway = 'Away';
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { date, time } = formatDateTimeUTC(match.utcDate);
|
||||||
|
return { date, time, opponent, homeAway };
|
||||||
|
}).filter(Boolean);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMatches(teamIds) {
|
||||||
|
// teamIds is an object like { inter: 6684, paris: 1045 }
|
||||||
|
const [matchesInter, setMatchesInter] = useState(null);
|
||||||
|
const [matchesParis, setMatchesParis] = useState(null);
|
||||||
|
|
||||||
|
async function updateMatchesData() {
|
||||||
|
try {
|
||||||
|
const [interMatches, parisMatches] = await Promise.all([
|
||||||
|
fetchScheduledMatches(teamIds.inter),
|
||||||
|
fetchScheduledMatches(teamIds.paris)
|
||||||
|
]);
|
||||||
|
setMatchesInter(interMatches);
|
||||||
|
setMatchesParis(parisMatches);
|
||||||
|
} catch (_) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateMatchesData();
|
||||||
|
const interval = setInterval(updateMatchesData, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [teamIds]);
|
||||||
|
|
||||||
|
|
||||||
|
return { matchesInter, matchesParis };
|
||||||
|
}
|
||||||
54
source/hooks/useTaskTroveTasks.js
Normal file
54
source/hooks/useTaskTroveTasks.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// hooks/useTaskTroveTasks.js
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { TASKTROVE_API, TASKTROVE_TOKEN } from '../config.js';
|
||||||
|
|
||||||
|
async function fetchTaskTroveTasks() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(TASKTROVE_API, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TASKTROVE_TOKEN}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return [];
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const tasks = data.tasks || [];
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
return tasks
|
||||||
|
.filter(task => {
|
||||||
|
if (task.completed) return false;
|
||||||
|
const isDueToday = task.dueDate === today;
|
||||||
|
const isP1 = task.priority === 1;
|
||||||
|
const isP2 = task.priority === 2;
|
||||||
|
return isDueToday || isP1 || isP2;
|
||||||
|
})
|
||||||
|
.map(task => ({
|
||||||
|
title: task.title,
|
||||||
|
priority: task.priority,
|
||||||
|
dueDate: task.dueDate
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTaskTroveTasks() {
|
||||||
|
const [tasks, setTasks] = useState(null);
|
||||||
|
|
||||||
|
async function updateTasks() {
|
||||||
|
const tsks = await fetchTaskTroveTasks();
|
||||||
|
setTasks(tsks);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTasks();
|
||||||
|
const interval = setInterval(updateTasks, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { tasks };
|
||||||
|
}
|
||||||
22
source/hooks/useTerminalSize.js
Normal file
22
source/hooks/useTerminalSize.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// hooks/useTerminalSize.js
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useStdout } from 'ink';
|
||||||
|
|
||||||
|
export function useTerminalSize() {
|
||||||
|
const { stdout } = useStdout();
|
||||||
|
const [size, setSize] = useState({ width: stdout.columns, height: stdout.rows });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stdout) return;
|
||||||
|
function onResize() {
|
||||||
|
setSize({ width: stdout.columns, height: stdout.rows });
|
||||||
|
}
|
||||||
|
stdout.on('resize', onResize);
|
||||||
|
return () => {
|
||||||
|
stdout.off('resize', onResize);
|
||||||
|
};
|
||||||
|
}, [stdout]);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
35
source/hooks/useWeather.js
Normal file
35
source/hooks/useWeather.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// hooks/useWeather.js
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { locations } from '../config.js';
|
||||||
|
|
||||||
|
async function fetchWeather(lat, lon) {
|
||||||
|
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=celsius&timezone=Europe/Paris`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
return data.current_weather;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWeather() {
|
||||||
|
const [weatherNancy, setWeatherNancy] = useState(null);
|
||||||
|
const [weatherParis, setWeatherParis] = useState(null);
|
||||||
|
|
||||||
|
async function updateWeatherData() {
|
||||||
|
try {
|
||||||
|
const [nancy, paris] = await Promise.all([
|
||||||
|
fetchWeather(locations.Nancy.latitude, locations.Nancy.longitude),
|
||||||
|
fetchWeather(locations.Paris.latitude, locations.Paris.longitude)
|
||||||
|
]);
|
||||||
|
setWeatherNancy(nancy);
|
||||||
|
setWeatherParis(paris);
|
||||||
|
} catch (_) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateWeatherData();
|
||||||
|
const interval = setInterval(updateWeatherData, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { weatherNancy, weatherParis };
|
||||||
|
}
|
||||||
22
source/utils/dateUtils.js
Normal file
22
source/utils/dateUtils.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// utils/dateUtils.js
|
||||||
|
|
||||||
|
export function formatTime(date) {
|
||||||
|
return date.toLocaleTimeString('fr-FR', { hour12: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date) {
|
||||||
|
return date.toLocaleDateString('fr-FR', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTimeUTC(utcString) {
|
||||||
|
const date = new Date(utcString);
|
||||||
|
return {
|
||||||
|
date: formatDate(date),
|
||||||
|
time: formatTime(date)
|
||||||
|
};
|
||||||
|
}
|
||||||
32
source/utils/weatherCodes.js
Normal file
32
source/utils/weatherCodes.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// utils/weatherCodes.js
|
||||||
|
|
||||||
|
export const weatherCodeMap = {
|
||||||
|
0: { desc: 'Ciel clair', color: 'yellow' },
|
||||||
|
1: { desc: 'Principalement clair', color: 'yellow' },
|
||||||
|
2: { desc: 'Partiellement nuageux', color: 'cyan' },
|
||||||
|
3: { desc: 'Couvert', color: 'gray' },
|
||||||
|
45: { desc: 'Brouillard', color: 'gray' },
|
||||||
|
48: { desc: 'Brouillard givrant', color: 'gray' },
|
||||||
|
51: { desc: 'Bruine légère', color: 'blue' },
|
||||||
|
53: { desc: 'Bruine modérée', color: 'blue' },
|
||||||
|
55: { desc: 'Bruine dense', color: 'blue' },
|
||||||
|
56: { desc: 'Bruine verglaçante légère', color: 'blue' },
|
||||||
|
57: { desc: 'Bruine verglaçante dense', color: 'blue' },
|
||||||
|
61: { desc: 'Pluie faible', color: 'blue' },
|
||||||
|
63: { desc: 'Pluie modérée', color: 'blue' },
|
||||||
|
65: { desc: 'Pluie forte', color: 'blue' },
|
||||||
|
66: { desc: 'Pluie verglaçante légère', color: 'blue' },
|
||||||
|
67: { desc: 'Pluie verglaçante forte', color: 'blue' },
|
||||||
|
71: { desc: 'Chute de neige légère', color: 'white' },
|
||||||
|
73: { desc: 'Chute de neige modérée', color: 'white' },
|
||||||
|
75: { desc: 'Chute de neige forte', color: 'white' },
|
||||||
|
77: { desc: 'Grains de neige', color: 'white' },
|
||||||
|
80: { desc: 'Averses de pluie faibles', color: 'blue' },
|
||||||
|
81: { desc: 'Averses de pluie modérées', color: 'blue' },
|
||||||
|
82: { desc: 'Averses de pluie violentes', color: 'blue' },
|
||||||
|
85: { desc: 'Averses de neige faibles', color: 'white' },
|
||||||
|
86: { desc: 'Averses de neige fortes', color: 'white' },
|
||||||
|
95: { desc: 'Orage', color: 'magenta' },
|
||||||
|
96: { desc: 'Orage avec faible grêle', color: 'magenta' },
|
||||||
|
99: { desc: 'Orage avec forte grêle', color: 'magenta' }
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user