Import Tripoli Cloud frontend prototype

React/TypeScript/Vite/Tailwind prototype from the design session (login,
dashboard, photos, backups, settings views with mocked API layer).
Infra work (Rust services, Ansible, OpenTofu) follows in subsequent commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-09-16 21:21:43 +02:00
commit 5dbd3b96f6
73 changed files with 4789 additions and 0 deletions

53
app/src/api/auth.ts Normal file
View file

@ -0,0 +1,53 @@
import type { User } from '../types';
import { mockFetch } from './client';
export interface Credentials {
email: string;
password: string;
}
export interface SignupPayload extends Credentials {
name: string;
}
function initialsOf(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return '??';
return parts
.slice(0, 2)
.map((p) => p[0]!.toUpperCase())
.join('');
}
export async function login({ email }: Credentials): Promise<User> {
return mockFetch({
id: 'usr_1',
name: 'Leïla Barghouti',
initials: 'LB',
email,
role: 'Administratrice',
isGuest: false,
});
}
export async function signup({ name, email }: SignupPayload): Promise<User> {
return mockFetch({
id: 'usr_new',
name,
initials: initialsOf(name),
email,
role: 'Administratrice',
isGuest: false,
});
}
export async function loginAsGuest(): Promise<User> {
return mockFetch({
id: 'usr_guest',
name: 'Invité',
initials: 'IN',
email: '',
role: 'Accès invité',
isGuest: true,
});
}

19
app/src/api/backups.ts Normal file
View file

@ -0,0 +1,19 @@
import type { BackupDevice } from '../types';
import { mockFetch } from './client';
const STATUS_LABEL: Record<BackupDevice['status'], string> = {
'up-to-date': 'À jour',
syncing: 'En cours',
late: 'Retard 2 j',
};
const RAW: Array<Omit<BackupDevice, 'id' | 'statusLabel'>> = [
{ device: 'Poste principal', detail: '64 Go · documents, messages, réglages', status: 'up-to-date', icon: 'smartphone' },
{ device: 'Portable de travail', detail: '212 Go · dossier de travail et bureau', status: 'up-to-date', icon: 'laptop' },
{ device: 'Tablette de consultation', detail: '38 Go · documents en lecture seule', status: 'syncing', icon: 'tablet' },
{ device: 'Coffre secondaire hors site', detail: '812 Go · miroir complet du volume', status: 'late', icon: 'server' },
];
export async function getBackups(): Promise<BackupDevice[]> {
return mockFetch(RAW.map((b, i) => ({ ...b, id: `backup_${i}`, statusLabel: STATUS_LABEL[b.status] })));
}

13
app/src/api/client.ts Normal file
View file

@ -0,0 +1,13 @@
/**
* Every function in src/api/ is written as if it called `API_BASE`, but
* currently resolves a local mock after a short simulated delay. Swapping
* in a real backend later means rewriting the body of these functions only
* — components and hooks never change.
*/
export const API_BASE = import.meta.env.VITE_API_URL;
export function mockFetch<T>(data: T, delayMs = 220): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => resolve(data), delayMs);
});
}

33
app/src/api/files.ts Normal file
View file

@ -0,0 +1,33 @@
import { THUMB_TONES } from '../lib/gradients';
import type { FileItem, StatItem } from '../types';
import { mockFetch } from './client';
const RAW_FILES: Array<Omit<FileItem, 'id' | 'thumbGradient'>> = [
{ name: 'Archives_fiscales_2026.zip', kind: 'archive', extLabel: 'Archive ZIP', size: '2,4 Go', modified: 'il y a 12 min' },
{ name: 'Contrats_signes_2026', kind: 'folder', extLabel: 'Dossier · 412 éléments', size: '18,7 Go', modified: 'il y a 2 h' },
{ name: 'Acte_de_propriete.pdf', kind: 'document', extLabel: 'Document PDF', size: '1,1 Mo', modified: 'hier' },
{ name: 'Backup_poste_principal', kind: 'backup', extLabel: 'Sauvegarde', size: '64 Go', modified: 'hier' },
{ name: 'Conseil_administration.mp4', kind: 'video', extLabel: 'Vidéo MP4', size: '820 Mo', modified: 'il y a 3 jours' },
{ name: 'Scans_administratifs', kind: 'folder', extLabel: 'Dossier · 38 éléments', size: '240 Mo', modified: 'il y a 5 jours' },
{ name: 'Statuts_et_annexes.docx', kind: 'document', extLabel: 'Document Word', size: '96 Ko', modified: 'il y a 1 sem.' },
{ name: 'Numerisations_1998', kind: 'folder', extLabel: 'Dossier · 219 éléments', size: '3,2 Go', modified: 'il y a 2 sem.' },
];
export async function getFiles(): Promise<FileItem[]> {
return mockFetch(
RAW_FILES.map((f, i) => ({
...f,
id: `file_${i}`,
thumbGradient: THUMB_TONES[i % THUMB_TONES.length]!,
})),
);
}
export async function getDashboardStats(): Promise<StatItem[]> {
return mockFetch([
{ id: 'stat_storage', label: 'Espace utilisé', value: '812 Go', note: '188 Go disponibles', icon: 'hard-drive', tone: 'brand' },
{ id: 'stat_docs', label: 'Documents', value: '12 480', note: '+142 cette semaine', icon: 'image', tone: 'brand' },
{ id: 'stat_devices', label: 'Postes', value: '4', note: 'tous scellés', icon: 'smartphone', tone: 'success' },
{ id: 'stat_access', label: 'Accès actifs', value: '7', note: '3 liens expirent bientôt', icon: 'share', tone: 'violet' },
]);
}

View file

@ -0,0 +1,13 @@
import type { NotificationItem } from '../types';
import { mockFetch } from './client';
const RAW: Array<Omit<NotificationItem, 'id'>> = [
{ icon: 'upload', title: 'Transfert terminé', body: 'Archives_fiscales_2026.zip a été chiffré et scellé.', time: 'il y a 12 min', unread: true },
{ icon: 'share', title: 'Nouveau dépôt', body: '42 documents ont été ajoutés au coffre Archives.', time: 'il y a 3 h', unread: true },
{ icon: 'check', title: 'Sauvegarde iPhone', body: '64 Go synchronisés et vérifiés, sans erreur.', time: 'hier, 23:40', unread: false },
{ icon: 'alert', title: 'Espace disque', body: "Il reste 18 % d'espace libre sur le volume principal.", time: 'il y a 2 jours', unread: false },
];
export async function getNotifications(): Promise<NotificationItem[]> {
return mockFetch(RAW.map((n, i) => ({ ...n, id: `notif_${i}` })));
}

28
app/src/api/photos.ts Normal file
View file

@ -0,0 +1,28 @@
import { THUMB_TONES, tone } from '../lib/gradients';
import type { Album, PhotoTile } from '../types';
import { mockFetch } from './client';
const RAW_ALBUMS: Array<Omit<Album, 'id' | 'thumbGradient'>> = [
{ name: 'Contrats', count: '412 éléments' },
{ name: 'Titres de propriété', count: '318 éléments' },
{ name: 'Numérisations 1998', count: '219 éléments' },
{ name: "Pièces d'identité", count: '1 204 éléments' },
];
export async function getAlbums(): Promise<Album[]> {
return mockFetch(
RAW_ALBUMS.map((a, i) => ({ ...a, id: `album_${i}`, thumbGradient: THUMB_TONES[i % THUMB_TONES.length]! })),
);
}
const TILE_TONES = ['#3b4a63', '#4a3f5f', '#51413a', '#33505a', '#3d4a41', '#46384f'];
export async function getPhotoTiles(): Promise<PhotoTile[]> {
return mockFetch(
Array.from({ length: 12 }, (_, i) => ({
id: `tile_${i}`,
label: `16 sept. · ${9 + i}:0${i % 6}`,
thumbGradient: tone(TILE_TONES[i % TILE_TONES.length]!, '#141821'),
})),
);
}

36
app/src/api/settings.ts Normal file
View file

@ -0,0 +1,36 @@
import type { NotificationPrefKey, SettingsValueRow } from '../types';
import { mockFetch } from './client';
export interface StaticSettingsRows {
language: SettingsValueRow[];
account: SettingsValueRow[];
}
export async function getStaticSettingsRows(): Promise<StaticSettingsRows> {
return mockFetch({
language: [
{ kind: 'value', id: 'lang', label: "Langue de l'interface", help: 'S’applique à tous les postes connectés à ce coffre.', value: 'Français' },
{ kind: 'value', id: 'region', label: 'Région', help: 'Détermine le format des dates, nombres et unités.', value: 'Tunisie' },
{ kind: 'value', id: 'tz', label: 'Fuseau horaire', help: 'Utilisé pour l’horodatage des sauvegardes.', value: 'GMT+1 · Tunis' },
{ kind: 'value', id: 'week-start', label: 'Premier jour de la semaine', help: 'Affichage des calendriers et des vues par date.', value: 'Lundi' },
],
account: [
{ kind: 'value', id: 'display-name', label: 'Nom affiché', help: 'Visible par les personnes autorisées sur ce coffre.', value: 'Leïla Barghouti' },
{ kind: 'value', id: 'email', label: 'Adresse e-mail', help: 'Sert d’identifiant de connexion.', value: 'leila@domaine.tn' },
{ kind: 'value', id: '2fa', label: 'Double authentification', help: 'Application d’authentification requise à chaque connexion.', value: 'Activée' },
{ kind: 'value', id: 'access', label: 'Accès autorisés', help: 'Quatre personnes disposent d’une clé de déverrouillage.', value: '4 accès' },
{ kind: 'value', id: 'recovery-key', label: 'Clé de récupération', help: 'À conserver hors ligne — elle ne peut pas être régénérée.', value: 'Téléchargée' },
],
});
}
export const NOTIFICATION_PREF_LABELS: Record<NotificationPrefKey, { label: string; help: string }> = {
push: { label: 'Notifications push', help: 'Alertes sur vos appareils mobiles et de bureau.' },
email: { label: 'Résumé par e-mail', help: 'Un e-mail à chaque dépôt ou retrait important.' },
weekly: { label: 'Rapport hebdomadaire', help: 'État des sauvegardes envoyé chaque lundi.' },
shared: { label: 'Activité des partages', help: 'Quand un accès autorisé dépose ou retire un document.' },
};
export async function getDefaultNotificationPrefs(): Promise<Record<NotificationPrefKey, boolean>> {
return mockFetch({ push: true, email: false, weekly: true, shared: true });
}

11
app/src/api/vault.ts Normal file
View file

@ -0,0 +1,11 @@
import { mockFetch } from './client';
export interface StorageSummary {
label: string;
percent: number;
sealedNote: string;
}
export async function getStorageSummary(): Promise<StorageSummary> {
return mockFetch({ label: '812 Go / 1 To', percent: 81, sealedNote: 'Coffre scellé il y a 2 h' });
}