## 15. Autres comportements

### 15.1 Partage de contenu

**Décision** : Système de partage complet avec web player

#### 15.1.1 Bouton "Partager"

**Disponibilité** : Partout dans l'application

**Emplacements** :
- Player en lecture (bouton dans contrôles)
- Page profil créateur (sur chaque contenu)
- Liste de recherche (menu contextuel)
- Historique personnel

**Icône** : ⬆️ (universelle iOS/Android)

**Menu options** :
- Copier le lien
- WhatsApp
- Email
- SMS
- Plus... (sheet natif OS)

**Justification** :
- Viralité = croissance organique gratuite
- Aucune friction, partage universel

---

#### 15.1.2 Comportement du lien partagé

**Format URL** : `https://roadwave.fr/share/c/[content_id]`

**Comportement multi-plateforme** :

```
User clique lien partagé
    ↓
Page web responsive
    ↓
┌─────────────────────────────────┐
│ Si app installée                │
│ → Deep link (ouverture directe) │
└─────────────────────────────────┘
    ↓
┌─────────────────────────────────┐
│ Si app non installée             │
│ → Web player + CTA téléchargement│
└─────────────────────────────────┘
```

**Contenu de la page web** :

```html
┌───────────────────────────────────────┐
│ RoadWave                              │
├───────────────────────────────────────┤
│ [Image cover 16:9]                    │
│                                       │
│ 📻 Titre du contenu                   │
│ Par @créateur · 12 min · 🎧 2.3K      │
│                                       │
│ 📍 Paris 5e · Ancré                   │
│ 🏷️ #Voyage #Histoire                  │
│                                       │
│ Description : Lorem ipsum...          │
│                                       │
│ [▶️ Écouter maintenant]               │
│ (Player HTML5 si contenu public)      │
│                                       │
│ ──────────────────────────────────    │
│                                       │
│ 📱 Télécharger l'app RoadWave         │
│ [App Store] [Google Play]             │
│                                       │
│ [Voir le profil de @créateur]         │
└───────────────────────────────────────┘
```

**Métadonnées Open Graph (SEO)** :

```html
<meta property="og:title" content="[Titre contenu] - RoadWave">
<meta property="og:description" content="[Description ou extrait]">
<meta property="og:image" content="[URL cover image]">
<meta property="og:audio" content="[URL audio si public]">
<meta property="og:type" content="music.song">
<meta property="og:site_name" content="RoadWave">
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://roadwave.fr/player/[content_id]">
```

**Deep linking** :
- iOS : Universal Links (configuration `apple-app-site-association`)
- Android : App Links (configuration `assetlinks.json`)
- URL scheme : `roadwave://content/[content_id]`

**Justification** :
- Meilleure viralité (partage social optimisé)
- SEO (contenus indexés Google)
- UX optimale (web + app)
- Coût : 0€ (backend simple + CDN existant)

---

#### 15.1.3 Contenus Premium partagés

**Décision** : Preview 30 secondes + paywall

**Comportement** :

1. User clique lien contenu Premium partagé
2. Page web affiche badge "👑 Contenu Premium"
3. Player démarre automatiquement
4. Après **30 secondes exactement** :
   - Fade out audio (2 secondes)
   - Overlay apparaît :

```
┌─────────────────────────────────┐
│ 👑 Contenu réservé Premium      │
│                                 │
│ Profitez de ce contenu complet  │
│ et de milliers d'autres         │
│ sans publicité                  │
│                                 │
│ [Passer Premium - 4.99€/mois]   │
│ [Télécharger l'app]             │
└─────────────────────────────────┘
```

5. Utilisateur peut :
   - S'abonner Premium (redirection web Mangopay)
   - Télécharger l'app (redirection stores)
   - Rejouer les 30 premières secondes (illimité)

**Tracking** :
- Métriques créateur : "Partages Premium" + "Conversions Premium"
- Créateur touche sa part si conversion (70%)

**Justification** :
- Équilibre viralité / monétisation
- 30s = assez pour donner envie, pas assez pour satisfaire
- Protège revenus créateurs

---

### 15.2 Profil créateur

**Décision** : Profil public complet et transparent

#### 15.2.1 Structure de la page profil

**URL** : `https://roadwave.fr/@[pseudo]`

**Layout** :

```
┌────────────────────────────────────────┐
│ [Photo profil 120×120]                 │
│ @pseudo ✓                              │
│ [Badge vérifié si applicable]          │
│                                        │
│ Bio : Lorem ipsum dolor sit amet...    │
│ (300 caractères max)                   │
│                                        │
│ 🎧 1.2K abonnés                        │
│ 📻 42 contenus                         │
│ ⏱️ 18h de contenu créé                 │
│ 🔊 54K écoutes totales                 │
│                                        │
│ [S'abonner] [Partager profil] [•••]   │
│                                        │
│ ────────────────────────────────────   │
│                                        │
│ Contenus ▼ [Plus récents ▼]           │
│                                        │
│ ┌──────────────────────────────────┐  │
│ │ [Cover] Titre contenu 1           │  │
│ │ 12 min · 🎧 2.3K · 📍 Paris       │  │
│ │ [▶️]                              │  │
│ └──────────────────────────────────┘  │
│                                        │
│ ┌──────────────────────────────────┐  │
│ │ [Cover] Titre contenu 2           │  │
│ │ 8 min · 🎧 5.1K · 📍 Lyon         │  │
│ │ [▶️]                              │  │
│ └──────────────────────────────────┘  │
│                                        │
│ [Charger plus]                         │
└────────────────────────────────────────┘
```

**Informations affichées** :

| Élément | Visibilité | Détails |
|---------|------------|---------|
| **Photo + pseudo** | ✅ Public | Identité visuelle |
| **Badge vérifié ✓** | ✅ Public (si applicable) | Compte authentique |
| **Bio** | ✅ Public | 0-300 caractères, markdown basique (gras, italique, liens) |
| **Nombre abonnés** | ✅ Public | Arrondi si >1000 (ex: 1.2K, 54K) |
| **Nombre contenus** | ✅ Public | Exact |
| **Durée totale créée** | ✅ Public | Arrondi en heures (ex: 18h, 142h) |
| **Écoutes totales** | ✅ Public | Arrondi (ex: 54K, 1.2M) |
| **Liste abonnés** | ❌ Privé | Protection vie privée (RGPD) |
| **Revenus** | ❌ Privé | Confidentialité financière |
| **Localisation précise** | ❌ Privé | Sécurité |
| **Email** | ❌ Privé | Anti-spam |

**Tri des contenus** :

| Option | Comportement |
|--------|--------------|
| **Plus récents** | Date publication DESC (défaut) |
| **Plus populaires** | Écoutes complètes × (1 + (date_publication - now) / 90 jours) |
| **Plus anciens** | Date publication ASC |
| **Par tag** | Filtre multi-sélection tags |

**Recherche locale** :
- Barre recherche dans profil : "Rechercher dans les contenus de @pseudo"
- Recherche full-text sur titres + descriptions

**Actions menu [•••]** :
- Partager profil
- Signaler profil (spam, usurpation)
- Bloquer créateur (masque tous ses contenus)

---

#### 15.2.2 Statistiques publiques

**Décision** : Stats arrondies et motivantes

**Affichage public** :

| Métrique | Format affichage | Exemple |
|----------|------------------|---------|
| **Abonnés** | Exact si <1000, arrondi sinon | 342 / 1.2K / 54K / 1.2M |
| **Écoutes totales** | Arrondi dès 1000 | 842 / 5.4K / 142K / 2.1M |
| **Contenus publiés** | Exact | 42 contenus |
| **Durée totale** | Arrondi en heures | 18h / 142h de contenu |

**Métriques PRIVÉES (créateur uniquement)** :

| Métrique | Disponible dans dashboard créateur |
|----------|-------------------------------------|
| **Taux complétion moyen** | 78% (écoutes >80% / écoutes totales) |
| **Évolution abonnés** | Graphique 30j / 90j / 1 an |
| **Écoutes par contenu** | Tableau détaillé |
| **Revenus** | Dashboard monétisation dédié |
| **Taux conversion Premium** | Partages → conversions |
| **Démographie** | Âge / zone géo (agrégée, anonymisée) |

**Justification** :
- Arrondi = évite comparaisons anxiogènes
- Preuve sociale pour nouveaux auditeurs (trust)
- Gamification douce (motivation créateurs)
- Privacy by design

---

#### 15.2.3 Badge vérifié

**Décision** : Badge unique ✓ (vérifié officiel)

**Critères d'attribution** (au moins UN des critères) :

1. **KYC monétisation validé** : identité vérifiée via Mangopay KYC
2. **Célébrité / Média officiel** : validation manuelle équipe RoadWave
3. **Communauté significative** : ≥10K abonnés + compte actif >6 mois

**Affichage** :
- Badge bleu **✓** accolé au pseudo (partout : profil, player, recherche)
- Tooltip au survol/appui long : "Compte vérifié"

**Processus d'obtention** :

| Type | Processus |
|------|-----------|
| **Automatique (KYC)** | Badge attribué dès validation documents Mangopay |
| **Manuel (célébrité)** | Formulaire demande → équipe vérifie identité → validation 48-72h |
| **Automatique (10K)** | Badge attribué automatiquement à 10K abonnés si compte >6 mois |

**Retrait du badge** :
- Suspension monétisation → badge retiré temporairement
- Strikes multiples → badge retiré définitivement
- Usurpation identité détectée → ban + retrait

**Justification** :
- Combat usurpations d'identité
- Trust auditeurs (surtout pour médias/personnalités)
- Simplicité (1 seul badge, pas de gamification excessive)
- Coût : 0€ (champ boolean `verified` en DB)

---

### 15.3 Recherche

**Décision** : Recherche full-text + géo + filtres avancés

#### 15.3.1 Recherche par mot-clé

**Implémentation** : PostgreSQL full-text search (français)

**Configuration technique** :

```sql
-- Index full-text optimisé français
CREATE INDEX idx_content_search ON contents
USING GIN(
    to_tsvector('french',
        coalesce(title, '') || ' ' ||
        coalesce(description, '') || ' ' ||
        coalesce(creator_pseudo, '')
    )
);

-- Recherche avec ranking
SELECT
    c.*,
    ts_rank(
        to_tsvector('french', c.title || ' ' || c.description),
        plainto_tsquery('french', $search_query)
    ) AS rank
FROM contents c
WHERE to_tsvector('french', c.title || ' ' || c.description)
      @@ plainto_tsquery('french', $search_query)
ORDER BY rank DESC, listen_count DESC
LIMIT 20;
```

**Champs indexés** :
- Titre du contenu (poids × 3)
- Description (poids × 1)
- Pseudo créateur (poids × 2)
- Tags (poids × 1.5)

**Fonctionnalités** :

| Feature | Description |
|---------|-------------|
| **Stemming français** | "voyages" trouve "voyage", "voyager", etc. |
| **Correction auto** | Suggestion si 0 résultat |
| **Recherches populaires** | "Essayez plutôt : balade paris, audio-guide louvre" |
| **Historique personnel** | 10 dernières recherches sauvegardées |
| **Autocomplete** | Suggestions pendant frappe (top 5) |

**Coût** : 0€ (PostgreSQL natif)

**Migration future** :
- Si >100K contenus : Meilisearch (typo-tolerance avancée, ~20-50€/mois)
- Si >1M contenus : Elasticsearch cluster

**Justification** :
- PostgreSQL full-text = performant jusqu'à 500K contenus
- Stemming français natif
- 0€, aucune dépendance externe

---

#### 15.3.2 Recherche géographique

**Décision** : Recherche lieu + rayon paramétrable

**Interface utilisateur** :

```
┌─────────────────────────────────────┐
│ 🔍 Recherche contenu...             │
├─────────────────────────────────────┤
│ �� Lieu                             │
│ [Paris, France              ▼]     │
│ · Autour de moi (GPS actuel)       │
│ · Entrer une adresse/ville         │
│                                     │
│ 📏 Rayon de recherche               │
│ [●─────────────────] 50 km         │
│ (curseur 5 km → 500 km)            │
│                                     │
│ 🗺️ [Afficher sur carte]            │
└─────────────────────────────────────┘
```

**Géocodage** :

| Service | Usage | Coût |
|---------|-------|------|
| **Nominatim (OSM)** | MVP (API publique) | 0€ (rate limit 1 req/s) |
| **Nominatim self-hosted** | Scale (Docker) | 20-50€/mois VPS |
| **Mapbox Geocoding** | Fallback premium | 0.50€ / 1000 requêtes |

**Processus de recherche géo** :

1. User tape "Louvre" ou "Paris"
2. Autocomplete via Nominatim → liste suggestions
3. User sélectionne → récupération coordonnées (lat, lon)
4. Requête PostGIS :

```sql
SELECT c.*,
       ST_Distance(c.location::geography, ST_Point($lon, $lat)::geography) AS distance
FROM contents c
WHERE ST_DWithin(
    c.location::geography,
    ST_Point($lon, $lat)::geography,
    $radius_meters
)
ORDER BY distance ASC;
```

**Affichage résultats** :
- Tri par défaut : distance croissante
- Indication distance : "À 2.3 km" / "À 15 km" / "À 142 km"
- Option carte : markers cliquables (clustering si >50 résultats)

**Coût** :
- MVP : 0€ (Nominatim public)
- Scale : 20-50€/mois (Nominatim self-hosted Docker)

**Justification** :
- Essentiel pour tourisme / planification trajet
- OpenStreetMap = pas de dépendance Google
- PostGIS = performant (index GIST natif)

---

#### 15.3.3 Filtres avancés

**Décision** : 7 catégories de filtres combinables

**Interface filtres** :

```
┌─────────────────────────────────────┐
│ Filtres                        [×]  │
├─────────────────────────────────────┤
│ Type de contenu                     │
│ ☐ Contenu court (<5 min)            │
│ ☐ Podcast (>5 min)                  │
│ ☐ Radio live                        │
│ ☐ Audio-guide                       │
│                                     │
│ Durée                               │
│ ○ Toutes durées                     │
│ ○ <5 min                            │
│ ○ 5-15 min                          │
│ ○ 15-30 min                         │
│ ○ >30 min                           │
│                                     │
│ Classification âge                  │
│ ☐ Tout public                       │
│ ☐ 13+                               │
│ ☐ 16+                               │
│ ☐ 18+                               │
│                                     │
│ Géo-pertinence                      │
│ ☐ Ancré (lieu précis)               │
│ ☐ Contextuel (zone large)           │
│ ☐ Neutre (national)                 │
│                                     │
│ Tags (multi-sélection)              │
│ ☐ Automobile   ☐ Voyage             │
│ ☐ Famille      ☐ Histoire           │
│ ☐ Économie     ☐ Sciences           │
│ ... (liste complète tags)           │
│                                     │
│ Date de publication                 │
│ ○ Toutes dates                      │
│ ○ Dernières 24h                     │
│ ○ Cette semaine                     │
│ ○ Ce mois                           │
│ ○ Cette année                       │
│                                     │
│ Abonnement                          │
│ ○ Tous les contenus                 │
│ ○ Gratuits uniquement               │
│ ○ Premium uniquement 👑             │
│                                     │
│ ──────────────────────────────      │
│ [Réinitialiser] [Appliquer]         │
└─────────────────────────────────────┘
```

**Options de tri** :

| Tri | Algorithme |
|-----|-----------|
| **Pertinence** | Score recherche × (1 + log(listen_count + 1)) |
| **Popularité** | Écoutes complètes derniers 30j DESC |
| **Récent** | Date publication DESC |
| **Proximité** | Distance GPS ASC (si recherche géo active) |
| **Durée** | Durée audio ASC ou DESC |

**Sauvegarde de recherches** :

- Bouton "💾 Sauvegarder cette recherche"
- Nom personnalisable : "Podcasts voyage Paris"
- Maximum **5 recherches sauvegardées**
- Accès rapide : onglet "Recherches sauvegardées" dans page recherche
- Notifications optionnelles : "3 nouveaux contenus dans 'Podcasts voyage Paris'"

**Performances** :

```sql
-- Index composites pour filtres
CREATE INDEX idx_content_filters ON contents (
    content_type,
    duration,
    age_rating,
    geo_type,
    published_at
);

-- Index GIN pour tags
CREATE INDEX idx_content_tags ON contents USING GIN(tags);
```

**Coût** : 0€ (PostgreSQL + index standards)

**Justification** :
- Filtres essentiels pour découvrabilité
- Combinables = puissance maximale
- Sauvegarde = gain temps utilisateurs réguliers

---

#### 15.3.4 Page de résultats

**Décision** : Liste avec previews enrichies

**Layout résultats** :

```
┌─────────────────────────────────────────┐
│ 🔍 "voyage paris"                       │
│ 42 résultats · Tri : Pertinence ▼       │
│ [Filtres] [Carte]                       │
├─────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ [Cover     ] Balade à Paris          │ │
│ │ [16:9      ] @paris_stories ✓        │ │
│ │ [Image     ] 12 min · 🎧 2.3K        │ │
│ │              📍 Paris 5e · Ancré     │ │
│ │              🏷️ #Voyage #Histoire    │ │
│ │              [▶️ Écouter] [⋮]        │ │
│ └─────────────────────────────────────┘ │
│                                         │
│ ┌─────────────────────────────────────┐ │
│ │ [Cover     ] Secrets Montmartre      │ │
│ │ [16:9      ] @explore_paris          │ │
│ │ [Image     ] 8 min · 🎧 5.1K         │ │
│ │              📍 Paris 18e · Guide    │ │
│ │              🏷️ #Voyage #Art         │ │
│ │              [▶️ Écouter] [⋮]        │ │
│ └─────────────────────────────────────┘ │
│                                         │
│ [Charger plus] (20 suivants)            │
└─────────────────────────────────────────┘
```

**Informations par résultat** :

| Élément | Affichage |
|---------|-----------|
| **Cover image** | 16:9, 120×68 px, lazy loading |
| **Titre** | Tronqué 2 lignes max |
| **Créateur** | @pseudo + badge ✓ si vérifié, cliquable → profil |
| **Durée** | Format : "3 min" / "12 min" / "1h 24 min" |
| **Écoutes** | Arrondi : "2.3K" / "54K" / "1.2M" |
| **Localisation** | Ville + type géo (Ancré/Contextuel/Neutre) |
| **Tags** | Maximum 3 premiers tags |
| **Badge Premium** | 👑 si contenu premium |
| **Distance** | Si recherche géo : "À 2.3 km" |

**Actions contextuelles [⋮]** :
- Partager
- Ajouter à une playlist (future feature)
- Télécharger (offline)
- Signaler

**Pagination** :
- **20 résultats** par page
- Infinite scroll (charger automatiquement si scroll >80%)
- Bouton "Charger 20 suivants" en bas (fallback si scroll auto désactivé)

**Vue carte (alternative)** :
- Bouton toggle "Liste / Carte"
- Map Leaflet (OpenStreetMap)
- Markers cliquables → popup avec preview
- Clustering si >50 résultats proches

**Coût** : 0€ (Leaflet open source + OSM tiles gratuit)

**Justification** :
- Équilibre information / compacité
- Lazy loading = performances
- Infinite scroll = UX moderne

---

## Récapitulatif Section 15

| Point | Décision | Coût | Complexité |
|-------|----------|------|------------|
| **15.1.1** Bouton partager | Disponible partout (⬆️), menu natif OS | 0€ | Faible |
| **15.1.2** Lien partagé | Web player + deep link + Open Graph SEO | 0€ | Moyenne |
| **15.1.3** Premium partagé | Preview 30s + paywall overlay | 0€ | Faible |
| **15.2.1** Page profil | Profil public complet (stats + bio + contenus + tri) | 0€ | Faible |
| **15.2.2** Stats publiques | Arrondies (abonnés, écoutes, durée totale) | 0€ | Faible |
| **15.2.3** Badge vérifié | ✓ si KYC/célébrité/>10K abonnés | 0€ | Faible |
| **15.3.1** Recherche texte | PostgreSQL full-text french + stemming | 0€ | Moyenne |
| **15.3.2** Recherche géo | Lieu + rayon (Nominatim OSM) | 0-50€/mois | Moyenne |
| **15.3.3** Filtres | 7 catégories combinables + sauvegarde recherches | 0€ | Moyenne |
| **15.3.4** Page résultats | Liste enrichie + vue carte Leaflet + infinite scroll | 0€ | Moyenne |

**Coût total MVP : 0-50€/mois** (Nominatim self-hosted optionnel)

---

## Points d'attention pour Gherkin

- Tester partage contenu public vs Premium (preview 30s)
- Tester deep linking iOS/Android (ouverture app si installée)
- Tester Open Graph (aperçu correct sur WhatsApp, Twitter, Facebook)
- Tester profil public (stats arrondies, badge vérifié)
- Tester recherche full-text français (stemming, accents)
- Tester recherche géo + rayon (PostGIS distance)
- Tester combinaison filtres multiples (AND logic)
- Tester sauvegarde recherches (max 5)
- Tester pagination infinite scroll + fallback bouton
- Tester vue carte Leaflet (clustering, markers cliquables)
