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' } +};