Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions front/src/components/tutorial/TutorialOverlay.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import {
View, Text, StyleSheet, Modal, TouchableOpacity,
Animated, useWindowDimensions, Platform,
Animated, useWindowDimensions,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTutorial } from '../../context/TutorialContext';
Expand Down Expand Up @@ -87,11 +87,16 @@ export default function TutorialOverlay({ navigation }) {
isLastStep, targets, nextStep, dismiss,
} = useTutorial();

const { width: W, height: H } = useWindowDimensions();
// Sur web desktop, le portail Modal est recadré à 430 px centré (CSS dans index.js).
// Les coordonnées renvoyées par measure() sont en coords viewport, donc on soustrait
// l'offset gauche du conteneur pour obtenir des coordonnées locales au portail.
const modalLeft = Platform.OS === 'web' && W > 430 ? (W - 430) / 2 : 0;
const { height: H } = useWindowDimensions();
// Sur web, le <Modal> de react-native-web portale dans un <div> en
// position:fixed attaché à document.body — INDÉPENDANT du conteneur #root
// (recentré à 430 px sur desktop via web/index.html). measure() renvoie déjà
// des coordonnées relatives au viewport complet, le même repère que celui
// utilisé par ce Modal plein-viewport : aucune correction d'offset n'est à
// appliquer. Un ancien code soustrayait un offset "modalLeft" en supposant
// le Modal lui-même recentré à 430 px — ce qui décalait le spotlight de
// plusieurs centaines de pixels sur une fenêtre desktop large (le cadre se
// retrouvait loin de la carte réellement visible, recentrée dans #root).

const slideY = useRef(new Animated.Value(24)).current;
const opacity = useRef(new Animated.Value(0)).current;
Expand Down Expand Up @@ -120,7 +125,7 @@ export default function TutorialOverlay({ navigation }) {
const hasSpot = !!targetRect;

const sp = hasSpot ? {
x: (targetRect.x - modalLeft) - SPOTLIGHT_PADDING,
x: targetRect.x - SPOTLIGHT_PADDING,
y: targetRect.y - SPOTLIGHT_PADDING,
w: targetRect.width + SPOTLIGHT_PADDING * 2,
h: targetRect.height + SPOTLIGHT_PADDING * 2,
Expand Down
11 changes: 10 additions & 1 deletion front/src/context/TutorialContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,17 @@ export function useTutorialTarget(key) {
});
}, [key, registerTarget]);

// Plusieurs mesures échelonnées sur ~1s : sur un écran atteint via une
// transition de stack (slide horizontal, @react-navigation/stack), la
// position mesurée juste après le premier layout peut encore refléter un
// état transitoire (mi-glissement, décalé vers la droite) plutôt que la
// position de repos finale — measure() renvoie la position réellement
// rendue à l'écran, transform en cours inclus. Chaque mesure écrase la
// précédente (registerTarget), donc la dernière — une fois la transition
// calmée — corrige automatiquement le spotlight, quel que soit le
// mécanisme ou la durée exacte de la transition.
const onLayout = useCallback(() => {
setTimeout(measure, 80);
[80, 250, 450, 700, 1000].forEach((delay) => setTimeout(measure, delay));
}, [measure]);

return { ref, onLayout, remeasure: measure };
Expand Down
25 changes: 19 additions & 6 deletions front/src/screens/Profile/InventoryScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,30 @@ export default function InventoryScreen({ navigation }) {

// ─── Tutorial (chapitre Inventaire) ──────────────────────────────────────────
const { pendingChapterId, activeChapterId, startChapter } = useTutorial();
const { ref: chestRef, onLayout: onChestLayout } = useTutorialTarget('inventory_chest');
const { ref: chestRef, onLayout: onChestLayout, remeasure: rChest } = useTutorialTarget('inventory_chest');

useFocusEffect(useCallback(() => { refetch(); }, [refetch]));

useFocusEffect(
useCallback(() => {
if (pendingChapterId === 'inventory') {
const t = setTimeout(() => startChapter('inventory'), 400);
return () => clearTimeout(t);
}
}, [pendingChapterId, startChapter]),
if (pendingChapterId !== 'inventory') return;
// ProfileStack (@react-navigation/stack) glisse cet écran depuis la
// droite : mesurer la cible pendant la transition capture sa position
// mi-glissement → spotlight décalé à droite, hors-cadre. On attend la
// fin RÉELLE de la transition (transitionEnd) avant de re-mesurer et de
// démarrer le chapitre ; le setTimeout reste un filet de sécurité si
// l'event ne se déclenche pas (écran déjà stable, pas de transition).
let started = false;
const launch = () => {
if (started) return;
started = true;
rChest();
setTimeout(() => startChapter('inventory'), 50);
};
const unsub = navigation.addListener('transitionEnd', launch);
const t = setTimeout(launch, 500);
return () => { unsub(); clearTimeout(t); };
}, [pendingChapterId, startChapter, navigation, rChest]),
);

const inventory = user?.inventory ?? [];
Expand Down
Loading