init project

This commit is contained in:
m
2026-07-11 20:12:00 +02:00
commit b1f7fe6959
42 changed files with 7930 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# VaultDrop
## Project Status
Project initialized — `webui/` (React Native) and `backend/` (Go) have scaffolding in place.
## Architecture
- **Backend**: Go, Gin HTTP framework, SQLite + FTS5, Tesseract OCR (system call)
- **Frontend**: React Native (Expo SDK 57), React Navigation, TanStack Query, react-native-image-picker
## Key Commands
```bash
# Backend
cd backend && go run cmd/server/main.go
# Frontend
cd webui && npx expo start
# Typecheck frontend
cd webui && npx tsc --noEmit
```
## Backend Structure
- Entry point: `backend/cmd/server/main.go`
- Internal packages: `handlers/`, `models/`, `repository/`, `service/`, `ocr/`
- Response helpers: `pkg/api/response.go`
- File uploads stored in `backend/uploads/`
- Standard JSON response envelope: `{ "data": ..., "meta": { "page": ..., "total": ... } }`
- Error format: `{ "error": { "code": "...", "message": "..." } }`
- OCR language: `fra+eng`
- Handlers are stub implementations (return not-implemented errors)
## Frontend Structure
- Entry point: `webui/App.tsx` (React Navigation + TanStack Query providers)
- Screens: `app/index.tsx` (home), `app/upload.tsx`, `app/scan.tsx`, `app/search.tsx`
- Components: `components/FileCard.tsx`, `TagChip.tsx`, `UploadProgress.tsx`
- Hooks: `hooks/useFiles.ts`, `hooks/useSearch.ts`, `hooks/useUpload.ts`
- API client: `api/client.ts`, types: `types/index.ts`, constants: `constants/api.ts`
- No business logic on the client — all heavy processing server-side
- API base URL configured via `EXPO_PUBLIC_API_BASE_URL` env var
## Non-Goals (V1)
Collaborative/multi-user, plugin system, complex offline sync, public sharing.
## References
- `README.md` — full spec, API endpoints, data flow, folder structure, iteration roadmap
+301
View File
@@ -0,0 +1,301 @@
# VaultDrop — Application de Gestion de Fichiers V1
## Vision
Application mobile tout-en-un permettant de centraliser, organiser et retrouver ses documents. Upload depuis l'appareil, scan caméra avec OCR, tagging et recherche rapide. Le back-end Go assure le traitement asynchrone (OCR, indexing) et la persistence. Le front React Native reste léger : il affiche, interagit et met en cache.
## Scope V1
On livre un produit fonctionnel utilisable au quotidien, pas un framework. V1 = gestion de fichiers avec scan OCR intégré. Pas de mode collaboratif, pas de plugins, pas de partage avancé.
## Non-Goals (V1)
- Mode collaboratif / multi-utilisateur
- Système de plugins
- Synchronisation offline complexe
- Plateforme de partage publique
---
## Vue d'Ensemble Architecture
```
┌─────────────────────┐ ┌─────────────────────┐
│ React Native App │ ────> │ API Go Backend │
│ (Expo managed) │ HTTPS │ (REST) │
│ - Upload │ │ - Upload fichiers │
│ - Caméra / Scan │ │ - OCR async │
│ - Tags / Recherche│ │ - Index/search │
│ - Affichage │ │ - Persistance │
└─────────────────────┘ └─────────────────────┘
```
Le mobile ne contient pas de logique métier. Tout traitement lourd (OCR, extraction de texte, indexation) est géré côté back-end.
---
## Fonctionnalités Attendues
### Upload de fichiers
- Sélection depuis la galerie appareil
- Upload par drag-and-drop interne
- Support des formats : PDF, images (JPG, PNG)
- Feedback visuel pendant l'envoi (progress)
- Retry automatique en cas d'échec réseau
### Scan Documents (Caméra)
- Prise de photo depuis l'app
- Recadrage et orientation automatique
- Envoi direct vers le back-end pour OCR
- Retour du texte extrait affiché à l'utilisateur
### Tagging
- Ajout de tags manuels sur chaque fichier
- Suggestion de tags basée sur le contenu OCR
- Filtrage par tag dans la liste
### Recherche
- Recherche full-text sur le contenu OCR
- Recherche par nom de fichier
- Filtres combinés (tag + texte)
### Listing
- Liste des fichiers uploadés avec aperçu
- Tri par date, nom, tag
- Pagination côté serveur
### Partage (Optionnel, Phase Later)
- Génération de lien temporaire
- Pas prioritaire en V1
---
## Exigences UX
- UI claire et épurée, minimaliste
- Temps de réponse < 2s pour les actions principales
- Feedback immédiat sur toutes les interactions
- Pas d'écran de chargement > 3s sans spinner
- Mode offline minimal : liste des fichiers déjà chargée visible même sans réseau
---
## Attentes API REST (Back-end Go)
### Endpoints
| Méthode | Path | Description |
|---|---|---|
| GET | /api/v1/files | Liste des fichiers (pagination) |
| POST | /api/v1/files/upload | Upload d'un fichier |
| GET | /api/v1/files/:id | Détail d'un fichier |
| DELETE | /api/v1/files/:id | Suppression d'un fichier |
| GET | /api/v1/files/search?q= | Recherche full-text |
| POST | /api/v1/files/:id/tags | Ajout de tags |
| GET | /api/v1/files/:id/tags | Tags d'un fichier |
| POST | /api/v1/ocr/jobs | Soumettre un job OCR |
| GET | /api/v1/ocr/jobs/:id | Statut d'un job OCR |
| GET | /api/v1/health | Health check |
### Format de réponse standard
```json
{
"data": { ... },
"meta": {
"page": 1,
"total": 42
}
}
```
### Erreurs
```json
{
"error": {
"code": "FILE_TOO_LARGE",
"message": "Fichier dépasse la limite de 50 Mo"
}
}
```
---
## Flux de Données
```
[Capture photo] ──> [Upload API] ──> [Back-end stocke]
v
[Job OCR créé]
v (async)
[Traitement OCR]
[Extraction texte]
[Indexation search]
v
[App affiche fichier] <──── [Polling statut] <── [Résultat prêt]
```
1. L'utilisateur prend une photo ou sélectionne un fichier
2. Le fichier est uploadé vers POST /api/v1/files/upload
3. Le back-end crée un job OCR et retourne immédiatement un ID
4. L'app poll GET /api/v1/ocr/jobs/:id ou utilise un webhook
5. Une fois l'OCR terminé, le texte est indexé et le fichier apparaît avec son contenu searchable
---
## Choix Techniques
### Front-end (React Native)
- Expo managed workflow (développement rapide, build plus simple)
- TanStack Query (gestion serveur state, cache, refetch)
- React Navigation (navigation entre écrans)
- react-native-image-picker (sélection + caméra)
- MMKV (stockage clé-valeur pour cache local)
### Back-end (Go)
- Framework HTTP : Gin ou Fiber (au choix implémenteur)
- Persistance : SQLite pour V1 (volumétrie faible attendue)
- OCR : Tesseract en ligne de commande (appel système)
- Search : SQLite FTS5 pour la recherche full-text
- Stockage fichiers : disque local avec chemin référencé en base
---
## Structure de Dossiers Suggérée
### Front-end (React Native / Expo)
```
mobile/
├── app/ # Expo Router ou navigation
│ ├── index.tsx # Ecran principal (liste)
│ ├── upload.tsx # Ecran upload
│ ├── scan.tsx # Ecran scan caméra
│ └── search.tsx # Ecran recherche
├── components/
│ ├── FileCard.tsx
│ ├── TagChip.tsx
│ └── UploadProgress.tsx
├── hooks/
│ ├── useFiles.ts # TanStack Query hooks
│ ├── useSearch.ts
│ └── useUpload.ts
├── api/
│ └── client.ts # Client API (axios ou fetch)
├── types/
│ └── index.ts # Types TypeScript
├── constants/
│ └── api.ts # URLs, clés API
└── package.json
```
### Back-end (Go)
```
backend/
├── cmd/
│ └── server/
│ └── main.go # Point d'entrée
├── internal/
│ ├── handlers/ # Handlers HTTP
│ ├── models/ # Modèles de données
│ ├── repository/ # Accès données
│ ├── service/ # Logique métier
│ └── ocr/ # Module OCR
├── pkg/
│ └── api/
│ └── response.go # Helpers réponse
├── uploads/ # Fichiers stockés
├── go.mod
└── go.sum
```
---
## Getting Started
### Prérequis
- Node.js latest lts
- npm ou yarn
- Expo CLI (`npm install -g expo-cli`)
- Go latest lts
- Tesseract OCR installé (`apt install tesseract-ocr` sur Debian/Ubuntu)
### Installation (Front-end)
```bash
cd mobile
npm install
npx expo start
```
### Installation (Back-end)
```bash
cd backend
go mod download
go run cmd/server/main.go
```
### Build APK Android (Expo)
```bash
npx expo run:android --variant release
```
---
## Variables d'Environnement
### Front-end (.env)
```
API_BASE_URL=http://localhost:8080/api/v1
TESSERACT_LANG=fr+eng
```
### Back-end (.env)
```
PORT=8080
UPLOAD_DIR=./uploads
MAX_FILE_SIZE_MB=50
OCR_LANG=fra+eng
```
---
## Itérations Futures
### V2 — Fiabilité et Performance
- Remplacement SQLite par PostgreSQL
- Upload chunked pour gros fichiers
- Compression d'images côté client
### V3 — Organisation Avancée
- Dossiers virtuels / hiérarchie
- Tags suggérés par IA
- OCR multilingue amélioré
### V4 — Collaboration
- Comptes utilisateurs
- Partage avec lien temporaire
- Rôle et permissions
### V5 — Plateforme
- Système de plugins (event bus)
- API publique
- Extensions tierces
---
## Statut du Projet
Phase : Conception et prototypage
Backend Go : En cours de structuration
Frontend React Native : À initier
+23
View File
@@ -0,0 +1,23 @@
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary
*.test
# Output
*.out
# Dependency directories
vendor/
# Uploads
uploads/*
!uploads/.gitkeep
# Environment files
.env
.env.local
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"log"
"os"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/handlers"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r := gin.Default()
r.GET("/api/v1/health", handlers.Health)
r.GET("/api/v1/files", handlers.ListFiles)
r.POST("/api/v1/files/upload", handlers.UploadFile)
r.GET("/api/v1/files/:id", handlers.GetFile)
r.DELETE("/api/v1/files/:id", handlers.DeleteFile)
r.GET("/api/v1/files/search", handlers.SearchFiles)
r.POST("/api/v1/files/:id/tags", handlers.AddTags)
r.GET("/api/v1/files/:id/tags", handlers.GetTags)
r.POST("/api/v1/ocr/jobs", handlers.CreateOcrJob)
r.GET("/api/v1/ocr/jobs/:id", handlers.GetOcrJobStatus)
log.Printf("Server starting on port %s", port)
if err := r.Run(":" + port); err != nil {
log.Fatal(err)
}
}
+37
View File
@@ -0,0 +1,37 @@
module github.com/vaultdrop/backend
go 1.26.4
require github.com/gin-gonic/gin v1.12.0
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
+89
View File
@@ -0,0 +1,89 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+69
View File
@@ -0,0 +1,69 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func ListFiles(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []interface{}{},
"meta": gin.H{
"page": 1,
"total": 0,
},
})
}
func GetFile(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{
"code": "FILE_NOT_FOUND",
"message": "File not found",
},
})
}
func UploadFile(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "Upload not yet implemented",
},
})
}
func DeleteFile(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "Delete not yet implemented",
},
})
}
func SearchFiles(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []interface{}{},
"meta": gin.H{
"page": 1,
"total": 0,
},
})
}
func AddTags(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "Add tags not yet implemented",
},
})
}
func GetTags(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []interface{}{},
})
}
+15
View File
@@ -0,0 +1,15 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"status": "healthy",
},
})
}
+25
View File
@@ -0,0 +1,25 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func CreateOcrJob(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "OCR job creation not yet implemented",
},
})
}
func GetOcrJobStatus(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{
"code": "JOB_NOT_FOUND",
"message": "OCR job not found",
},
})
}
+43
View File
@@ -0,0 +1,43 @@
package models
import "time"
type File struct {
ID string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
Size int64 `json:"size"`
Path string `json:"-"`
OcrText string `json:"ocrText,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type Tag struct {
ID string `json:"id"`
Name string `json:"name"`
}
type OcrJob struct {
ID string `json:"id"`
FileID string `json:"fileId"`
Status string `json:"status"`
Result string `json:"result,omitempty"`
CreatedAt time.Time `json:"createdAt"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
}
type PaginatedResponse struct {
Data interface{} `json:"data"`
Meta struct {
Page int `json:"page"`
Total int `json:"total"`
} `json:"meta"`
}
type ErrorResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
+38
View File
@@ -0,0 +1,38 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"data": data,
})
}
func Created(c *gin.Context, data interface{}) {
c.JSON(http.StatusCreated, gin.H{
"data": data,
})
}
func Paginated(c *gin.Context, data interface{}, page, total int) {
c.JSON(http.StatusOK, gin.H{
"data": data,
"meta": gin.H{
"page": page,
"total": total,
},
})
}
func Error(c *gin.Context, status int, code, message string) {
c.JSON(status, gin.H{
"error": gin.H{
"code": code,
"message": message,
},
})
}
View File
+5
View File
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"expo@claude-plugins-official": true
}
}
+41
View File
@@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# generated native folders
/ios
/android
+3
View File
@@ -0,0 +1,3 @@
# Expo HAS CHANGED
Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code.
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { HomeScreen } from './app/index';
import { UploadScreen } from './app/upload';
import { ScanScreen } from './app/scan';
import { SearchScreen } from './app/search';
const Stack = createNativeStackNavigator();
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} options={{ title: 'VaultDrop' }} />
<Stack.Screen name="Upload" component={UploadScreen} options={{ title: 'Upload' }} />
<Stack.Screen name="Scan" component={ScanScreen} options={{ title: 'Scan' }} />
<Stack.Screen name="Search" component={SearchScreen} options={{ title: 'Recherche' }} />
</Stack.Navigator>
</NavigationContainer>
<StatusBar style="auto" />
</QueryClientProvider>
);
}
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+66
View File
@@ -0,0 +1,66 @@
import { API_BASE_URL } from '../constants/api';
import { ApiError } from '../types';
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.error?.message || 'Request failed');
}
return response.json();
}
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint);
}
async post<T>(endpoint: string, body?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
});
}
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
async uploadFile<T>(endpoint: string, formData: FormData): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.error?.message || 'Upload failed');
}
return response.json();
}
}
export const apiClient = new ApiClient(API_BASE_URL);
+25
View File
@@ -0,0 +1,25 @@
{
"expo": {
"name": "webui",
"slug": "webui",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/android-icon-foreground.png",
"backgroundImage": "./assets/android-icon-background.png",
"monochromeImage": "./assets/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
+108
View File
@@ -0,0 +1,108 @@
import React from 'react';
import { View, FlatList, StyleSheet, TouchableOpacity, Text } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useFiles } from '../hooks/useFiles';
import { FileCard } from '../components/FileCard';
import { FileItem } from '../types';
type RootStackParamList = {
Home: undefined;
Upload: undefined;
Scan: undefined;
Search: undefined;
};
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
export function HomeScreen() {
const navigation = useNavigation<NavigationProp>();
const { data, isLoading, error } = useFiles();
const renderItem = ({ item }: { item: FileItem }) => (
<FileCard
file={item}
onPress={(file) => console.log('File pressed:', file.id)}
/>
);
if (isLoading) {
return (
<View style={styles.center}>
<Text>Chargement...</Text>
</View>
);
}
if (error) {
return (
<View style={styles.center}>
<Text>Erreur de chargement</Text>
</View>
);
}
return (
<View style={styles.container}>
<FlatList
data={data?.data || []}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
/>
<View style={styles.bottomNav}>
<TouchableOpacity
style={styles.navButton}
onPress={() => navigation.navigate('Upload')}
>
<Text style={styles.navText}>Upload</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navButton}
onPress={() => navigation.navigate('Scan')}
>
<Text style={styles.navText}>Scan</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navButton}
onPress={() => navigation.navigate('Search')}
>
<Text style={styles.navText}>Recherche</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
list: {
padding: 16,
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingVertical: 12,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
navButton: {
padding: 8,
},
navText: {
fontSize: 16,
color: '#1976D2',
},
});
+73
View File
@@ -0,0 +1,73 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import * as ImagePicker from 'react-native-image-picker';
export function ScanScreen() {
const takePhoto = async () => {
try {
const result = await ImagePicker.launchCamera({
mediaType: 'photo',
quality: 1,
});
if (result.didCancel) return;
if (result.errorCode) {
Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la caméra");
return;
}
if (result.assets && result.assets[0]) {
console.log('Photo taken:', result.assets[0]);
// TODO: Send to backend for OCR
}
} catch (err) {
Alert.alert('Erreur', "Impossible d'accéder à la caméra");
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Scanner un document</Text>
<Text style={styles.description}>
Prenez une photo de votre document pour extraire le texte via OCR
</Text>
<TouchableOpacity style={styles.scanButton} onPress={takePhoto}>
<Text style={styles.scanText}>Prendre une photo</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: '700',
marginBottom: 12,
},
description: {
fontSize: 16,
color: '#666',
textAlign: 'center',
marginBottom: 32,
},
scanButton: {
backgroundColor: '#4CAF50',
padding: 16,
borderRadius: 8,
width: '100%',
alignItems: 'center',
},
scanText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
+62
View File
@@ -0,0 +1,62 @@
import React, { useState } from 'react';
import { View, TextInput, FlatList, StyleSheet, Text } from 'react-native';
import { useSearch } from '../hooks/useSearch';
import { FileCard } from '../components/FileCard';
import { FileItem } from '../types';
export function SearchScreen() {
const [query, setQuery] = useState('');
const { data, isLoading } = useSearch(query);
const renderItem = ({ item }: { item: FileItem }) => (
<FileCard
file={item}
onPress={(file) => console.log('File pressed:', file.id)}
/>
);
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Rechercher un fichier..."
value={query}
onChangeText={setQuery}
autoCapitalize="none"
/>
{isLoading && <Text style={styles.loading}>Recherche en cours...</Text>}
<FlatList
data={data?.data || []}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
input: {
backgroundColor: '#fff',
padding: 12,
margin: 16,
borderRadius: 8,
fontSize: 16,
borderWidth: 1,
borderColor: '#e0e0e0',
},
loading: {
textAlign: 'center',
color: '#666',
marginBottom: 8,
},
list: {
padding: 16,
},
});
+77
View File
@@ -0,0 +1,77 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import * as ImagePicker from 'react-native-image-picker';
import { useUpload } from '../hooks/useUpload';
import { UploadProgress } from '../components/UploadProgress';
export function UploadScreen() {
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'processing' | 'success' | 'error'>('idle');
const [error, setError] = useState<string>();
const upload = useUpload();
const pickImage = async () => {
try {
const result = await ImagePicker.launchImageLibrary({
mediaType: 'photo',
quality: 1,
});
if (result.didCancel) return;
if (result.errorCode) {
Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la galerie");
return;
}
if (result.assets && result.assets[0]) {
setUploadStatus('uploading');
const asset = result.assets[0];
const file = {
uri: asset.uri,
type: asset.type || 'image/jpeg',
name: asset.fileName || 'photo.jpg',
};
try {
const response = await upload.mutateAsync(file as any);
setUploadStatus('processing');
console.log('Upload success:', response);
setUploadStatus('success');
} catch (err) {
setUploadStatus('error');
setError(err instanceof Error ? err.message : 'Upload failed');
}
}
} catch (err) {
Alert.alert('Erreur', "Impossible d'accéder à la galerie");
}
};
return (
<View style={styles.container}>
<UploadProgress status={uploadStatus} error={error} />
<TouchableOpacity style={styles.uploadButton} onPress={pickImage}>
<Text style={styles.uploadText}>Sélectionner une photo</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
uploadButton: {
backgroundColor: '#1976D2',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
uploadText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+81
View File
@@ -0,0 +1,81 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { FileItem } from '../types';
import { TagChip } from './TagChip';
interface FileCardProps {
file: FileItem;
onPress?: (file: FileItem) => void;
}
export function FileCard({ file, onPress }: FileCardProps) {
const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
return (
<TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}>
<View style={styles.header}>
<Text style={styles.name} numberOfLines={1}>
{file.name}
</Text>
<Text style={styles.size}>{formatSize(file.size)}</Text>
</View>
{file.ocrText && (
<Text style={styles.preview} numberOfLines={2}>
{file.ocrText}
</Text>
)}
<View style={styles.tags}>
{file.tags.map((tag) => (
<TagChip key={tag.id} name={tag.name} />
))}
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 16,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
name: {
fontSize: 16,
fontWeight: '600',
flex: 1,
marginRight: 8,
},
size: {
fontSize: 14,
color: '#666',
},
preview: {
fontSize: 14,
color: '#444',
marginBottom: 8,
lineHeight: 20,
},
tags: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
},
});
+43
View File
@@ -0,0 +1,43 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
interface TagChipProps {
name: string;
onRemove?: () => void;
}
export function TagChip({ name, onRemove }: TagChipProps) {
return (
<View style={styles.container}>
<Text style={styles.text}>{name}</Text>
{onRemove && (
<TouchableOpacity onPress={onRemove} style={styles.removeButton}>
<Text style={styles.removeText}>×</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#E3F2FD',
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 4,
flexDirection: 'row',
alignItems: 'center',
},
text: {
fontSize: 12,
color: '#1976D2',
},
removeButton: {
marginLeft: 4,
},
removeText: {
fontSize: 14,
color: '#1976D2',
fontWeight: '600',
},
});
+64
View File
@@ -0,0 +1,64 @@
import React from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
interface UploadProgressProps {
progress?: number;
status: 'idle' | 'uploading' | 'processing' | 'success' | 'error';
error?: string;
}
export function UploadProgress({ progress, status, error }: UploadProgressProps) {
if (status === 'idle') return null;
return (
<View style={styles.container}>
{status === 'uploading' && (
<>
<ActivityIndicator size="small" color="#1976D2" />
<Text style={styles.text}>
Upload en cours... {progress !== undefined && `${progress}%`}
</Text>
</>
)}
{status === 'processing' && (
<>
<ActivityIndicator size="small" color="#1976D2" />
<Text style={styles.text}>Traitement OCR en cours...</Text>
</>
)}
{status === 'success' && (
<Text style={[styles.text, styles.success]}>Upload terminé !</Text>
)}
{status === 'error' && (
<Text style={[styles.text, styles.error]}>
{error || "Erreur lors de l'upload"}
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
backgroundColor: '#F5F5F5',
borderRadius: 8,
marginBottom: 16,
},
text: {
marginLeft: 8,
fontSize: 14,
color: '#333',
},
success: {
color: '#4CAF50',
},
error: {
color: '#F44336',
},
});
+9
View File
@@ -0,0 +1,9 @@
export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://localhost:8080/api/v1';
export const ENDPOINTS = {
FILES: '/files',
UPLOAD: '/files/upload',
SEARCH: '/files/search',
OCR_JOBS: '/ocr/jobs',
HEALTH: '/health',
} as const;
+45
View File
@@ -0,0 +1,45 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { FileItem, PaginatedResponse } from '../types';
export function useFiles(page: number = 1, limit: number = 20) {
return useQuery({
queryKey: ['files', page, limit],
queryFn: () =>
apiClient.get<PaginatedResponse<FileItem>>(
`${ENDPOINTS.FILES}?page=${page}&limit=${limit}`
),
});
}
export function useFile(id: string) {
return useQuery({
queryKey: ['files', id],
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.FILES}/${id}`),
enabled: !!id,
});
}
export function useDeleteFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.delete(`${ENDPOINTS.FILES}/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
},
});
}
export function useAddTags() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) =>
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags }),
onSuccess: (_, { fileId }) => {
queryClient.invalidateQueries({ queryKey: ['files', fileId] });
},
});
}
+15
View File
@@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { FileItem, PaginatedResponse } from '../types';
export function useSearch(query: string, page: number = 1, limit: number = 20) {
return useQuery({
queryKey: ['search', query, page, limit],
queryFn: () =>
apiClient.get<PaginatedResponse<FileItem>>(
`${ENDPOINTS.FILES}/search?q=${encodeURIComponent(query)}&page=${page}&limit=${limit}`
),
enabled: query.length > 0,
});
}
+36
View File
@@ -0,0 +1,36 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { OcrJob } from '../types';
export function useUpload() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (file: File) => {
const formData = new FormData();
formData.append('file', file);
return apiClient.uploadFile<{ id: string; ocrJobId: string }>(
ENDPOINTS.UPLOAD,
formData
);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] });
},
});
}
export function useOcrJobStatus(jobId: string) {
return useQuery({
queryKey: ['ocr', jobId],
queryFn: () => apiClient.get<OcrJob>(`${ENDPOINTS.OCR_JOBS}/${jobId}`),
enabled: !!jobId,
refetchInterval: (query: any) => {
const status = query.state.data?.status;
if (status === 'completed' || status === 'failed') return false;
return 1000;
},
});
}
+8
View File
@@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
+6316
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "webui",
"version": "1.0.0",
"main": "index.ts",
"dependencies": {
"@react-navigation/native": "^7.3.8",
"@react-navigation/native-stack": "^7.17.10",
"@tanstack/react-query": "^5.101.2",
"expo": "~57.0.4",
"expo-status-bar": "~57.0.0",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native-image-picker": "^8.2.1",
"react-native-mmkv": "^4.3.2",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2"
},
"devDependencies": {
"@types/react": "~19.2.2",
"typescript": "~6.0.3"
},
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"private": true
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}
+39
View File
@@ -0,0 +1,39 @@
export interface FileItem {
id: string;
name: string;
mimeType: string;
size: number;
createdAt: string;
updatedAt: string;
ocrText?: string;
tags: Tag[];
}
export interface Tag {
id: string;
name: string;
}
export interface OcrJob {
id: string;
fileId: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
result?: string;
createdAt: string;
completedAt?: string;
}
export interface PaginatedResponse<T> {
data: T[];
meta: {
page: number;
total: number;
};
}
export interface ApiError {
error: {
code: string;
message: string;
};
}