fix: corrections post-audit (traçabilité, identité État, accents, graphique mobile)
Backend: - table audits réellement alimentée via trait Auditable (observers Eloquent) sur Utilisateur, SaisieTracker, Feed, Emotion ; mot de passe jamais loggé - 4 nouveaux tests (19/19 au total) Frontend web: - en-tête/pied République Française ajoutés aux pages connexion/inscription - bouton variant danger réellement rouge (tokens erreur DSFR) Mobile: - ~30 libellés ré-accentués (Intensité, Succès, Déconnexion, Répartition...) - Colors.secondary = rouge Marianne #E1000F (différenciation cartes/icônes) - vrai camembert SVG des rapports (react-native-svg) Docs: plans oraux mis à jour (audits actif, 29 tests, stockage JWT en Q/R) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
- **6** émotions de base **+ 36** sous-émotions (référentiel du sujet)
|
||||
- **8** entités au MCD · **1** API REST → **2** clients (web + mobile)
|
||||
- JWT **60 min** + refresh · throttle login **5/min**
|
||||
- **25** tests automatisés (15 back + 6 front + 4 mobile)
|
||||
- **29** tests automatisés (19 back + 6 front + 4 mobile)
|
||||
- **DSFR** : interface au Système de Design de l'État (charte gouv.fr) — web + mobile
|
||||
- **Déployé en ligne** : https://cesizen.cleanows.fr (Docker + HTTPS, vrai service en prod)
|
||||
- Contraintes sujet : budget **75 000 €**, livraison **12 mois**
|
||||
@@ -57,11 +57,11 @@
|
||||
|
||||
**16:00–17:30 · RGPD & sécurité (slide 13)**
|
||||
- Données de santé = sensibles (art. 9) · consentement · droits CNIL (export/anonymisation)
|
||||
- bcrypt (mdp) · soft delete · table audits · AES-256 prévu en prod HDS · TLS
|
||||
- bcrypt (mdp) · soft delete · **table audits alimentée** (observers Eloquent : création/modif/suppression tracées) · AES-256 prévu en prod HDS · TLS
|
||||
- ➡️ *« Tout cela est concrétisé par un prototype. »*
|
||||
|
||||
**17:30–19:00 · Prototype & perspectives (slide 14)**
|
||||
- Stack réelle : Laravel + Next.js + Expo + Docker · **25 tests automatisés**
|
||||
- Stack réelle : Laravel + Next.js + Expo + Docker · **29 tests automatisés**
|
||||
- **Déployé en ligne** : https://cesizen.cleanows.fr (Coolify, conteneurs Docker, HTTPS) → *« ce n'est pas qu'une maquette, c'est en production »*
|
||||
- Ouverture : activer les modules écartés sans réécrire · Mon Espace Santé (FHIR) · Open Data
|
||||
|
||||
@@ -100,8 +100,10 @@
|
||||
- **Données sensibles / RGPD ?** → consentement · droits CNIL implémentés · HDS · bcrypt + AES prévu
|
||||
- **Soft delete vs anonymisation ?** → soft delete = réversible 30j ; anonymisation = dissocie identité
|
||||
- **JWT, risques ?** → stateless/scalable · expiration courte + refresh · throttle anti-bruteforce · MFA prévu
|
||||
- **Où est stocké le token ?** → mobile = SecureStore (chiffré OS) · web = localStorage (limite XSS assumée, cible = cookie HttpOnly en v2)
|
||||
- **Traçabilité ?** → table audits **réellement alimentée** par observers Eloquent (qui/quoi/quand : action, table, anciennes/nouvelles valeurs, IP) · mdp jamais loggé
|
||||
- **Scalabilité 5000 users ?** → API stateless · index Postgres · pagination · cache TanStack Query
|
||||
- **Réellement développé ?** → prototype 3 modules (API+web+mobile) + 25 tests automatisés · **déployé en ligne** (Coolify/Docker/HTTPS)
|
||||
- **Réellement développé ?** → prototype 3 modules (API+web+mobile) + 29 tests automatisés · **déployé en ligne** (Coolify/Docker/HTTPS)
|
||||
- **Pourquoi le DSFR ?** → service de l'État → charte obligatoire gouv.fr · accessibilité RGAA native · cohérence + confiance · gain de temps (composants prêts)
|
||||
- **Vraiment en production ?** → oui : https://cesizen.cleanows.fr · conteneurs Docker · base PostgreSQL · CI de build · HTTPS
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
| **6:00 – 8:30** (2′30) | 7 — 8 | **Réponse fonctionnelle (Comptes + Informations)** | Module Comptes : JWT, RGPD, admin (slide 7 + maquette). Module Informations : feeds, modération, slug auto (slide 8 + maquette). 💬 Point fort transversal : **toute l'interface (web + mobile) suit le Système de Design de l'État (DSFR)** — la charte officielle gouv.fr → crédibilité institutionnelle + accessibilité native. Enchaîner : « pour le Tracker, je vais vous le montrer en direct. » |
|
||||
| **8:30 – 12:30** (4′) | démo live | **🎬 Démo de l'application — Tracker** | Sur l'app (compte `demo@cesizen.fr` **déjà connecté**) — **en ligne** sur https://cesizen.cleanows.fr **ou** en local : page Informations → journal de bord → **nouvelle saisie** (wizard 3 étapes : émotion → sous-émotion + intensité → date + note) → **Rapports** (changer la période : camembert + courbe d'évolution). 💬 Insister sur l'en-tête « République Française » (DSFR). 🛟 Si ça plante : basculer sur les maquettes (slide 9) sans s'excuser. |
|
||||
| **12:30 – 16:00** (3′30) | 10 — 11 — 12 | **Modélisation** | Cas d'usage UML (slide 10) : couverture par acteur. MCD Merise (slide 11) : 8 entités, cardinalités, hiérarchie d'émotions auto-référencée. Architecture MVC découplée (slide 12) : 1 API REST Laravel → 2 clients ; justifier le pattern (testabilité, évolutivité, non-duplication). |
|
||||
| **16:00 – 17:30** (1′30) | 13 | **RGPD & données sensibles** | Données de santé = sensibles (art. 9). Consentement, droits CNIL (export/anonymisation), soft delete. Sécurité : bcrypt (mots de passe), JWT, HDS, table audits ; chiffrement AES-256 au repos prévu en production. |
|
||||
| **17:30 – 19:00** (1′30) | 14 | **Prototype & perspectives** | Prototype fonctionnel (Laravel + Next.js + Expo + Docker), **25 tests automatisés** (15 back + 6 front + 4 mobile), **interface conforme DSFR** et **déployé en ligne** (Coolify, conteneurs Docker, HTTPS) sur https://cesizen.cleanows.fr — pas qu'une maquette, un vrai service en production. Ouverture : activer les modules écartés sans réécrire le modèle, Open Data, Mon Espace Santé (FHIR). |
|
||||
| **16:00 – 17:30** (1′30) | 13 | **RGPD & données sensibles** | Données de santé = sensibles (art. 9). Consentement, droits CNIL (export/anonymisation), soft delete. Sécurité : bcrypt (mots de passe), JWT, HDS, **table audits réellement alimentée** (observers Eloquent : création/modification/suppression tracées avec anciennes/nouvelles valeurs + IP, mot de passe jamais loggé) ; chiffrement AES-256 au repos prévu en production. |
|
||||
| **17:30 – 19:00** (1′30) | 14 | **Prototype & perspectives** | Prototype fonctionnel (Laravel + Next.js + Expo + Docker), **29 tests automatisés** (19 back + 6 front + 4 mobile), **interface conforme DSFR** et **déployé en ligne** (Coolify, conteneurs Docker, HTTPS) sur https://cesizen.cleanows.fr — pas qu'une maquette, un vrai service en production. Ouverture : activer les modules écartés sans réécrire le modèle, Open Data, Mon Espace Santé (FHIR). |
|
||||
| **19:00 – 20:00** (1′) | 14 | **Conclusion** | Synthèse : besoin reformulé → périmètre priorisé → réponse fonctionnelle → architecture pérenne → conformité. Remercier, ouvrir les questions. |
|
||||
|
||||
**Repères de rythme :** checkpoint mental à **12:30** (fin de la démo). Si retard, raccourcir la modélisation (garder MCD + MVC, survoler les cas d'usage) et garder 1 min pour conclure. ⚠️ **Lance la stack et connecte-toi AVANT d'entrer** dans la salle (sois déjà sur le journal de bord).
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
### Questions « piège » fréquentes
|
||||
**Q13. Qu'est-ce qui est réellement développé vs spécifié ?**
|
||||
> Un prototype fonctionnel des 3 modules (API + web + mobile) accompagne le dossier, avec **25 tests automatisés** (15 PHPUnit back, 6 Vitest web, 4 Jest mobile) ; les spécifications détaillées couvrent les 2 modules obligatoires + le Tracker, conformément à la consigne.
|
||||
> Un prototype fonctionnel des 3 modules (API + web + mobile) accompagne le dossier, avec **29 tests automatisés** (19 PHPUnit back, 6 Vitest web, 4 Jest mobile) ; les spécifications détaillées couvrent les 2 modules obligatoires + le Tracker, conformément à la consigne.
|
||||
|
||||
**Q14. Si c'était à refaire, que changeriez-vous ?**
|
||||
> J'ajouterais des tests automatisés d'accessibilité RGAA dès le début, et je formaliserais un diagramme de classes en complément du MCD pour la couche métier.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Models\Audit;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Trait de traçabilité Eloquent.
|
||||
*
|
||||
* Ajoute automatiquement une ligne dans la table `audits` lors des
|
||||
* événements created / updated / deleted du modèle qui l'utilise.
|
||||
*/
|
||||
trait Auditable
|
||||
{
|
||||
/**
|
||||
* Champs sensibles à ne jamais stocker dans l'audit.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected static array $champsAuditExclus = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Enregistre les hooks d'audit sur les événements du modèle.
|
||||
*/
|
||||
public static function bootAuditable(): void
|
||||
{
|
||||
static::created(function (Model $model) {
|
||||
$model->ecrireAudit('creation', null, $model->valeursAuditables($model->getAttributes()));
|
||||
});
|
||||
|
||||
static::updated(function (Model $model) {
|
||||
$modifies = array_keys($model->getDirty());
|
||||
|
||||
if ($modifies === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$anciennes = $model->valeursAuditables($model->getOriginal());
|
||||
$nouvelles = $model->valeursAuditables($model->getDirty());
|
||||
|
||||
$model->ecrireAudit(
|
||||
'modification',
|
||||
array_intersect_key($anciennes, $nouvelles),
|
||||
$nouvelles
|
||||
);
|
||||
});
|
||||
|
||||
static::deleted(function (Model $model) {
|
||||
$model->ecrireAudit('suppression', $model->valeursAuditables($model->getAttributes()), null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les champs sensibles d'un jeu d'attributs.
|
||||
*
|
||||
* @param array<string, mixed> $valeurs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function valeursAuditables(array $valeurs): array
|
||||
{
|
||||
return array_diff_key($valeurs, array_flip(static::$champsAuditExclus));
|
||||
}
|
||||
|
||||
/**
|
||||
* Insère une ligne d'audit.
|
||||
*
|
||||
* @param array<string, mixed>|null $anciennes
|
||||
* @param array<string, mixed>|null $nouvelles
|
||||
*/
|
||||
protected function ecrireAudit(string $action, ?array $anciennes, ?array $nouvelles): void
|
||||
{
|
||||
Audit::create([
|
||||
'utilisateur_id' => auth()->id(),
|
||||
'action' => $action,
|
||||
'table_cible' => $this->getTable(),
|
||||
'enregistrement_id' => $this->getKey(),
|
||||
'anciennes_valeurs' => $anciennes,
|
||||
'nouvelles_valeurs' => $nouvelles,
|
||||
'ip_address' => request()->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\Auditable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Emotion extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use Auditable, HasFactory;
|
||||
|
||||
protected $table = 'emotions';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\Auditable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Feed extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use Auditable, HasFactory;
|
||||
|
||||
protected $table = 'feeds';
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\Auditable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SaisieTracker extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use Auditable, HasFactory;
|
||||
|
||||
protected $table = 'saisie_trackers';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\Auditable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -13,7 +14,7 @@ use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||
|
||||
class Utilisateur extends Authenticatable implements JWTSubject
|
||||
{
|
||||
use HasFactory, Notifiable, SoftDeletes;
|
||||
use Auditable, HasFactory, Notifiable, SoftDeletes;
|
||||
|
||||
protected $table = 'utilisateurs';
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Audit;
|
||||
use App\Models\Feed;
|
||||
use App\Models\Role;
|
||||
use App\Models\Utilisateur;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuditTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected Utilisateur $auteur;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RoleSeeder::class);
|
||||
|
||||
$role = Role::where('nom', 'membre')->first();
|
||||
$this->auteur = Utilisateur::create([
|
||||
'nom' => 'Audit',
|
||||
'prenom' => 'Test',
|
||||
'email' => 'audit@test.fr',
|
||||
'password' => 'Password123!',
|
||||
'role_id' => $role->id,
|
||||
'consentement_rgpd' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function creerFeed(array $attributs = []): Feed
|
||||
{
|
||||
return Feed::create(array_merge([
|
||||
'titre' => 'Article test',
|
||||
'slug' => 'article-test-'.uniqid(),
|
||||
'contenu' => 'Contenu',
|
||||
'est_publie' => true,
|
||||
'ordre' => 1,
|
||||
'auteur_id' => $this->auteur->id,
|
||||
], $attributs));
|
||||
}
|
||||
|
||||
public function test_la_creation_d_un_feed_genere_un_audit(): void
|
||||
{
|
||||
$feed = $this->creerFeed();
|
||||
|
||||
$this->assertDatabaseHas('audits', [
|
||||
'action' => 'creation',
|
||||
'table_cible' => 'feeds',
|
||||
'enregistrement_id' => $feed->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_la_modification_genere_un_audit_avec_anciennes_et_nouvelles_valeurs(): void
|
||||
{
|
||||
$feed = $this->creerFeed(['titre' => 'Avant']);
|
||||
|
||||
$feed->update(['titre' => 'Apres']);
|
||||
|
||||
$audit = Audit::where('action', 'modification')
|
||||
->where('enregistrement_id', $feed->id)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($audit);
|
||||
$this->assertSame('Avant', $audit->anciennes_valeurs['titre']);
|
||||
$this->assertSame('Apres', $audit->nouvelles_valeurs['titre']);
|
||||
}
|
||||
|
||||
public function test_la_suppression_genere_un_audit(): void
|
||||
{
|
||||
$feed = $this->creerFeed();
|
||||
$id = $feed->id;
|
||||
$feed->delete();
|
||||
|
||||
$this->assertDatabaseHas('audits', [
|
||||
'action' => 'suppression',
|
||||
'table_cible' => 'feeds',
|
||||
'enregistrement_id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_le_mot_de_passe_n_est_pas_stocke_dans_l_audit(): void
|
||||
{
|
||||
$audit = Audit::where('table_cible', 'utilisateurs')
|
||||
->where('enregistrement_id', $this->auteur->id)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($audit);
|
||||
$this->assertArrayNotHasKey('password', $audit->nouvelles_valeurs);
|
||||
$this->assertArrayNotHasKey('remember_token', $audit->nouvelles_valeurs);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
import Header from '@/components/layout/Header';
|
||||
import Footer from '@/components/layout/Footer';
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<div className="fr-container fr-py-8w">
|
||||
<div className="fr-grid-row fr-grid-row--center">
|
||||
<div className="fr-col-12 fr-col-md-8 fr-col-lg-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,3 +10,16 @@
|
||||
border: 1px solid var(--border-default-grey);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Bouton d'action destructive (rouge) — le DSFR ne fournit pas de variante
|
||||
« danger » native, on surcharge donc les couleurs avec les tokens d'erreur. */
|
||||
.fr-btn.app-btn--danger {
|
||||
color: var(--text-inverted-grey);
|
||||
background-color: var(--background-action-high-error);
|
||||
}
|
||||
.fr-btn.app-btn--danger:hover {
|
||||
background-color: var(--background-action-high-error-hover);
|
||||
}
|
||||
.fr-btn.app-btn--danger:active {
|
||||
background-color: var(--background-action-high-error-active);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const variantClasses: Record<ButtonVariant, string> = {
|
||||
secondary: "fr-btn--secondary",
|
||||
outline: "fr-btn--secondary",
|
||||
ghost: "fr-btn--tertiary-no-outline",
|
||||
danger: "fr-btn--secondary",
|
||||
danger: "app-btn--danger",
|
||||
};
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function TabLayout() {
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors.secondary,
|
||||
tabBarActiveTintColor: Colors.primary,
|
||||
tabBarInactiveTintColor: Colors.gray[400],
|
||||
tabBarStyle: {
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function DashboardScreen() {
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
|
||||
<Text style={styles.statLabel}>Intensite moy.</Text>
|
||||
<Text style={styles.statLabel}>Intensité moy.</Text>
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Text style={styles.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
|
||||
@@ -63,7 +63,7 @@ export default function DashboardScreen() {
|
||||
{recentSaisies.length > 0 && (
|
||||
<View style={styles.recentContainer}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>Dernieres emotions</Text>
|
||||
<Text style={styles.sectionTitle}>Dernières émotions</Text>
|
||||
<TouchableOpacity onPress={() => router.push('/(tabs)/journal')}>
|
||||
<Text style={styles.seeAll}>Tout voir</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -76,7 +76,7 @@ export default function DashboardScreen() {
|
||||
<View style={styles.saisieContent}>
|
||||
<Text style={styles.saisieEmotion}>{saisie.emotion?.nom}</Text>
|
||||
<Text style={styles.saisieDate}>
|
||||
{new Date(saisie.date_saisie).toLocaleDateString('fr-FR')} — Intensite: {saisie.intensite}/10
|
||||
{new Date(saisie.date_saisie).toLocaleDateString('fr-FR')} — Intensité: {saisie.intensite}/10
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function InfoLayout() {
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: Colors.white },
|
||||
headerTitleStyle: { fontWeight: '700', color: Colors.black },
|
||||
headerTintColor: Colors.secondary,
|
||||
headerTintColor: Colors.primary,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" options={{ title: 'Infos' }} />
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function EditEntryScreen() {
|
||||
note: note.trim() || undefined,
|
||||
date_saisie: dateSaisie,
|
||||
});
|
||||
Alert.alert('Succes', 'Saisie modifiee !', [
|
||||
Alert.alert('Succès', 'Saisie modifiée !', [
|
||||
{ text: 'OK', onPress: () => router.back() },
|
||||
]);
|
||||
} catch {
|
||||
@@ -60,13 +60,13 @@ export default function EditEntryScreen() {
|
||||
|
||||
{/* Emotion */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Emotion</Text>
|
||||
<Text style={styles.label}>Émotion</Text>
|
||||
<EmotionPicker value={emotionId} onChange={setEmotionId} />
|
||||
</View>
|
||||
|
||||
{/* Intensite */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>Intensite</Text>
|
||||
<Text style={styles.label}>Intensité</Text>
|
||||
<View style={styles.intensityContainer}>
|
||||
<Text style={styles.intensityValue}>{intensite}</Text>
|
||||
<Text style={styles.intensityUnit}>/10</Text>
|
||||
@@ -95,7 +95,7 @@ export default function EditEntryScreen() {
|
||||
style={styles.textArea}
|
||||
value={note}
|
||||
onChangeText={setNote}
|
||||
placeholder="Decrivez ce que vous ressentez..."
|
||||
placeholder="Décrivez ce que vous ressentez..."
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
textAlignVertical="top"
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function JournalScreen() {
|
||||
<View style={[styles.badge, { backgroundColor: `${item.emotion?.couleur}20` }]}>
|
||||
<Text style={[styles.badgeText, { color: item.emotion?.couleur }]}>{item.emotion?.nom}</Text>
|
||||
</View>
|
||||
<Text style={styles.intensityText}>Intensite: {item.intensite}/10</Text>
|
||||
<Text style={styles.intensityText}>Intensité: {item.intensite}/10</Text>
|
||||
</View>
|
||||
<View style={styles.intensityBar}>
|
||||
<View style={[styles.intensityFill, { width: `${item.intensite * 10}%`, backgroundColor: item.emotion?.couleur || Colors.primary }]} />
|
||||
@@ -76,7 +76,7 @@ export default function JournalScreen() {
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="book-outline" size={48} color={Colors.gray[300]} />
|
||||
<Text style={styles.emptyTitle}>Aucune saisie</Text>
|
||||
<Text style={styles.emptyText}>Commencez par enregistrer votre premiere emotion</Text>
|
||||
<Text style={styles.emptyText}>Commencez par enregistrer votre première émotion</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function NewEntryScreen() {
|
||||
note: note.trim() || undefined,
|
||||
date_saisie: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
Alert.alert('Succes', 'Saisie enregistree !', [
|
||||
Alert.alert('Succès', 'Saisie enregistrée !', [
|
||||
{ text: 'OK', onPress: () => router.back() },
|
||||
]);
|
||||
} catch {
|
||||
@@ -67,7 +67,7 @@ export default function NewEntryScreen() {
|
||||
{/* Step 2: Intensity */}
|
||||
{step === 2 && (
|
||||
<View>
|
||||
<Text style={styles.stepTitle}>A quel point ressentez-vous cette emotion ?</Text>
|
||||
<Text style={styles.stepTitle}>À quel point ressentez-vous cette émotion ?</Text>
|
||||
<View style={styles.intensityContainer}>
|
||||
<Text style={styles.intensityValue}>{intensite}</Text>
|
||||
<Text style={styles.intensityLabel}>/10</Text>
|
||||
@@ -115,7 +115,7 @@ export default function NewEntryScreen() {
|
||||
style={styles.textArea}
|
||||
value={note}
|
||||
onChangeText={setNote}
|
||||
placeholder="Decrivez ce que vous ressentez, ce qui a declenche cette emotion..."
|
||||
placeholder="Décrivez ce que vous ressentez, ce qui a déclenché cette émotion..."
|
||||
multiline
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-nati
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRapport } from '@/hooks/useRapport';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import EmotionPieChart from '@/components/EmotionPieChart';
|
||||
|
||||
type Period = 'week' | 'month' | 'quarter' | 'year';
|
||||
|
||||
@@ -10,7 +11,7 @@ const PERIODS: { value: Period; label: string }[] = [
|
||||
{ value: 'week', label: 'Semaine' },
|
||||
{ value: 'month', label: 'Mois' },
|
||||
{ value: 'quarter', label: 'Trimestre' },
|
||||
{ value: 'year', label: 'Annee' },
|
||||
{ value: 'year', label: 'Année' },
|
||||
];
|
||||
|
||||
export default function ReportsScreen() {
|
||||
@@ -50,10 +51,10 @@ export default function ReportsScreen() {
|
||||
<View style={styles.statCard}>
|
||||
<Ionicons name="trending-up" size={20} color={Colors.secondary} />
|
||||
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
|
||||
<Text style={styles.statLabel}>Intensite moy.</Text>
|
||||
<Text style={styles.statLabel}>Intensité moy.</Text>
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Ionicons name="heart" size={20} color={Colors.rougeMarianne} />
|
||||
<Ionicons name="heart" size={20} color={rapport.stats.emotion_dominante?.couleur || Colors.blueFranceSun} />
|
||||
<Text style={styles.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
|
||||
<Text style={styles.statLabel}>{rapport.stats.emotion_dominante?.nom || 'Aucune'}</Text>
|
||||
</View>
|
||||
@@ -69,7 +70,19 @@ export default function ReportsScreen() {
|
||||
{/* Repartition */}
|
||||
{rapport.repartition.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Repartition des emotions</Text>
|
||||
<Text style={styles.sectionTitle}>Répartition des émotions</Text>
|
||||
|
||||
{/* Camembert (donut) — équivalent mobile du graphique Recharts du web */}
|
||||
<EmotionPieChart
|
||||
data={rapport.repartition.map((item) => ({
|
||||
nom: item.nom,
|
||||
couleur: item.couleur,
|
||||
count: item.count,
|
||||
pourcentage: item.pourcentage,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Text style={styles.subSectionTitle}>Détail par émotion</Text>
|
||||
{rapport.repartition.map((item) => (
|
||||
<View key={item.nom} style={styles.repartitionItem}>
|
||||
<View style={styles.repartitionHeader}>
|
||||
@@ -92,7 +105,7 @@ export default function ReportsScreen() {
|
||||
) : (
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="bar-chart-outline" size={48} color={Colors.gray[300]} />
|
||||
<Text style={styles.emptyText}>Pas de donnees pour cette periode</Text>
|
||||
<Text style={styles.emptyText}>Pas de données pour cette période</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
@@ -120,6 +133,7 @@ const styles = StyleSheet.create({
|
||||
periodInfoText: { fontSize: 13, color: Colors.gray[400], textAlign: 'center' },
|
||||
section: { marginBottom: 24 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '700', color: Colors.black, marginBottom: 16 },
|
||||
subSectionTitle: { fontSize: 14, fontWeight: '700', color: Colors.gray[600], marginBottom: 12, marginTop: 4 },
|
||||
repartitionItem: { marginBottom: 16 },
|
||||
repartitionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 6 },
|
||||
colorDot: { width: 10, height: 10, borderRadius: 5, marginRight: 8 },
|
||||
|
||||
@@ -9,10 +9,10 @@ export default function ProfileScreen() {
|
||||
const { user, logout } = useAuthStore();
|
||||
|
||||
const handleLogout = () => {
|
||||
Alert.alert('Deconnexion', 'Voulez-vous vous deconnecter ?', [
|
||||
Alert.alert('Déconnexion', 'Voulez-vous vous déconnecter ?', [
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
{
|
||||
text: 'Se deconnecter',
|
||||
text: 'Se déconnecter',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await logout();
|
||||
@@ -44,14 +44,14 @@ export default function ProfileScreen() {
|
||||
<View style={styles.menuCard}>
|
||||
<MenuItem icon="person-outline" label="Informations personnelles" />
|
||||
<MenuItem icon="lock-closed-outline" label="Changer le mot de passe" />
|
||||
<MenuItem icon="shield-checkmark-outline" label="Donnees personnelles (RGPD)" />
|
||||
<MenuItem icon="shield-checkmark-outline" label="Données personnelles (RGPD)" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.menuSection}>
|
||||
<Text style={styles.sectionTitle}>Application</Text>
|
||||
<View style={styles.menuCard}>
|
||||
<MenuItem icon="information-circle-outline" label="A propos de CESIZen" />
|
||||
<MenuItem icon="information-circle-outline" label="À propos de CESIZen" />
|
||||
<MenuItem icon="document-text-outline" label="Conditions d'utilisation" />
|
||||
</View>
|
||||
</View>
|
||||
@@ -59,7 +59,7 @@ export default function ProfileScreen() {
|
||||
{/* Logout */}
|
||||
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout}>
|
||||
<Ionicons name="log-out-outline" size={20} color={Colors.error} />
|
||||
<Text style={styles.logoutText}>Se deconnecter</Text>
|
||||
<Text style={styles.logoutText}>Se déconnecter</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.version}>CESIZen v1.0.0</Text>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function LoginScreen() {
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.logo}>CESIZen</Text>
|
||||
<Text style={styles.subtitle}>Votre compagnon de sante mentale</Text>
|
||||
<Text style={styles.subtitle}>Votre compagnon de santé mentale</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
|
||||
@@ -25,10 +25,10 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => setSelectedParent(null)}>
|
||||
<Text style={styles.backText}>← Retour aux emotions</Text>
|
||||
<Text style={styles.backText}>← Retour aux émotions</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.subTitle}>
|
||||
Precisez votre emotion de{' '}
|
||||
Précisez votre émotion de{' '}
|
||||
<Text style={{ color: selectedParent.couleur, fontWeight: '700' }}>{selectedParent.nom}</Text> :
|
||||
</Text>
|
||||
<View style={styles.grid}>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import Svg, { Circle, G, Text as SvgText } from 'react-native-svg';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
export interface EmotionPieSlice {
|
||||
nom: string;
|
||||
couleur: string;
|
||||
count: number;
|
||||
pourcentage: number;
|
||||
}
|
||||
|
||||
interface EmotionPieChartProps {
|
||||
data: EmotionPieSlice[];
|
||||
/** Diamètre total du graphique en pixels. */
|
||||
size?: number;
|
||||
/** Épaisseur de l'anneau (donut) en pixels. */
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Camembert (donut) de répartition des émotions, équivalent mobile du
|
||||
* graphique Recharts du web. Construit avec react-native-svg : chaque part
|
||||
* est un arc de cercle tracé via le stroke-dasharray d'un <Circle>.
|
||||
*/
|
||||
export default function EmotionPieChart({
|
||||
data,
|
||||
size = 200,
|
||||
strokeWidth = 38,
|
||||
}: EmotionPieChartProps) {
|
||||
const total = data.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
if (total === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const center = size / 2;
|
||||
|
||||
// Décalage cumulé pour positionner chaque arc à la suite du précédent.
|
||||
let offsetAcc = 0;
|
||||
const segments = data.map((item) => {
|
||||
const fraction = item.count / total;
|
||||
const dash = fraction * circumference;
|
||||
const segment = {
|
||||
...item,
|
||||
dash,
|
||||
gap: circumference - dash,
|
||||
rotation: (offsetAcc / circumference) * 360,
|
||||
};
|
||||
offsetAcc += dash;
|
||||
return segment;
|
||||
});
|
||||
|
||||
const topPct = Math.round((data[0]?.count / total) * 100);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Svg width={size} height={size}>
|
||||
{/* Anneau de fond pour les arrondis */}
|
||||
<Circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
stroke={Colors.gray[100]}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
<G rotation={-90} origin={`${center}, ${center}`}>
|
||||
{segments.map((seg) => (
|
||||
<Circle
|
||||
key={seg.nom}
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
stroke={seg.couleur}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={`${seg.dash} ${seg.gap}`}
|
||||
strokeDashoffset={-((seg.rotation / 360) * circumference)}
|
||||
fill="none"
|
||||
/>
|
||||
))}
|
||||
</G>
|
||||
{/* Centre du donut : part dominante */}
|
||||
<SvgText
|
||||
x={center}
|
||||
y={center - 6}
|
||||
fontSize={30}
|
||||
fontWeight="800"
|
||||
fill={Colors.black}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{`${topPct}%`}
|
||||
</SvgText>
|
||||
<SvgText
|
||||
x={center}
|
||||
y={center + 16}
|
||||
fontSize={12}
|
||||
fill={Colors.gray[500]}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{data[0]?.nom ?? ''}
|
||||
</SvgText>
|
||||
</Svg>
|
||||
|
||||
{/* Légende */}
|
||||
<View style={styles.legend}>
|
||||
{data.map((item) => (
|
||||
<View key={item.nom} style={styles.legendRow}>
|
||||
<View style={[styles.legendDot, { backgroundColor: item.couleur }]} />
|
||||
<Text style={styles.legendName} numberOfLines={1}>
|
||||
{item.nom}
|
||||
</Text>
|
||||
<Text style={styles.legendPct}>{item.pourcentage}%</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { alignItems: 'center', marginBottom: 20 },
|
||||
legend: { alignSelf: 'stretch', marginTop: 20, gap: 8 },
|
||||
legendRow: { flexDirection: 'row', alignItems: 'center' },
|
||||
legendDot: { width: 12, height: 12, borderRadius: 6, marginRight: 10 },
|
||||
legendName: { flex: 1, fontSize: 14, color: Colors.black },
|
||||
legendPct: { fontSize: 14, fontWeight: '700', color: Colors.gray[600] },
|
||||
});
|
||||
@@ -6,9 +6,9 @@ export const Colors = {
|
||||
primary: '#000091',
|
||||
primaryDark: '#1212FF', // bleu survol / accent
|
||||
blueFranceSun: '#6A6AF4',
|
||||
// Rouge Marianne — accent institutionnel / alertes
|
||||
secondary: '#000091', // conservé en bleu pour les accents secondaires institutionnels
|
||||
secondaryDark: '#1212FF',
|
||||
// Rouge Marianne — accent institutionnel secondaire (DSFR officiel)
|
||||
secondary: '#E1000F',
|
||||
secondaryDark: '#C9000E',
|
||||
rougeMarianne: '#E1000F',
|
||||
black: '#161616', // texte principal DSFR
|
||||
white: '#FFFFFF',
|
||||
|
||||
Generated
+180
-3
@@ -25,6 +25,7 @@
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
@@ -3996,7 +3997,7 @@
|
||||
"version": "19.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
||||
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -4591,6 +4592,12 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/bplist-creator": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||
@@ -5155,6 +5162,56 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.14",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
|
||||
@@ -5186,7 +5243,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
@@ -5354,6 +5411,44 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domexception": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
|
||||
@@ -5378,6 +5473,35 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
@@ -8747,6 +8871,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
@@ -9022,6 +9152,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nullthrows": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||
@@ -9721,6 +9863,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom/node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
@@ -9879,6 +10041,21 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-svg": {
|
||||
"version": "15.12.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
|
||||
"integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^1.1.3",
|
||||
"warn-once": "0.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-worklets": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
||||
@@ -11295,7 +11472,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user