import React, { useState, useEffect } from 'react'; import { Text, Box, useStdout } from 'ink'; import fetch from 'node-fetch'; const locations = { Nancy: { latitude: 48.6921, longitude: 6.1844 }, Paris: { latitude: 48.8566, longitude: 2.3522 } }; 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'; 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' } }; function formatTime(date) { return date.toLocaleTimeString('fr-FR', { hour12: false }); } function formatDate(date) { return date.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } function formatDateTimeUTC(utcString) { const date = new Date(utcString); return { date: formatDate(date), time: formatTime(date) }; } 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; } 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": "1535f68086e542528841b5e276f50b45" } }); 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 []; } } function translateHomeAway(homeAway) { if (homeAway === 'Home') return 'domicile'; if (homeAway === 'Away') return 'extérieur'; return homeAway; } 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) ); } 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(); } const LoadingBar = ({ percentage }) => { const width = 30; const filledWidth = Math.round((percentage / 100) * width); const emptyWidth = width - filledWidth; return ( [ {'='.repeat(filledWidth)} {' '.repeat(emptyWidth)} ] {percentage}% ); }; 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'} ); } } 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}`; } } 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 []; } } 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} ); })} ); } 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); const [matchesInter, setMatchesInter] = useState(null); const [matchesParis, setMatchesParis] = useState(null); const [deviceInfo, setDeviceInfo] = useState(null); const [deviceError, setDeviceError] = useState(null); 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), ]); setWeatherNancy(nancy); setWeatherParis(paris); } catch (_) {} } // Mise à jour matchs programmés async function updateMatchesData() { try { const [interMatches, parisMatches] = await Promise.all([ fetchScheduledMatches(INTER_ID), fetchScheduledMatches(PARIS_ID), ]); setMatchesInter(interMatches); setMatchesParis(parisMatches); } catch (_) {} } // Mise à jour device info (implémenter fetchDeviceInfo) 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); } } // 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 ( = 80 ? "center" : "flex-start"}> Hello, {name} ! {formatDate(now)} {formatTime(now)} {/* Nouveaux tableaux classement */} Press Ctrl+C to exit. ); }