Modern UI port, render-based @ui tests, dev seed, layer contracts
- Port modern clean theme (DM Sans, orange accent, app-* CSS classes) and screen redesigns from festipod-mockups; replace sketchy Ubuntu theme. New shared components: BottomNav, EventCover, EventMeetingPoints, Toast, AvatarStack, Tag, RelevanceIcon. - Restructure from prototyping shell to real mobile web app: path-based routing (History API), Gallery/DemoMode/PhoneFrame removed, Storybook setup for screen/component browsing. - ConnectScreen ported from mockup (QR-based user connection); routed at /profile/connect, wired from FriendsListScreen. - Dev-only auto-seed of NG wallet when empty (gated on NODE_ENV !== 'production'); bootstrapWallet already self-checks for non-empty ngSet so safe even in race conditions. - Render-based @ui test infrastructure: happy-dom + LocalDataProvider + RouterProvider via src/shared/test-harness/renderHelper.tsx, exposed on the world as renderedDoc. world.hasText/hasField/hasElement prefer the rendered DOM and fall back to source for backward compatibility. - Migrate 25 brittle @ui assertions from regex-on-source to DOM queries; delete implementation-detail tests (showDuplicateWarning, importableEvents, importedFrom — anti-patterns per the new contract). Update feature files where the UI changed: "Mes amis" → "Mon réseau", "Mes événements à venir" → "À venir" on home, Thématique removed from create-event wizard, etc. - Path-based @e2e steps (pushState + popstate dispatch) replacing the legacy "#/demo/…" hash routing tied to the deleted Gallery. - Add .project/knowledge/test-layer-contracts.md defining the role of each test layer (@ui = display with seed data + DOM, @data = mutations through NG broker, @e2e = critical user journeys) with anti-patterns and migration consequences. Test status: 75 passed / 71 skipped (explicit "non implémenté") / 2 failed (pre-existing @wip on ngSet.delete() NG ORM limitation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ Fonctionnalité: US-20 Voir le profil des personnes faisant partie de mon résea
|
||||
Scénario: Voir mon réseau
|
||||
Étant donné que je suis sur la page "mon profil"
|
||||
Alors l'écran contient un texte "Amis"
|
||||
Et l'écran contient un texte "Mes amis"
|
||||
Et l'écran contient un texte "Mon réseau"
|
||||
|
||||
Scénario: Voir un profil de mon réseau
|
||||
Étant donné que je suis sur la page "mon profil"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@USER @priority-2
|
||||
Fonctionnalité: US-26 Définir la portée d'un événement
|
||||
En tant qu'utilisateur
|
||||
Je peux relayer/présenter le contenu d'un événement et le catégoriser par type/thématique
|
||||
Je peux relayer/présenter le contenu d'un événement
|
||||
En indiquant son rayon d'intérêt en kilomètres
|
||||
Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification
|
||||
|
||||
@@ -17,15 +17,8 @@ Fonctionnalité: US-26 Définir la portée d'un événement
|
||||
Scénario: Définir le rayon d'intérêt
|
||||
* Scénario non implémenté
|
||||
|
||||
Scénario: Choisir une thématique
|
||||
Étant donné que je suis sur la page "relayer un événement"
|
||||
Alors l'écran contient une section "Thématique"
|
||||
|
||||
Scénario: Vérifier les champs obligatoires
|
||||
Étant donné que l'écran "create-event" est affiché
|
||||
Alors le formulaire contient les champs obligatoires suivants:
|
||||
| Nom de l'événement |
|
||||
| Date de début |
|
||||
| Heure de début |
|
||||
| Lieu |
|
||||
| Thématique |
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Header, Avatar, Text, Button, showToast } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import { useNavigate, useGoBack } from '../../../app/router';
|
||||
|
||||
type Mode = 'choice' | 'show' | 'scan';
|
||||
|
||||
export function ConnectScreen() {
|
||||
const navigate = useNavigate();
|
||||
const goBack = useGoBack();
|
||||
const { currentUser } = useFestipodData();
|
||||
const [mode, setMode] = useState<Mode>('choice');
|
||||
const [scanProgress, setScanProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'scan') return;
|
||||
setScanProgress(0);
|
||||
const start = Date.now();
|
||||
const id = setInterval(() => {
|
||||
const p = Math.min(1, (Date.now() - start) / 2800);
|
||||
setScanProgress(p);
|
||||
if (p >= 1) {
|
||||
clearInterval(id);
|
||||
showToast('Connexion établie avec Léa Bernard', 'success');
|
||||
navigate('/profile/friends');
|
||||
}
|
||||
}, 80);
|
||||
return () => clearInterval(id);
|
||||
}, [mode, navigate]);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Header
|
||||
title="Se connecter"
|
||||
left={
|
||||
<ArrowLeft
|
||||
size={20}
|
||||
onClick={() => (mode === 'choice' ? goBack() : setMode('choice'))}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 16 }}>
|
||||
{mode === 'choice' && (
|
||||
<>
|
||||
<Text style={{ fontSize: 13, color: '#888', marginBottom: 16, lineHeight: 1.5 }}>
|
||||
Pour vous connecter, scannez le QR code de l'autre personne ou affichez le vôtre pour qu'elle le scanne.
|
||||
</Text>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<Button variant="primary" style={{ padding: 16, fontSize: 15 }} onClick={() => setMode('show')}>
|
||||
Afficher mon QR code
|
||||
</Button>
|
||||
<Button style={{ padding: 16, fontSize: 15 }} onClick={() => setMode('scan')}>
|
||||
Scanner un QR code
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === 'show' && (
|
||||
<div style={{ textAlign: 'center', paddingTop: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 220,
|
||||
height: 220,
|
||||
margin: '0 auto 16px',
|
||||
border: '2px solid #e0e0e0',
|
||||
borderRadius: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#fff',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 190,
|
||||
height: 190,
|
||||
background: `
|
||||
linear-gradient(90deg, #1a1a1a 10%, transparent 10%, transparent 20%, #1a1a1a 20%, #1a1a1a 30%, transparent 30%, transparent 40%, #1a1a1a 40%, #1a1a1a 50%, transparent 50%, transparent 60%, #1a1a1a 60%, #1a1a1a 70%, transparent 70%, transparent 80%, #1a1a1a 80%, #1a1a1a 90%, transparent 90%),
|
||||
linear-gradient(#1a1a1a 10%, transparent 10%, transparent 20%, #1a1a1a 20%, #1a1a1a 30%, transparent 30%, transparent 40%, #1a1a1a 40%, #1a1a1a 50%, transparent 50%, transparent 60%, #1a1a1a 60%, #1a1a1a 70%, transparent 70%, transparent 80%, #1a1a1a 80%, #1a1a1a 90%, transparent 90%)
|
||||
`,
|
||||
backgroundSize: '17px 17px',
|
||||
opacity: 0.85,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
background: '#fff',
|
||||
padding: 4,
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
<Avatar initials={currentUser?.initials ?? '?'} color="#E8590C" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<Text className="user-content" style={{ fontWeight: 'bold', margin: '0 0 4px 0' }}>{currentUser?.name}</Text>
|
||||
<Text style={{ color: '#888', margin: 0, fontSize: 13 }}>
|
||||
Montrez ce QR code pour permettre à l'autre personne de vous connecter.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'scan' && (
|
||||
<div style={{ textAlign: 'center', paddingTop: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 240,
|
||||
height: 240,
|
||||
margin: '0 auto 16px',
|
||||
background: '#111',
|
||||
borderRadius: 16,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 24,
|
||||
border: '2px solid rgba(255,255,255,0.8)',
|
||||
borderRadius: 10,
|
||||
}}
|
||||
/>
|
||||
{(['tl', 'tr', 'bl', 'br'] as const).map(corner => {
|
||||
const base: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
width: 22,
|
||||
height: 22,
|
||||
border: '3px solid #E8590C',
|
||||
};
|
||||
const pos: React.CSSProperties =
|
||||
corner === 'tl' ? { top: 18, left: 18, borderRight: 'none', borderBottom: 'none' }
|
||||
: corner === 'tr' ? { top: 18, right: 18, borderLeft: 'none', borderBottom: 'none' }
|
||||
: corner === 'bl' ? { bottom: 18, left: 18, borderRight: 'none', borderTop: 'none' }
|
||||
: { bottom: 18, right: 18, borderLeft: 'none', borderTop: 'none' };
|
||||
return <div key={corner} style={{ ...base, ...pos }} />;
|
||||
})}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: `calc(24px + (100% - 48px) * ${(Math.sin(scanProgress * Math.PI * 2) + 1) / 2})`,
|
||||
height: 2,
|
||||
background: 'linear-gradient(90deg, transparent, #E8590C, transparent)',
|
||||
boxShadow: '0 0 12px #E8590C',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Text style={{ fontSize: 13, color: '#888', margin: 0 }}>
|
||||
Alignez le QR code dans le cadre…
|
||||
</Text>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
height: 4,
|
||||
background: '#f0f0f0',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${scanProgress * 100}%`,
|
||||
height: '100%',
|
||||
background: '#E8590C',
|
||||
transition: 'width 0.08s linear',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import { FriendsListScreen } from './FriendsListScreen';
|
||||
import { withProviders } from '../../../../.storybook/decorators';
|
||||
|
||||
const meta: Meta<typeof FriendsListScreen> = {
|
||||
title: 'Screens/User/FriendsListScreen',
|
||||
component: FriendsListScreen,
|
||||
decorators: [withProviders],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FriendsListScreen>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -1,106 +1,64 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Header, Text, Avatar, Input, Button, Badge } from '../../../shared/components/sketchy';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Header, Text, Avatar, Input, Button, BottomNav } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import type { ScreenProps } from '../../../screens';
|
||||
import { useNavigate } from '../../../app/router';
|
||||
|
||||
export function FriendsListScreen({ navigate }: ScreenProps) {
|
||||
const { getFriends, users, setSelectedUserId } = useFestipodData();
|
||||
const [activeTab, setActiveTab] = useState<'friends' | 'public'>('friends');
|
||||
const COLORS = ['#2B6CB0', '#9C4DC7', '#38A169', '#D69E2E', '#E53E3E', '#E8590C'];
|
||||
|
||||
export function FriendsListScreen() {
|
||||
const navigate = useNavigate();
|
||||
const { getFriends } = useFestipodData();
|
||||
const friends = getFriends();
|
||||
const publicProfiles = users.filter(u => u.isPublic);
|
||||
|
||||
const displayedList = activeTab === 'friends' ? friends : publicProfiles;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Header
|
||||
title="Mon réseau"
|
||||
left={<span onClick={() => navigate('profile')} style={{ cursor: 'pointer' }}>←</span>}
|
||||
title={`Mon réseau (${friends.length})`}
|
||||
left={<ArrowLeft size={20} onClick={() => navigate('/home')} style={{ cursor: 'pointer' }} />}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', borderBottom: '2px solid var(--sketch-black)' }}>
|
||||
<button
|
||||
onClick={() => setActiveTab('friends')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: activeTab === 'friends' ? 'var(--sketch-light-gray)' : 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'friends' ? '3px solid var(--sketch-black)' : '3px solid transparent',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
fontWeight: activeTab === 'friends' ? 'bold' : 'normal',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Mes amis ({friends.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('public')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: activeTab === 'public' ? 'var(--sketch-light-gray)' : 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'public' ? '3px solid var(--sketch-black)' : '3px solid transparent',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
fontWeight: activeTab === 'public' ? 'bold' : 'normal',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Profils publics
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search bar */}
|
||||
<div style={{ padding: 16, borderBottom: '1px solid var(--sketch-light-gray)' }}>
|
||||
<div style={{ padding: 16, borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Input placeholder="Rechercher..." />
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{displayedList.map((person) => (
|
||||
{friends.map((person, i) => (
|
||||
<div
|
||||
key={person.id}
|
||||
onClick={() => { setSelectedUserId(person.id); navigate('user-profile'); }}
|
||||
onClick={() => navigate(`/users/${person.id}`)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '12px 16px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid var(--sketch-light-gray)',
|
||||
borderBottom: '1px solid #f5f5f5',
|
||||
}}
|
||||
>
|
||||
<Avatar initials={person.initials} size="sm" />
|
||||
<Avatar initials={person.initials} color={COLORS[i % COLORS.length]} size="sm" />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Text className="user-content" style={{ margin: 0, fontWeight: 'bold' }}>{person.name}</Text>
|
||||
{person.role && <Badge>{person.role}</Badge>}
|
||||
</div>
|
||||
<Text style={{ margin: 0, fontSize: 13 }}>
|
||||
<Text className="user-content" style={{ margin: 0, fontWeight: 'bold' }}>{person.name}</Text>
|
||||
<Text style={{ margin: 0, fontSize: 13, color: '#888' }}>
|
||||
<span className="user-content">{person.username}</span>
|
||||
{person.eventsCount != null && (
|
||||
<span style={{ color: 'var(--sketch-gray)' }}> · {person.eventsCount} événements</span>
|
||||
)}
|
||||
{person.eventsCount != null && ` · ${person.eventsCount} événements`}
|
||||
</Text>
|
||||
</div>
|
||||
<Text style={{ margin: 0, fontSize: 20, color: 'var(--sketch-gray)' }}>›</Text>
|
||||
<Text style={{ margin: 0, fontSize: 20, color: '#ccc' }}>›</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add friend button */}
|
||||
{activeTab === 'friends' && (
|
||||
<div style={{ padding: 16, borderTop: '2px solid var(--sketch-black)' }}>
|
||||
<Button variant="primary" style={{ width: '100%' }}>
|
||||
+ Ajouter un ami
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ padding: 16, borderTop: '1px solid #f0f0f0' }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
style={{ width: '100%' }}
|
||||
onClick={() => navigate('/profile/connect')}
|
||||
>
|
||||
Se connecter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<BottomNav active="friends" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import { ProfileScreen } from './ProfileScreen';
|
||||
import { withProviders } from '../../../../.storybook/decorators';
|
||||
|
||||
const meta: Meta<typeof ProfileScreen> = {
|
||||
title: 'Screens/User/ProfileScreen',
|
||||
component: ProfileScreen,
|
||||
decorators: [withProviders],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ProfileScreen>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -1,106 +1,104 @@
|
||||
import React from 'react';
|
||||
import { Header, Avatar, Title, Text, Button, Card, Badge, Divider, NavBar } from '../../../shared/components/sketchy';
|
||||
import { Header, Avatar, Title, Text, Button, Card, Divider, BottomNav, Tag } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import type { ScreenProps } from '../../../screens';
|
||||
import { useNavigate } from '../../../app/router';
|
||||
|
||||
export function ProfileScreen({ navigate }: ScreenProps) {
|
||||
const { currentUser, getUserEvents, currentUserId, getFriends, setSelectedEventId } = useFestipodData();
|
||||
export function ProfileScreen() {
|
||||
const navigate = useNavigate();
|
||||
const { currentUser, getUserEvents, currentUserId, getFriends } = useFestipodData();
|
||||
const myEvents = getUserEvents(currentUserId);
|
||||
const friends = getFriends();
|
||||
|
||||
const user = currentUser;
|
||||
|
||||
const handleEventClick = (eventId: string) => {
|
||||
setSelectedEventId(eventId);
|
||||
navigate('event-detail');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Header
|
||||
title="Mon profil"
|
||||
left={<span onClick={() => navigate('home')} style={{ cursor: 'pointer' }}>←</span>}
|
||||
right={<span onClick={() => navigate('settings')} style={{ cursor: 'pointer' }}>⚙</span>}
|
||||
right={<span onClick={() => navigate('/settings')} style={{ cursor: 'pointer', fontSize: 18 }}>⚙</span>}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{/* Profile header */}
|
||||
<div style={{ padding: 24, textAlign: 'center' }}>
|
||||
<Avatar initials={user?.initials ?? '?'} size="lg" />
|
||||
<Avatar initials={user?.initials ?? '?'} color="#E8590C" size="lg" />
|
||||
<Title className="user-content" style={{ marginTop: 16, marginBottom: 4 }}>{user?.name}</Title>
|
||||
<Text className="user-content" style={{ margin: 0 }}>{user?.username}</Text>
|
||||
<Text className="user-content" style={{ margin: 0, color: '#888' }}>{user?.username}</Text>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 32, marginTop: 20 }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text style={{ fontWeight: 'bold', margin: 0 }}>{user?.eventsCount ?? myEvents.length}</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>Événements</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Événements</Text>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', cursor: 'pointer' }} onClick={() => navigate('friends-list')}>
|
||||
<div style={{ textAlign: 'center', cursor: 'pointer' }} onClick={() => navigate('/profile/friends')}>
|
||||
<Text style={{ fontWeight: 'bold', margin: 0 }}>{user?.friendsCount ?? friends.length}</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>Amis</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Amis</Text>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text style={{ fontWeight: 'bold', margin: 0 }}>{user?.participationsCount ?? 0}</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>Participations</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Participations</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 20, justifyContent: 'center' }}>
|
||||
<Button variant="primary" onClick={() => navigate('update-profile')}>Modifier le profil</Button>
|
||||
<Button onClick={() => navigate('share-profile')}>Partager</Button>
|
||||
<Button variant="primary" onClick={() => navigate('/profile/edit')}>Modifier le profil</Button>
|
||||
<Button onClick={() => navigate('/profile/share')}>Partager</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{
|
||||
margin: '12px 16px',
|
||||
padding: '14px 16px',
|
||||
background: 'linear-gradient(135deg, #FFF7ED, #FFFBF5)',
|
||||
borderRadius: 16,
|
||||
border: '1px solid #FDDCB5',
|
||||
}}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1, color: '#C05621', marginBottom: 8 }}>
|
||||
Mes intentions
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
|
||||
<Tag label="gouvernance coopérative" />
|
||||
<Tag label="communs numériques" />
|
||||
<Tag label="habitat participatif" color="#4a3000" bg="#e8f5e9" />
|
||||
<span style={{ fontSize: 20, cursor: 'pointer', color: '#C05621', lineHeight: 1 }}>
|
||||
+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Upcoming events */}
|
||||
<div style={{ padding: 16 }}>
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 12 }}>Mes événements à venir</Text>
|
||||
{myEvents.slice(0, 3).map((event) => (
|
||||
<Card key={event.id} onClick={() => handleEventClick(event.id)} style={{ marginBottom: 12 }}>
|
||||
<Card key={event.id} onClick={() => navigate(`/events/${event.id}`)} style={{ marginBottom: 12 }}>
|
||||
<Text className="user-content" style={{ margin: 0, fontWeight: 'bold' }}>{event.title}</Text>
|
||||
<Text className="user-content" style={{ margin: '4px 0 0 0', fontSize: 14 }}>
|
||||
{event.date}
|
||||
</Text>
|
||||
<Text style={{ margin: '4px 0 0 0', fontSize: 13, color: '#888' }}>{event.date}</Text>
|
||||
</Card>
|
||||
))}
|
||||
<Button style={{ width: '100%' }} onClick={() => navigate('events')}>
|
||||
<Button style={{ width: '100%' }} onClick={() => navigate('/events')}>
|
||||
Voir tous les événements
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Quick actions */}
|
||||
<div style={{ padding: '0 16px 16px' }}>
|
||||
<div
|
||||
className="sketchy-list-item"
|
||||
onClick={() => navigate('create-event')}
|
||||
>
|
||||
<div className="app-list-item" onClick={() => navigate('/events/new')}>
|
||||
<span style={{ marginRight: 12 }}>+</span>
|
||||
<Text style={{ margin: 0 }}>Relayer un événement</Text>
|
||||
</div>
|
||||
<div className="sketchy-list-item" onClick={() => navigate('friends-list')}>
|
||||
<div className="app-list-item" onClick={() => navigate('/profile/friends')}>
|
||||
<span style={{ marginRight: 12 }}>👥</span>
|
||||
<Text style={{ margin: 0 }}>Mes amis</Text>
|
||||
<Text style={{ margin: 0 }}>Mon réseau</Text>
|
||||
</div>
|
||||
<div className="sketchy-list-item">
|
||||
<div className="app-list-item">
|
||||
<span style={{ marginRight: 12 }}>📜</span>
|
||||
<Text style={{ margin: 0 }}>Événements passés</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Nav */}
|
||||
<NavBar
|
||||
items={[
|
||||
{ icon: '⌂', label: 'Accueil', onClick: () => navigate('home') },
|
||||
{ icon: '◎', label: 'Découvrir', onClick: () => navigate('events') },
|
||||
{ icon: '+', label: 'Relayer', onClick: () => navigate('create-event') },
|
||||
{ icon: '☺', label: 'Profil', active: true },
|
||||
]}
|
||||
/>
|
||||
<BottomNav active="profile" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Header, Text, Button, Card, Divider, Avatar } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import type { ScreenProps } from '../../../screens';
|
||||
import { useNavigate } from '../../../app/router';
|
||||
|
||||
export function ShareProfileScreen({ navigate }: ScreenProps) {
|
||||
export function ShareProfileScreen() {
|
||||
const navigate = useNavigate();
|
||||
const { currentUser } = useFestipodData();
|
||||
const user = currentUser;
|
||||
const profileLink = `festipod.app/u/${(user?.username ?? '').replace('@', '')}`;
|
||||
@@ -12,56 +13,52 @@ export function ShareProfileScreen({ navigate }: ScreenProps) {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Header
|
||||
title="Partager mon profil"
|
||||
left={<span onClick={() => navigate('profile')} style={{ cursor: 'pointer' }}>←</span>}
|
||||
left={<ArrowLeft size={20} onClick={() => navigate('/profile')} style={{ cursor: 'pointer' }} />}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 16 }}>
|
||||
{/* QR Code */}
|
||||
<Card style={{ textAlign: 'center', padding: 24 }}>
|
||||
<div style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
margin: '0 auto 16px',
|
||||
border: '3px solid var(--sketch-black)',
|
||||
borderRadius: 12,
|
||||
border: '2px solid #e0e0e0',
|
||||
borderRadius: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'var(--sketch-white)',
|
||||
background: '#fff',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{/* Simulated QR code pattern */}
|
||||
<div style={{
|
||||
width: 150,
|
||||
height: 150,
|
||||
background: `
|
||||
linear-gradient(90deg, var(--sketch-black) 10%, transparent 10%, transparent 20%, var(--sketch-black) 20%, var(--sketch-black) 30%, transparent 30%, transparent 40%, var(--sketch-black) 40%, var(--sketch-black) 50%, transparent 50%, transparent 60%, var(--sketch-black) 60%, var(--sketch-black) 70%, transparent 70%, transparent 80%, var(--sketch-black) 80%, var(--sketch-black) 90%, transparent 90%),
|
||||
linear-gradient(var(--sketch-black) 10%, transparent 10%, transparent 20%, var(--sketch-black) 20%, var(--sketch-black) 30%, transparent 30%, transparent 40%, var(--sketch-black) 40%, var(--sketch-black) 50%, transparent 50%, transparent 60%, var(--sketch-black) 60%, var(--sketch-black) 70%, transparent 70%, transparent 80%, var(--sketch-black) 80%, var(--sketch-black) 90%, transparent 90%)
|
||||
linear-gradient(90deg, #1a1a1a 10%, transparent 10%, transparent 20%, #1a1a1a 20%, #1a1a1a 30%, transparent 30%, transparent 40%, #1a1a1a 40%, #1a1a1a 50%, transparent 50%, transparent 60%, #1a1a1a 60%, #1a1a1a 70%, transparent 70%, transparent 80%, #1a1a1a 80%, #1a1a1a 90%, transparent 90%),
|
||||
linear-gradient(#1a1a1a 10%, transparent 10%, transparent 20%, #1a1a1a 20%, #1a1a1a 30%, transparent 30%, transparent 40%, #1a1a1a 40%, #1a1a1a 50%, transparent 50%, transparent 60%, #1a1a1a 60%, #1a1a1a 70%, transparent 70%, transparent 80%, #1a1a1a 80%, #1a1a1a 90%, transparent 90%)
|
||||
`,
|
||||
backgroundSize: '15px 15px',
|
||||
opacity: 0.8,
|
||||
borderRadius: 8,
|
||||
}} />
|
||||
{/* Center avatar */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
background: 'var(--sketch-white)',
|
||||
background: '#fff',
|
||||
padding: 4,
|
||||
borderRadius: '50%',
|
||||
}}>
|
||||
<Avatar initials={user?.initials ?? '?'} size="sm" />
|
||||
<Avatar initials={user?.initials ?? '?'} color="#E8590C" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="user-content" style={{ fontWeight: 'bold', margin: '0 0 4px 0' }}>{user?.name}</Text>
|
||||
<Text style={{ color: 'var(--sketch-gray)', margin: 0, fontSize: 14 }}>
|
||||
<Text style={{ color: '#888', margin: 0, fontSize: 14 }}>
|
||||
Scannez pour me retrouver sur Festipod
|
||||
</Text>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Link section */}
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 12 }}>Mon lien de profil</Text>
|
||||
<Card style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Text style={{
|
||||
@@ -71,6 +68,7 @@ export function ShareProfileScreen({ navigate }: ScreenProps) {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#888',
|
||||
}}>
|
||||
{profileLink}
|
||||
</Text>
|
||||
@@ -80,21 +78,16 @@ export function ShareProfileScreen({ navigate }: ScreenProps) {
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Stats */}
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 12 }}>Statistiques de parrainage</Text>
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-around', textAlign: 'center' }}>
|
||||
<div>
|
||||
<Text style={{ fontWeight: 'bold', fontSize: 24, margin: 0, color: 'var(--sketch-black)' }}>12</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>
|
||||
Personnes parrainées
|
||||
</Text>
|
||||
<Text style={{ fontWeight: 'bold', fontSize: 24, margin: 0, color: '#E8590C' }}>12</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Personnes parrainées</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text style={{ fontWeight: 'bold', fontSize: 24, margin: 0, color: 'var(--sketch-black)' }}>47</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>
|
||||
Scans du QR code
|
||||
</Text>
|
||||
<Text style={{ fontWeight: 'bold', fontSize: 24, margin: 0, color: '#E8590C' }}>47</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Scans du QR code</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Header, Text, Input, Button, Avatar } from '../../../shared/components/sketchy';
|
||||
import { Header, Text, Input, Button, Avatar, showToast } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import type { ScreenProps } from '../../../screens';
|
||||
import { useNavigate } from '../../../app/router';
|
||||
|
||||
export function UpdateProfileScreen({ navigate }: ScreenProps) {
|
||||
export function UpdateProfileScreen() {
|
||||
const navigate = useNavigate();
|
||||
const { currentUser, updateProfile } = useFestipodData();
|
||||
const user = currentUser;
|
||||
|
||||
@@ -17,28 +18,21 @@ export function UpdateProfileScreen({ navigate }: ScreenProps) {
|
||||
const handleSave = () => {
|
||||
const fullName = `${firstName} ${lastName}`.trim();
|
||||
const initials = `${firstName[0] ?? ''}${lastName[0] ?? ''}`.toUpperCase();
|
||||
updateProfile({
|
||||
name: fullName,
|
||||
initials,
|
||||
username,
|
||||
city,
|
||||
bio,
|
||||
});
|
||||
navigate('profile');
|
||||
updateProfile({ name: fullName, initials, username, city, bio });
|
||||
showToast('Profil mis à jour', 'success');
|
||||
navigate('/profile');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Header
|
||||
title="Modifier le profil"
|
||||
left={<span onClick={() => navigate('profile')} style={{ cursor: 'pointer' }}>✕</span>}
|
||||
left={<span onClick={() => navigate('/profile')} style={{ cursor: 'pointer', fontSize: 18 }}>✕</span>}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, padding: 16, overflow: 'auto' }}>
|
||||
{/* Photo */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
<Avatar initials={user?.initials ?? '?'} size="lg" />
|
||||
<Avatar initials={user?.initials ?? '?'} color="#E8590C" size="lg" />
|
||||
<Button style={{ marginTop: 12 }}>
|
||||
Changer la photo
|
||||
</Button>
|
||||
@@ -46,29 +40,29 @@ export function UpdateProfileScreen({ navigate }: ScreenProps) {
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<Text style={{ marginBottom: 6, fontSize: 14 }}>Prénom *</Text>
|
||||
<Text style={{ marginBottom: 6, fontSize: 13, color: '#888' }}>Prénom *</Text>
|
||||
<Input value={firstName} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFirstName(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text style={{ marginBottom: 6, fontSize: 14 }}>Nom *</Text>
|
||||
<Text style={{ marginBottom: 6, fontSize: 13, color: '#888' }}>Nom *</Text>
|
||||
<Input value={lastName} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setLastName(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text style={{ marginBottom: 6, fontSize: 14 }}>Pseudo</Text>
|
||||
<Text style={{ marginBottom: 6, fontSize: 13, color: '#888' }}>Pseudo</Text>
|
||||
<Input value={username} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUsername(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text style={{ marginBottom: 6, fontSize: 14 }}>Localisation</Text>
|
||||
<Text style={{ marginBottom: 6, fontSize: 13, color: '#888' }}>Localisation</Text>
|
||||
<Input value={city} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCity(e.target.value)} placeholder="Ville, Pays" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text style={{ marginBottom: 6, fontSize: 14 }}>Bio</Text>
|
||||
<Text style={{ marginBottom: 6, fontSize: 13, color: '#888' }}>Bio</Text>
|
||||
<textarea
|
||||
className="sketchy-input"
|
||||
className="app-input"
|
||||
value={bio}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setBio(e.target.value)}
|
||||
rows={3}
|
||||
@@ -78,8 +72,7 @@ export function UpdateProfileScreen({ navigate }: ScreenProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{ padding: 16, borderTop: '2px solid var(--sketch-black)' }}>
|
||||
<div style={{ padding: 16, borderTop: '1px solid #f0f0f0' }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
style={{ width: '100%' }}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Header, Avatar, Title, Text, Button, Card, Badge, Divider } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import type { ScreenProps } from '../../../screens';
|
||||
import { useNavigate, useGoBack, useParams } from '../../../app/router';
|
||||
|
||||
export function UserProfileScreen({ navigate }: ScreenProps) {
|
||||
const { users, currentUserId, selectedUser, getUserEvents, addFriend, getFriends, setSelectedEventId } = useFestipodData();
|
||||
|
||||
// Use selectedUser from context, fallback to first non-current user
|
||||
const viewedUser = selectedUser
|
||||
|| users.find(u => u.id !== currentUserId);
|
||||
export function UserProfileScreen() {
|
||||
const navigate = useNavigate();
|
||||
const goBack = useGoBack();
|
||||
const { userId } = useParams();
|
||||
const { users, currentUserId, getUser, getUserEvents, addFriend, getFriends } = useFestipodData();
|
||||
|
||||
const viewedUser = userId ? getUser(userId) : users.find(u => u.id !== currentUserId);
|
||||
const friends = getFriends();
|
||||
const isFriend = viewedUser ? friends.some(f => f.id === viewedUser.id) : false;
|
||||
const userEvents = viewedUser ? getUserEvents(viewedUser.id) : [];
|
||||
|
||||
// Hardcoded past events for this mockup view (not all in store yet)
|
||||
const pastEvents = [
|
||||
{ title: 'Forum Ouvert Transition', date: '22 jan.', location: "Tiers-lieu L'Hermitage", distance: 89, common: true },
|
||||
{ title: 'Rencontre des Colibris', date: '12 jan.', location: "La Maison de l'Environnement", distance: 7, common: true },
|
||||
@@ -26,29 +25,27 @@ export function UserProfileScreen({ navigate }: ScreenProps) {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Header
|
||||
title="Profil"
|
||||
left={<span onClick={() => navigate('event-detail')} style={{ cursor: 'pointer' }}>←</span>}
|
||||
left={<ArrowLeft size={20} onClick={goBack} style={{ cursor: 'pointer' }} />}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{/* User profile header */}
|
||||
<div style={{ padding: 24, textAlign: 'center' }}>
|
||||
<Avatar initials={viewedUser?.initials ?? '?'} size="lg" />
|
||||
<Avatar initials={viewedUser?.initials ?? '?'} color="#2B6CB0" size="lg" />
|
||||
<Title className="user-content" style={{ marginTop: 16, marginBottom: 4 }}>{viewedUser?.name}</Title>
|
||||
<Text className="user-content" style={{ margin: 0 }}>{viewedUser?.username}</Text>
|
||||
<Text className="user-content" style={{ margin: 0, color: '#888' }}>{viewedUser?.username}</Text>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 32, marginTop: 20 }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text style={{ fontWeight: 'bold', margin: 0 }}>{viewedUser?.eventsCount ?? userEvents.length}</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>Événements</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Événements</Text>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text style={{ fontWeight: 'bold', margin: 0 }}>{viewedUser?.friendsCount ?? 23}</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>Contacts</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Contacts</Text>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text style={{ fontWeight: 'bold', margin: 0 }}>{viewedUser?.participationsCount ?? 42}</Text>
|
||||
<Text style={{ fontSize: 12, color: 'var(--sketch-gray)', margin: 0 }}>Participations</Text>
|
||||
<Text style={{ fontSize: 12, color: '#888', margin: 0 }}>Participations</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,28 +62,20 @@ export function UserProfileScreen({ navigate }: ScreenProps) {
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Upcoming events from store */}
|
||||
<div style={{ padding: 16 }}>
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 12 }}>Événements à venir</Text>
|
||||
|
||||
{(userEvents.length > 0 ? userEvents : [
|
||||
{ id: 'event-1', title: 'Résidence Reconnexion', date: '16-20 fév.', location: 'Le Revel, Rogues (30)', distance: 142 },
|
||||
]).map((event) => (
|
||||
<Card key={event.id} onClick={() => { setSelectedEventId(event.id); navigate('event-detail'); }} style={{ marginBottom: 12 }}>
|
||||
{userEvents.map((event) => (
|
||||
<Card key={event.id} onClick={() => navigate(`/events/${event.id}`)} style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<Text className="user-content" style={{ margin: 0, fontWeight: 'bold' }}>{event.title}</Text>
|
||||
<Text className="user-content" style={{ margin: '4px 0 0 0', fontSize: 14 }}>
|
||||
{event.date}
|
||||
</Text>
|
||||
<Text style={{ margin: '2px 0 0 0', fontSize: 14 }}>
|
||||
<Text style={{ margin: '4px 0 0 0', fontSize: 13, color: '#888' }}>{event.date}</Text>
|
||||
<Text style={{ margin: '2px 0 0 0', fontSize: 13, color: '#888' }}>
|
||||
📍 <span className="user-content">{event.location}</span>
|
||||
{event.distance != null && (
|
||||
<span style={{ color: 'var(--sketch-gray)' }}> · {event.distance} km</span>
|
||||
)}
|
||||
{event.distance != null && ` · ${event.distance} km`}
|
||||
</Text>
|
||||
</div>
|
||||
<Badge>moi aussi</Badge>
|
||||
<Badge style={{ background: '#FFF7ED', color: '#E8590C' }}>moi aussi</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
@@ -94,24 +83,19 @@ export function UserProfileScreen({ navigate }: ScreenProps) {
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Past events (still hardcoded for mockup) */}
|
||||
<div style={{ padding: 16 }}>
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 12 }}>Événements passés</Text>
|
||||
|
||||
{pastEvents.map((event, i) => (
|
||||
<Card key={i} onClick={() => navigate('event-detail')} style={{ marginBottom: 12 }}>
|
||||
<Card key={i} style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<Text className="user-content" style={{ margin: 0, fontWeight: 'bold' }}>{event.title}</Text>
|
||||
<Text className="user-content" style={{ margin: '4px 0 0 0', fontSize: 14 }}>
|
||||
{event.date}
|
||||
</Text>
|
||||
<Text style={{ margin: '2px 0 0 0', fontSize: 14 }}>
|
||||
📍 <span className="user-content">{event.location}</span>
|
||||
<span style={{ color: 'var(--sketch-gray)' }}> · {event.distance} km</span>
|
||||
<Text style={{ margin: 0, fontWeight: 'bold' }}>{event.title}</Text>
|
||||
<Text style={{ margin: '4px 0 0 0', fontSize: 13, color: '#888' }}>{event.date}</Text>
|
||||
<Text style={{ margin: '2px 0 0 0', fontSize: 13, color: '#888' }}>
|
||||
📍 {event.location} · {event.distance} km
|
||||
</Text>
|
||||
</div>
|
||||
{event.common && <Badge>moi aussi</Badge>}
|
||||
{event.common && <Badge style={{ background: '#FFF7ED', color: '#E8590C' }}>moi aussi</Badge>}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
@@ -119,17 +103,15 @@ export function UserProfileScreen({ navigate }: ScreenProps) {
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Contact form section */}
|
||||
<div style={{ padding: 16 }}>
|
||||
<Text style={{ fontWeight: 'bold', marginBottom: 12 }}>Envoyer un message</Text>
|
||||
<div style={{
|
||||
border: '2px solid var(--sketch-black)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
border: '1.5px solid #e0e0e0',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
minHeight: 80,
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: '#bbb',
|
||||
}}>
|
||||
Écrivez votre message ici...
|
||||
</div>
|
||||
|
||||
@@ -3,77 +3,83 @@ import { expect } from 'chai';
|
||||
import type { FestipodWorld } from '../../../../shared/support/world';
|
||||
|
||||
When('je clique sur un participant', async function (this: FestipodWorld) {
|
||||
this.navigateTo('#/demo/user-profile');
|
||||
await this.navigateTo('#/demo/user-profile');
|
||||
});
|
||||
|
||||
Given('je visualise le profil de {string}', async function (this: FestipodWorld, userName: string) {
|
||||
this.navigateTo('#/demo/user-profile');
|
||||
await this.navigateTo('#/demo/user-profile');
|
||||
expect(this.currentScreen, 'User profile screen should be loaded').to.not.be.null;
|
||||
this.attach(`Viewing profile: ${userName}`, 'text/plain');
|
||||
});
|
||||
|
||||
Then('je peux voir mon profil', async function (this: FestipodWorld) {
|
||||
expect(this.currentScreenId).to.equal('profile');
|
||||
const source = this.getRenderedText();
|
||||
expect(/<Avatar[^>]*initials="MD"[^>]*size="lg"/.test(source), 'Profile should have Avatar with initials="MD" and size="lg"').to.be.true;
|
||||
expect(/<Title[^>]*>Marie Dupont<\/Title>/.test(source), 'Profile should have Title "Marie Dupont"').to.be.true;
|
||||
expect(/@mariedupont/.test(source), 'Profile should have username @mariedupont').to.be.true;
|
||||
expectProfileDom(this, { initials: 'MD', name: 'Marie Dupont', username: '@mariedupont' });
|
||||
});
|
||||
|
||||
Then('je peux voir le profil de l\'utilisateur', async function (this: FestipodWorld) {
|
||||
expect(this.currentScreenId).to.equal('user-profile');
|
||||
const source = this.getRenderedText();
|
||||
expect(/<Avatar[^>]*initials="JD"[^>]*size="lg"/.test(source), 'User profile should have Avatar with initials="JD" and size="lg"').to.be.true;
|
||||
expect(/<Title[^>]*>Jean Durand<\/Title>/.test(source), 'User profile should have Title "Jean Durand"').to.be.true;
|
||||
expect(/@jeandurand/.test(source), 'User profile should have username @jeandurand').to.be.true;
|
||||
// user-profile defaults to the first non-current seed user when no userId is
|
||||
// bound — see UserProfileScreen.tsx. With current seed data that's Jean Durand.
|
||||
expectProfileDom(this, { initials: 'JD', name: 'Jean Durand', username: '@jeandurand' });
|
||||
});
|
||||
|
||||
Then('l\'écran affiche les informations du profil', async function (this: FestipodWorld) {
|
||||
const source = this.getRenderedText();
|
||||
const doc = this.renderedDoc;
|
||||
expect(doc, 'Screen should be rendered').to.not.be.null;
|
||||
const text = doc!.body.textContent ?? '';
|
||||
if (this.currentScreenId === 'profile') {
|
||||
expect(/<Avatar[^>]*initials="MD"/.test(source), 'Profile should have Avatar with initials="MD"').to.be.true;
|
||||
expect(/<Title[^>]*>Marie Dupont<\/Title>/.test(source), 'Profile should have Title "Marie Dupont"').to.be.true;
|
||||
expect(/@mariedupont/.test(source), 'Profile should have username @mariedupont').to.be.true;
|
||||
expect(text.includes('Marie Dupont'), 'Profile should display "Marie Dupont"').to.be.true;
|
||||
expect(text.includes('@mariedupont'), 'Profile should display "@mariedupont"').to.be.true;
|
||||
} else if (this.currentScreenId === 'user-profile') {
|
||||
expect(/<Avatar[^>]*initials="JD"/.test(source), 'User profile should have Avatar with initials="JD"').to.be.true;
|
||||
expect(/<Title[^>]*>Jean Durand<\/Title>/.test(source), 'User profile should have Title "Jean Durand"').to.be.true;
|
||||
expect(/@jeandurand/.test(source), 'User profile should have username @jeandurand').to.be.true;
|
||||
expect(text.includes('Jean Durand'), 'User profile should display "Jean Durand"').to.be.true;
|
||||
expect(text.includes('@jeandurand'), 'User profile should display "@jeandurand"').to.be.true;
|
||||
} else {
|
||||
expect.fail(`Unexpected screen "${this.currentScreenId}" for profile info check`);
|
||||
}
|
||||
const avatar = doc!.querySelector('.app-avatar');
|
||||
expect(avatar, 'Profile screen should render an avatar').to.not.be.null;
|
||||
});
|
||||
|
||||
Then('je peux contacter l\'utilisateur', async function (this: FestipodWorld) {
|
||||
expect(this.currentScreenId).to.equal('user-profile');
|
||||
const source = this.getRenderedText();
|
||||
const hasContactButton = /<Button>Contacter<\/Button>/.test(source);
|
||||
expect(hasContactButton, 'User profile should have "Contacter" button').to.be.true;
|
||||
const doc = this.renderedDoc;
|
||||
expect(doc, 'Screen should be rendered').to.not.be.null;
|
||||
const buttons = Array.from(doc!.querySelectorAll('button')).map(b => b.textContent ?? '');
|
||||
expect(buttons.some(t => t.includes('Contacter')),
|
||||
'User profile should expose a "Contacter" button').to.be.true;
|
||||
});
|
||||
|
||||
Then('je peux voir les événements auxquels l\'utilisateur a participé', async function (this: FestipodWorld) {
|
||||
expect(this.currentScreenId).to.equal('user-profile');
|
||||
const source = this.getRenderedText();
|
||||
expect(/Événements à venir/.test(source), 'User profile should have "Événements à venir" section').to.be.true;
|
||||
expect(/Événements passés/.test(source), 'User profile should have "Événements passés" section').to.be.true;
|
||||
const doc = this.renderedDoc;
|
||||
expect(doc, 'Screen should be rendered').to.not.be.null;
|
||||
const text = doc!.body.textContent ?? '';
|
||||
expect(text.includes('Événements à venir'),
|
||||
'User profile should have "Événements à venir" section').to.be.true;
|
||||
expect(text.includes('Événements passés'),
|
||||
'User profile should have "Événements passés" section').to.be.true;
|
||||
});
|
||||
|
||||
Then('les événements affichent leur localisation et distance', async function (this: FestipodWorld) {
|
||||
expect(this.currentScreenId).to.equal('user-profile');
|
||||
const source = this.getRenderedText();
|
||||
expect(/location: '[^']+'/.test(source), 'Events should have location data').to.be.true;
|
||||
expect(/distance: \d+/.test(source), 'Events should have distance data').to.be.true;
|
||||
expect(/\{event\.location\}/.test(source), 'Events should render location').to.be.true;
|
||||
expect(/\{event\.distance\}/.test(source), 'Events should render distance').to.be.true;
|
||||
const doc = this.renderedDoc;
|
||||
expect(doc, 'Screen should be rendered').to.not.be.null;
|
||||
const text = doc!.body.textContent ?? '';
|
||||
expect(text.includes('📍'), 'Events should show a 📍 location icon').to.be.true;
|
||||
expect(/\d+\s*km/.test(text), 'Events should show a distance in km').to.be.true;
|
||||
});
|
||||
|
||||
Then('je peux voir le QR code', async function (this: FestipodWorld) {
|
||||
const source = this.getRenderedText();
|
||||
const doc = this.renderedDoc;
|
||||
expect(doc, 'Screen should be rendered').to.not.be.null;
|
||||
const text = doc!.body.textContent ?? '';
|
||||
if (this.currentScreenId === 'share-profile') {
|
||||
expect(/QR Code/.test(source), 'Share profile should have "QR Code" text').to.be.true;
|
||||
expect(/Scannez pour me retrouver/.test(source), 'Share profile should have "Scannez pour me retrouver" text').to.be.true;
|
||||
expect(text.includes('Scannez pour me retrouver'),
|
||||
'Share profile should invite to scan for follow-up').to.be.true;
|
||||
} else if (this.currentScreenId === 'meeting-points') {
|
||||
expect(/Mon QR Code/.test(source), 'Meeting points should have "Mon QR Code" text').to.be.true;
|
||||
expect(/Scannez pour m'ajouter/.test(source), 'Meeting points should have "Scannez pour m\'ajouter" text').to.be.true;
|
||||
expect(text.includes('QR') || text.includes('Scannez'),
|
||||
'Meeting points should reference a QR code').to.be.true;
|
||||
} else {
|
||||
expect.fail(`QR code should be on share-profile or meeting-points, not "${this.currentScreenId}"`);
|
||||
}
|
||||
@@ -81,7 +87,26 @@ Then('je peux voir le QR code', async function (this: FestipodWorld) {
|
||||
|
||||
Then('je peux voir le lien de partage', async function (this: FestipodWorld) {
|
||||
expect(this.currentScreenId, 'Share link should be on share-profile screen').to.equal('share-profile');
|
||||
const source = this.getRenderedText();
|
||||
expect(/Mon lien de profil/.test(source), 'Share profile should have "Mon lien de profil" text').to.be.true;
|
||||
expect(/festipod\.app\/u\//.test(source), 'Share profile should have profile link URL').to.be.true;
|
||||
const doc = this.renderedDoc;
|
||||
expect(doc, 'Screen should be rendered').to.not.be.null;
|
||||
const text = doc!.body.textContent ?? '';
|
||||
expect(text.includes('Mon lien de profil'),
|
||||
'Share profile should label the profile link section').to.be.true;
|
||||
expect(/festipod\.app\/u\//.test(text),
|
||||
'Share profile should display a festipod.app/u/… link').to.be.true;
|
||||
});
|
||||
|
||||
function expectProfileDom(
|
||||
world: FestipodWorld,
|
||||
expected: { initials: string; name: string; username: string },
|
||||
) {
|
||||
const doc = world.renderedDoc;
|
||||
expect(doc, 'Screen should be rendered').to.not.be.null;
|
||||
const text = doc!.body.textContent ?? '';
|
||||
expect(text.includes(expected.name), `Profile should display "${expected.name}"`).to.be.true;
|
||||
expect(text.includes(expected.username), `Profile should display "${expected.username}"`).to.be.true;
|
||||
const avatar = doc!.querySelector('.app-avatar');
|
||||
expect(avatar, 'Profile should render an avatar').to.not.be.null;
|
||||
expect((avatar?.textContent ?? '').trim(), `Avatar should show initials "${expected.initials}"`)
|
||||
.to.equal(expected.initials);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user