Merge pull request #118 from R0m1k3/antigravity

feat: add comprehensive dashboard page with todo management, RSS feed…
This commit is contained in:
LogiFlow authored and GitHub committed 2025-11-23 15:45:35 +01:00
commit 341affdd30
5 files changed
+129 -30

No files matched your search

+20 -24
View File
@@ -138,6 +138,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -3681,6 +3682,7 @@
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -3691,6 +3693,7 @@
"integrity": "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3701,6 +3704,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3757,6 +3761,7 @@
"integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.4",
"@typescript-eslint/types": "8.46.4",
@@ -4042,6 +4047,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4476,6 +4482,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -5328,7 +5335,8 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -5373,29 +5381,6 @@
"node": ">= 0.8"
}
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -5552,6 +5537,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7006,6 +6992,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -8239,6 +8226,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@@ -8378,6 +8366,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -8768,6 +8757,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8808,6 +8798,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9860,6 +9851,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -10064,6 +10056,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -10192,6 +10185,7 @@
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10439,6 +10433,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -10530,6 +10525,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
+48 -5
View File
@@ -19,7 +19,7 @@ router.get('/', async (req, res) => {
// Essayer d'abord avec le champ priority (nouvelle version)
try {
const todos = await getAll(`
SELECT id, text, completed, priority, created_at
SELECT id, text, completed, priority, in_progress, created_at
FROM global_todos
WHERE user_id = $1
ORDER BY priority DESC, created_at DESC
@@ -37,8 +37,8 @@ router.get('/', async (req, res) => {
ORDER BY created_at DESC
`, [req.user.id]);
// Ajouter priority=false par défaut pour compatibilité frontend
const todosWithPriority = todos.map(t => ({ ...t, priority: false }));
// Ajouter priority=false et in_progress=false par défaut pour compatibilité frontend
const todosWithPriority = todos.map(t => ({ ...t, priority: false, in_progress: false }));
res.json(todosWithPriority);
}
} catch (error) {
@@ -77,6 +77,7 @@ router.post('/',
text,
completed: false,
priority: false,
in_progress: false,
created_at: new Date().toISOString()
});
} catch (error) {
@@ -94,7 +95,8 @@ router.put('/:id',
[
body('text').optional().trim().notEmpty(),
body('completed').optional().isBoolean(),
body('priority').optional().isBoolean()
body('priority').optional().isBoolean(),
body('in_progress').optional().isBoolean()
],
async (req, res) => {
try {
@@ -103,7 +105,7 @@ router.put('/:id',
return res.status(400).json({ error: 'Données invalides', details: errors.array() });
}
const { text, completed, priority } = req.body;
const { text, completed, priority, in_progress } = req.body;
// Vérifier que le todo appartient à l'utilisateur
const todo = await getOne('SELECT id FROM global_todos WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
@@ -130,6 +132,11 @@ router.put('/:id',
updates.push(`priority = $${paramCount}`);
params.push(priority ? 1 : 0);
}
if (in_progress !== undefined) {
paramCount++;
updates.push(`in_progress = $${paramCount}`);
params.push(in_progress ? 1 : 0);
}
if (updates.length > 0) {
paramCount++;
@@ -205,6 +212,42 @@ router.patch('/:id/priority', async (req, res) => {
}
});
/**
* PATCH /api/todos/:id/in-progress
* Basculer l'état in_progress d'un todo
*/
router.patch('/:id/in-progress', async (req, res) => {
try {
// Vérifier que le todo appartient à l'utilisateur
try {
const todo = await getOne('SELECT id, in_progress FROM global_todos WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
if (!todo) {
return res.status(404).json({ error: 'Todo non trouvé' });
}
const newInProgress = todo.in_progress ? 0 : 1;
await runQuery('UPDATE global_todos SET in_progress = $1 WHERE id = $2', [newInProgress, req.params.id]);
logger.info(`Todo global ${newInProgress ? 'marqué en cours' : 'démarqué en cours'} (ID: ${req.params.id}) par ${req.user.username}`);
res.json({ message: 'Statut en cours modifié avec succès', in_progress: newInProgress === 1 });
} catch (inProgressError) {
// Si le champ in_progress n'existe pas encore
if (inProgressError.message && inProgressError.message.includes('in_progress')) {
logger.warn('Tentative d\'utilisation de la fonctionnalité in_progress sans migration');
return res.status(400).json({
error: 'Fonctionnalité in_progress non disponible',
message: 'Veuillez exécuter la migration: node scripts/migrate_003.js'
});
}
throw inProgressError;
}
} catch (error) {
logger.error('Erreur lors du toggle du statut en cours:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* DELETE /api/todos/:id
* Supprimer un todo global
+32
View File
@@ -0,0 +1,32 @@
const { pool } = require('../config/database');
const logger = require('../config/logger');
async function migrate() {
const client = await pool.connect();
try {
logger.info('Starting migration: Add in_progress column to global_todos');
// Check if column exists
const checkRes = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='global_todos' AND column_name='in_progress'
`);
if (checkRes.rows.length === 0) {
await client.query('ALTER TABLE global_todos ADD COLUMN in_progress INTEGER DEFAULT 0');
logger.info('✓ Column in_progress added successfully');
} else {
logger.info('⚠ Column in_progress already exists');
}
} catch (error) {
logger.error('Migration failed:', error);
process.exit(1);
} finally {
client.release();
await pool.end();
}
}
migrate();
+18 -1
View File
@@ -572,6 +572,15 @@ const Index = () => {
}
};
const handleToggleTodoInProgress = async (todoId: number) => {
try {
await TodosService.toggleInProgress(todoId);
setTodos(todos.map(t => t.id === todoId ? { ...t, in_progress: !t.in_progress } : t));
} catch (error) {
showError("Erreur lors de la mise à jour du statut");
}
};
const confirmAddRssFeed = async (url: string) => {
try {
const newFeed = await RssService.addFeed(url);
@@ -1617,7 +1626,15 @@ const Index = () => {
<span className="text-sm flex-1 leading-snug">
{todo.text}
</span>
<Activity className="h-3.5 w-3.5 text-muted-foreground mr-1" title="En cours" />
<Button
variant="ghost"
size="icon"
className={`h-7 w-7 ${todo.in_progress ? 'opacity-100 text-orange-500' : 'opacity-0 group-hover:opacity-100 text-muted-foreground'} transition-all`}
onClick={() => todo.id && handleToggleTodoInProgress(todo.id)}
title={todo.in_progress ? "Marquer comme non commencé" : "Marquer comme en cours"}
>
<Activity className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
+11
View File
@@ -5,6 +5,7 @@ export interface Todo {
text: string;
completed: boolean;
priority?: boolean;
in_progress?: boolean;
due_date?: string;
created_at?: string;
}
@@ -81,6 +82,16 @@ class TodosService {
throw error;
}
}
async toggleInProgress(todoId: number): Promise<boolean> {
try {
await axios.patch(`${this.baseURL}/${todoId}/in-progress`, {}, this.getAuthHeader());
return true;
} catch (error) {
console.error('Error toggling in progress status:', error);
throw error;
}
}
}
export default new TodosService();