diff --git a/package-lock.json b/package-lock.json
index 38bc4c3..652e683 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"ink": "^4.1.0",
+ "ink-table": "^3.1.0",
"meow": "^11.0.0",
"node-fetch": "^3.3.2",
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-3.0.0.tgz",
@@ -6437,6 +6451,15 @@
"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": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
diff --git a/package.json b/package.json
index f3ed585..ee474e0 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
],
"dependencies": {
"ink": "^4.1.0",
+ "ink-table": "^3.1.0",
"meow": "^11.0.0",
"node-fetch": "^3.3.2",
"react": "^18.2.0"
diff --git a/source/app.js b/source/app.js
index c23378b..7d50dd6 100644
--- a/source/app.js
+++ b/source/app.js
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { Text, Box } from 'ink';
+import { Text, Box, useStdout } from 'ink';
import fetch from 'node-fetch';
const locations = {
@@ -10,8 +10,12 @@ const locations = {
const INTER_ID = 6684; // SC Internacional 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_TOKEN = 'eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoidGFza3Ryb3ZlIiwiaXNzIjoibWVtb3MiLCJzdWIiOiIzIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1OTk5MjI0N30.666jJ97j9a3d8c3a2a2a2a2a2a2a2a2a2a2a2a2a2a2'; // From your memo API
+const TASKTROVE_TOKEN = 'eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoidGFza3Ryb3ZlIiwiaXNzIjoibWVtb3MiLCJzdWIiOiIzIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1OTk5MjI0N30.666jJ97j9a3d8c3a2a2a2a2a2a2a2a2a2a2a2a2a2a2';
const weatherCodeMap = {
0: { desc: 'Ciel clair', color: 'yellow' },
@@ -197,7 +201,6 @@ async function fetchLatestMemo() {
}
}
-// New function to fetch and render TaskTrove tasks
async function fetchTaskTroveTasks() {
try {
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 (
+
+ {renderWeather('Nancy', weatherNancy)}
+ {renderWeather('Paris', weatherParis)}
+
+ );
+}
+
+function MatchesSection({ matches, teamColor, teamName, width }) {
+ return (
+
+ Matches à venir de {teamName} :
+ {!matches && Chargement des matchs...}
+ {matches && matches.length === 0 && Aucun match trouvé.}
+ {matches && matches.slice(0, 2).map((m, i) => (
+
+ {m.date} {m.time} - {teamName} vs {m.opponent} ({translateHomeAway(m.homeAway)})
+
+ ))}
+
+ );
+}
+
+function DeviceSection({ deviceInfo, deviceError, width }) {
+ return (
+
+ Informations du device :
+ {renderDeviceInfo(deviceInfo, deviceError)}
+
+ );
+}
+
+function MemoSection({ latestMemo, width, height }) {
+ const lines = latestMemo.split('\n');
+ const maxLines = Math.max(height - 28, 4);
+ return (
+
+ Liste de courses
+ {lines.slice(0, maxLines).map((line, index) => (
+ {line}
+ ))}
+
+ );
+}
+
+function TaskTroveSection({ tasks, width }) {
+ return (
+
+ To-Do List
+ {renderTaskTroveList(tasks)}
+
+ );
+}
+
+
+// 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 Aucun classement disponible.;
+ }
+
+ // 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 (
+
+
+ {pad("Pos", COL_POS)}
+ {pad("Club", COL_CLUB)}
+ {pad("J", COL_J)}
+ {pad("Pts", COL_PTS)}
+
+ {subsetStandings.length === 0 && (
+ Équipe non trouvée dans le classement.
+ )}
+ {subsetStandings.map((teamData, idx) => {
+ const isHighlighted = teamData.team.id === highlightTeamId;
+ const teamKey = teamData.team.id ?? `team-${start + idx}`;
+ return (
+
+ {pad(teamData.position, COL_POS)}
+ {pad(teamData.team.name, COL_CLUB)}
+ {pad(teamData.playedGames, COL_J)}
+ {pad(teamData.points, COL_PTS)}
+
+ );
+ })}
+
+ );
+}
+
+
+
+export default function App({ name = "Mathias" }) {
const [now, setNow] = useState(new Date());
const [weatherNancy, setWeatherNancy] = 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 [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(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
+ // Mise à jour météo (implémenter fetchWeather)
async function updateWeatherData() {
try {
const [nancy, paris] = await Promise.all([
fetchWeather(locations.Nancy.latitude, locations.Nancy.longitude),
- fetchWeather(locations.Paris.latitude, locations.Paris.longitude)
+ fetchWeather(locations.Paris.latitude, locations.Paris.longitude),
]);
setWeatherNancy(nancy);
setWeatherParis(paris);
- } catch (err) {}
+ } catch (_) {}
}
+ // Mise à jour matchs programmés
async function updateMatchesData() {
try {
const [interMatches, parisMatches] = await Promise.all([
fetchScheduledMatches(INTER_ID),
- fetchScheduledMatches(PARIS_ID)
+ fetchScheduledMatches(PARIS_ID),
]);
setMatchesInter(interMatches);
setMatchesParis(parisMatches);
- } catch (err) {}
+ } catch (_) {}
}
+ // Mise à jour device info (implémenter fetchDeviceInfo)
async function updateDeviceInfo() {
try {
const data = await fetchDeviceInfo();
setDeviceInfo(data);
setDeviceError(null);
- } catch (err) {
+ } catch (_) {
setDeviceError("Impossible de récupérer les informations du device");
setDeviceInfo(null);
}
}
+ // Mise à jour dernière note (implémenter fetchLatestMemo)
async function updateLatestMemo() {
const memo = await fetchLatestMemo();
setLatestMemo(memo);
}
+ // Mise à jour tâches TaskTrove (implémenter fetchTaskTroveTasks)
async function updateTaskTroveTasks() {
const tasks = await fetchTaskTroveTasks();
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(() => {
updateWeatherData();
updateMatchesData();
updateDeviceInfo();
updateLatestMemo();
updateTaskTroveTasks();
+ updateStandings();
const weatherInterval = setInterval(updateWeatherData, 60000);
+ const matchesInterval = setInterval(updateMatchesData, 60000);
const deviceInterval = setInterval(updateDeviceInfo, 60000);
const memoInterval = setInterval(updateLatestMemo, 60000);
const taskTroveInterval = setInterval(updateTaskTroveTasks, 60000);
+ const standingsInterval = setInterval(updateStandings, 60000);
return () => {
clearInterval(weatherInterval);
+ clearInterval(matchesInterval);
clearInterval(deviceInterval);
clearInterval(memoInterval);
clearInterval(taskTroveInterval);
+ clearInterval(standingsInterval);
};
}, []);
return (
-
- Hello, {name} !
- {formatDate(now)}
- {formatTime(now)}
-
- {renderWeather('Nancy', weatherNancy)}
- {renderWeather('Paris', weatherParis)}
-
-
- Matches à venir de SC Internacional :
- {!matchesInter && Chargement des matchs...}
- {matchesInter && matchesInter.length === 0 && Aucun match trouvé.}
- {matchesInter && matchesInter.slice(0, 2).map((m, i) => (
-
- {m.date} {m.time} - SC Internacional vs {m.opponent} ({translateHomeAway(m.homeAway)})
-
- ))}
+ = 80 ? "center" : "flex-start"}>
+
+ Hello, {name} !
+
+
+ {formatDate(now)}
+
+
+ {formatTime(now)}
-
- Matches à venir de Paris FC :
- {!matchesParis && Chargement des matchs...}
- {matchesParis && matchesParis.length === 0 && Aucun match trouvé.}
- {matchesParis && matchesParis.slice(0, 2).map((m, i) => (
-
- {m.date} {m.time} - Paris FC vs {m.opponent} ({translateHomeAway(m.homeAway)})
-
- ))}
-
+
-
- Informations du device :
- {renderDeviceInfo(deviceInfo, deviceError)}
-
+
-
- Liste de courses
- {latestMemo.split('\n').map((line, index) => (
- {line}
- ))}
-
+
-
- To-Do List
- {renderTaskTroveList(taskTroveTasks)}
-
+ {/* Nouveaux tableaux classement */}
+
+
- Press Ctrl+C to exit.
+
+
+
+
+
+
+
+ Press Ctrl+C to exit.
+
);
}
diff --git a/source/components/DeviceSection.js b/source/components/DeviceSection.js
new file mode 100644
index 0000000..d254bd8
--- /dev/null
+++ b/source/components/DeviceSection.js
@@ -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 {deviceError};
+ }
+ if (!deviceInfo) {
+ return Chargement des informations...;
+ }
+
+ const percentage = deviceInfo.percentage;
+ if (percentage == null) {
+ return Status : {deviceInfo.status || 'N/A'};
+ } else {
+ return (
+
+
+ Temps restant : {deviceInfo.remainingTime || 'N/A'}
+ Status : {deviceInfo.status || 'N/A'}
+
+ );
+ }
+}
+
+export function DeviceSection({ deviceInfo, deviceError, width }) {
+ return (
+
+ Informations du device :
+ {renderDeviceInfo(deviceInfo, deviceError)}
+
+ );
+}
diff --git a/source/components/LoadingBar.js b/source/components/LoadingBar.js
new file mode 100644
index 0000000..60a121d
--- /dev/null
+++ b/source/components/LoadingBar.js
@@ -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 (
+
+ [
+ {'='.repeat(filledWidth)}
+ {' '.repeat(emptyWidth)}
+ ]
+ {percentage}%
+
+ );
+};
diff --git a/source/components/MatchesSection.js b/source/components/MatchesSection.js
new file mode 100644
index 0000000..9c803dd
--- /dev/null
+++ b/source/components/MatchesSection.js
@@ -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 (
+
+ Matches à venir de {teamName} :
+ {!matches && Chargement des matchs...}
+ {matches && matches.length === 0 && Aucun match trouvé.}
+ {matches && matches.slice(0, 2).map((m, i) => (
+
+ {m.date} {m.time} - {teamName} vs {m.opponent} ({translateHomeAway(m.homeAway)})
+
+ ))}
+
+ );
+}
diff --git a/source/components/MemoSection.js b/source/components/MemoSection.js
new file mode 100644
index 0000000..1df9923
--- /dev/null
+++ b/source/components/MemoSection.js
@@ -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 (
+
+ Liste de courses
+ {lines.slice(0, maxLines).map((line, index) => (
+ {line}
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/source/components/TaskTroveSection.js b/source/components/TaskTroveSection.js
new file mode 100644
index 0000000..9f6acb0
--- /dev/null
+++ b/source/components/TaskTroveSection.js
@@ -0,0 +1,36 @@
+// components/TaskTroveSection.js
+
+import React from 'react';
+import { Box, Text } from 'ink';
+
+function renderTaskTroveList(tasks) {
+ if (!tasks) {
+ return Chargement des tâches...;
+ }
+
+ if (tasks.length === 0) {
+ return Aucune tâche à afficher.;
+ }
+
+ return (
+
+ {tasks.map((task, index) => {
+ const color = task.priority === 1 ? 'red' : task.priority === 2 ? 'yellow' : 'white';
+ return (
+
+ - {task.title}
+
+ );
+ })}
+
+ );
+}
+
+export function TaskTroveSection({ tasks, width }) {
+ return (
+
+ To-Do List
+ {renderTaskTroveList(tasks)}
+
+ );
+}
\ No newline at end of file
diff --git a/source/components/WeatherSection.js b/source/components/WeatherSection.js
new file mode 100644
index 0000000..3028ac1
--- /dev/null
+++ b/source/components/WeatherSection.js
@@ -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 {location}: Loading weather...;
+
+ const weatherInfo = weatherCodeMap[weather.weathercode] || { desc: 'Unknown', color: 'white' };
+
+ return (
+
+ {location}: {weatherInfo.desc},{' '}
+ {weather.temperature}°C (Vent : {weather.windspeed} km/h)
+
+ );
+}
+
+export function WeatherSection({ weatherNancy, weatherParis, width }) {
+ const flexDirection = width >= 60 ? "row" : "column";
+ return (
+
+ {renderWeather('Nancy', weatherNancy)}
+ {renderWeather('Paris', weatherParis)}
+
+ );
+}
diff --git a/source/config.js b/source/config.js
new file mode 100644
index 0000000..7cdc329
--- /dev/null
+++ b/source/config.js
@@ -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";
diff --git a/source/hooks/useDeviceInfo.js b/source/hooks/useDeviceInfo.js
new file mode 100644
index 0000000..e41267d
--- /dev/null
+++ b/source/hooks/useDeviceInfo.js
@@ -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 };
+}
diff --git a/source/hooks/useLatestMemo.js b/source/hooks/useLatestMemo.js
new file mode 100644
index 0000000..60ff288
--- /dev/null
+++ b/source/hooks/useLatestMemo.js
@@ -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 };
+}
diff --git a/source/hooks/useMatches.js b/source/hooks/useMatches.js
new file mode 100644
index 0000000..a0aa7e6
--- /dev/null
+++ b/source/hooks/useMatches.js
@@ -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 };
+}
diff --git a/source/hooks/useTaskTroveTasks.js b/source/hooks/useTaskTroveTasks.js
new file mode 100644
index 0000000..2071353
--- /dev/null
+++ b/source/hooks/useTaskTroveTasks.js
@@ -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 };
+}
diff --git a/source/hooks/useTerminalSize.js b/source/hooks/useTerminalSize.js
new file mode 100644
index 0000000..90f1bb9
--- /dev/null
+++ b/source/hooks/useTerminalSize.js
@@ -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;
+}
diff --git a/source/hooks/useWeather.js b/source/hooks/useWeather.js
new file mode 100644
index 0000000..44b48bc
--- /dev/null
+++ b/source/hooks/useWeather.js
@@ -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 };
+}
diff --git a/source/utils/dateUtils.js b/source/utils/dateUtils.js
new file mode 100644
index 0000000..1ce2762
--- /dev/null
+++ b/source/utils/dateUtils.js
@@ -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)
+ };
+}
diff --git a/source/utils/weatherCodes.js b/source/utils/weatherCodes.js
new file mode 100644
index 0000000..356740a
--- /dev/null
+++ b/source/utils/weatherCodes.js
@@ -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' }
+};