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.
);
}