feat(epic-3): implémentation du Dashboard, mode lecture et notifications temps réel

This commit is contained in:
Michael committed 2026-02-04 15:12:54 +01:00
1 parent 1d31de95ec
commit dc26f22f51
20 files changed
+1237 -107

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
import { API_BASE, handleResponse } from './client';
export class ApiError extends Error {
public status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.name = 'ApiError';
}
}
export interface Article {
id: string;
feed_id: string;
guid: string;
title: string;
url?: string;
content?: string;
summary?: string;
author?: string;
image_url?: string;
published_at?: string;
is_read: boolean;
is_favorite: boolean;
read_at?: string;
created_at: string;
feed_title?: string;
}
export interface ListArticlesOptions {
limit?: number;
offset?: number;
unread?: boolean;
favorite?: boolean;
feed_id?: string;
}
export const articlesApi = {
async list(options: ListArticlesOptions = {}): Promise<Article[]> {
const params = new URLSearchParams();
if (options.limit) params.append('limit', options.limit.toString());
if (options.offset) params.append('offset', options.offset.toString());
if (options.unread) params.append('unread', 'true');
if (options.favorite) params.append('favorite', 'true');
let url = `${API_BASE}/articles`;
if (options.favorite) {
url = `${API_BASE}/articles/favorites`;
} else if (options.feed_id) {
url = `${API_BASE}/feeds/${options.feed_id}/articles`;
}
if (params.toString()) {
url += `?${params.toString()}`;
}
const response = await fetch(url, {
credentials: 'include',
});
return handleResponse<Article[]>(response);
},
async get(id: string): Promise<Article> {
const response = await fetch(`${API_BASE}/articles/${id}`, {
credentials: 'include',
});
return handleResponse<Article>(response);
},
async markRead(id: string): Promise<{ is_read: boolean }> {
const response = await fetch(`${API_BASE}/articles/${id}/read`, {
method: 'POST',
credentials: 'include',
});
return handleResponse(response);
},
async markUnread(id: string): Promise<{ is_read: boolean }> {
const response = await fetch(`${API_BASE}/articles/${id}/read`, {
method: 'DELETE',
credentials: 'include',
});
return handleResponse(response);
},
async toggleFavorite(id: string): Promise<{ is_favorite: boolean }> {
const response = await fetch(`${API_BASE}/articles/${id}/favorite`, {
method: 'POST',
credentials: 'include',
});
return handleResponse(response);
},
async markAllRead(feedId: string): Promise<{ message: string }> {
const response = await fetch(`${API_BASE}/feeds/${feedId}/read-all`, {
method: 'POST',
credentials: 'include',
});
return handleResponse(response);
},
async markAllReadGlobal(): Promise<{ message: string }> {
const response = await fetch(`${API_BASE}/articles/read-all`, {
method: 'POST',
credentials: 'include',
});
return handleResponse(response);
},
};
+14 -32
View File
@@ -1,48 +1,33 @@
const API_BASE = '/api/v1';
import { API_BASE, handleResponse } from './client';
interface RegisterRequest {
email: string;
password: string;
}
interface LoginRequest {
email: string;
password: string;
}
interface User {
export interface User {
id: string;
email: string;
is_admin: boolean;
}
interface LoginResponse {
export interface RegisterRequest {
email: string;
password: string;
}
export interface LoginRequest {
email: string;
password: string;
}
export interface LoginResponse {
token: string;
expires_at: string;
user: User;
}
interface RegisterResponse {
export interface RegisterResponse {
id: string;
email: string;
created_at: string;
}
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Request failed' }));
throw new ApiError(response.status, error.error || 'Request failed');
}
return response.json();
}
export const authApi = {
async register(data: RegisterRequest): Promise<RegisterResponse> {
const response = await fetch(`${API_BASE}/auth/register`, {
@@ -78,6 +63,3 @@ export const authApi = {
return handleResponse<User>(response);
},
};
export { ApiError };
export type { User, LoginResponse, RegisterResponse };
+18
View File
@@ -0,0 +1,18 @@
export const API_BASE = '/api/v1';
export class ApiError extends Error {
public status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.name = 'ApiError';
}
}
export async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Request failed' }));
throw new ApiError(response.status, error.error || 'Request failed');
}
return response.json();
}
+65
View File
@@ -0,0 +1,65 @@
import { API_BASE, handleResponse } from './client';
export interface Feed {
id: string;
user_id: string;
url: string;
title: string;
description?: string;
site_url?: string;
image_url?: string;
last_fetched_at?: string;
fetch_error?: string;
created_at: string;
updated_at: string;
unread_count?: number;
}
export interface AddFeedRequest {
url: string;
}
export const feedsApi = {
async list(): Promise<Feed[]> {
const response = await fetch(`${API_BASE}/feeds`, {
credentials: 'include',
});
return handleResponse<Feed[]>(response);
},
async add(data: AddFeedRequest): Promise<Feed> {
const response = await fetch(`${API_BASE}/feeds`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
});
return handleResponse<Feed>(response);
},
async get(id: string): Promise<Feed> {
const response = await fetch(`${API_BASE}/feeds/${id}`, {
credentials: 'include',
});
return handleResponse<Feed>(response);
},
async delete(id: string): Promise<void> {
const response = await fetch(`${API_BASE}/feeds/${id}`, {
method: 'DELETE',
credentials: 'include',
});
await handleResponse(response);
},
async importOPML(file: File): Promise<{ imported: number; skipped: number; errors?: string[] }> {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE}/feeds/import/opml`, {
method: 'POST',
credentials: 'include',
body: formData,
});
return handleResponse(response);
},
};