Agrégateur de produits québécois — www.fabri-ka.com
- 385commits
- 1branches
- 0releases
- 89.9 MBon disk
- maindefault branch
- 4 days agolast push
1import { useEffect, useState } from 'react'2import { Link, NavLink, Route, Routes, useLocation, useNavigate } from 'react-router-dom'3import { fetchMe, logout, type Me } from './api'4import BottomNav from './components/BottomNav'5import SearchBar from './components/SearchBar'6import { FavoritesProvider, useFavorites } from './favorites'7import { productFavItem } from './api'8import { Ico, IcoHeart } from './components/KaIcons'9import { useCurrentListing } from './fiche/current'10import GroupeKaBadge from './ka/GroupeKaBadge'11import KaFooter from './ka/KaFooter'12import About from './pages/About'13import Catalog from './pages/Catalog'14import Contact from './pages/Contact'15import Home from './pages/Home'16import Inscrire from './pages/Inscrire'17import NotFound from './pages/NotFound'18import ProductDetail from './pages/ProductDetail'19import Profile from './pages/Profile'20import Stats from './pages/Stats'21import StoreDetail from './pages/StoreDetail'22import Stores from './pages/Stores'2324const HEADER_SEARCH_ID = 'header-search-input'2526/** Bouton « Se connecter avec KA » (KA ID — SSO Groupe KA) ou avatar + menu. */27function AccountMenu() {28 const location = useLocation()29 const navigate = useNavigate()30 const [enabled, setEnabled] = useState(false)31 const [me, setMe] = useState<Me | null>(null)32 const [open, setOpen] = useState(false)3334 useEffect(() => {35 fetchMe()36 .then((r) => {37 setEnabled(r.enabled)38 setMe(r.user)39 })40 .catch(() => {})41 }, [location.pathname])4243 if (!enabled) return null4445 if (!me) {46 const next = location.pathname + location.search47 return (48 <div className="site-header-account">49 <a50 className="login-btn"51 href={`/api/auth/ka/login?next=${encodeURIComponent(next)}`}52 >53 <span className="login-ka" aria-hidden="true">KA</span>54 Se connecter avec KA55 </a>56 <a57 className="signup-link"58 href="https://www.groupe-ka.com/connexion"59 target="_blank"60 rel="noopener noreferrer"61 >62 Créer un compte63 </a>64 </div>65 )66 }6768 return (69 <div className="site-header-account">70 <button71 className="account-btn"72 onClick={() => setOpen(!open)}73 aria-expanded={open}74 aria-haspopup="menu"75 aria-label={`Compte : ${me.name || me.email}`}76 >77 {me.picture ? (78 <img src={me.picture} alt="" referrerPolicy="no-referrer" />79 ) : (80 <span className="account-initial">81 {(me.name || me.email).charAt(0).toUpperCase()}82 </span>83 )}84 </button>85 {open && (86 <>87 <div88 className="account-backdrop"89 onClick={() => setOpen(false)}90 aria-hidden="true"91 />92 <div className="account-menu" role="menu">93 <div className="account-id">94 <b>{me.name || 'Mon compte'}</b>95 <span>{me.email}</span>96 <span className="account-kaid">{me.ka_id}</span>97 <span className="account-note">98 Compte Groupe KA — le même KA-ID sur toutes les plateformes99 </span>100 </div>101 <Link102 to="/profil"103 role="menuitem"104 className="account-link"105 onClick={() => setOpen(false)}106 >107 Mon profil108 </Link>109 <a110 href="https://www.groupe-ka.com/compte"111 target="_blank"112 rel="noopener noreferrer"113 role="menuitem"114 className="account-link"115 onClick={() => setOpen(false)}116 >117 Mes favoris — Mon univers Ka ↗118 </a>119 <button120 role="menuitem"121 onClick={async () => {122 await logout()123 setMe(null)124 setOpen(false)125 navigate('/')126 }}127 >128 Se déconnecter129 </button>130 </div>131 </>132 )}133 </div>134 )135}136137/** Actions du header sur une fiche produit : favoris + partage (compactes).138 Le produit affiché est publié par la page fiche (fiche/current.ts). */139function FicheHeaderActions() {140 const p = useCurrentListing()141 const { ids, toggle } = useFavorites()142 if (!p) return null143 const fav = ids.has(p.uid)144 const share = async () => {145 const url = window.location.origin + window.location.pathname146 try {147 if (navigator.share) await navigator.share({ title: document.title, url })148 else await navigator.clipboard.writeText(url)149 } catch { /* annulé */ }150 }151 return (152 <div className="hdr-actions">153 <button type="button" className={`hdr-btn ${fav ? 'on' : ''}`} aria-pressed={fav}154 aria-label={fav ? 'Retirer des favoris' : 'Ajouter aux favoris'} onClick={() => toggle(productFavItem(p))}>155 <IcoHeart size={18} filled={fav} />156 </button>157 <button type="button" className="hdr-btn" aria-label="Partager" onClick={share}>158 <Ico name="share" size={17} />159 </button>160 </div>161 )162}163164function Header() {165 const location = useLocation()166 // fiche produit : header compact (56 px), sans badge ni recherche ; favoris + partage167 const isFiche = /^\/produits\/[^/]+$/.test(location.pathname)168169 // "/" focuses the desktop header search.170 useEffect(() => {171 function onKey(e: KeyboardEvent) {172 if (e.key !== '/' || e.metaKey || e.ctrlKey || e.altKey) return173 const target = e.target as HTMLElement | null174 const tag = target?.tagName ?? ''175 if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return176 if (target?.isContentEditable) return177 const input = document.getElementById(HEADER_SEARCH_ID)178 if (input instanceof HTMLInputElement) {179 e.preventDefault()180 input.focus()181 input.select()182 }183 }184 window.addEventListener('keydown', onKey)185 return () => window.removeEventListener('keydown', onKey)186 }, [])187188 return (189 <header className={`site-header ${isFiche ? 'header--fiche' : ''}`}>190 <div className="site-header-inner">191 <div className="header-brand">192 <Link to="/" className="logo" aria-label="Fabri-Ka — Accueil">193 Fabri<span className="logo-ka">·Ka</span>194 </Link>195 {/* badge Groupe KA — desktop et mobile (masqué < 480 px, repris196 par le pied de page KaFooter) */}197 {!isFiche && <GroupeKaBadge />}198 </div>199 {isFiche && <FicheHeaderActions />}200 <nav className="site-nav" aria-label="Navigation principale">201 <NavLink to="/produits">Produits</NavLink>202 <NavLink to="/boutiques">Boutiques</NavLink>203 <NavLink to="/stats">Stats</NavLink>204 <NavLink to="/inscrire">Inscrire mon atelier</NavLink>205 <NavLink to="/a-propos">À propos</NavLink>206 <NavLink to="/contact">Contact</NavLink>207 </nav>208 <div className="site-header-search">209 {/* remount on navigation so the field reflects the current query */}210 <SearchBar211 compact212 shortcutHint213 inputId={HEADER_SEARCH_ID}214 key={location.pathname + location.search}215 initialValue={216 location.pathname === '/produits'217 ? new URLSearchParams(location.search).get('q') ?? ''218 : ''219 }220 placeholder="Rechercher…"221 />222 </div>223 <AccountMenu />224 </div>225 </header>226 )227}228229export default function App() {230 return (231 <FavoritesProvider>232 <div className="app">233 <Header />234 <main className="site-main">235 <Routes>236 <Route path="/" element={<Home />} />237 <Route path="/produits" element={<Catalog />} />238 <Route path="/produits/:uid" element={<ProductDetail />} />239 {/* Pages programmatiques SEO — mêmes URLs que le rendu serveur */}240 <Route path="/categorie/:cslug" element={<Catalog />} />241 <Route path="/categorie/:cslug/region/:rslug" element={<Catalog />} />242 <Route path="/region/:rslug" element={<Catalog />} />243 <Route path="/boutiques" element={<Stores />} />244 <Route path="/boutiques/:id" element={<StoreDetail />} />245 <Route path="/stats" element={<Stats />} />246 <Route path="/profil" element={<Profile />} />247 <Route path="/inscrire" element={<Inscrire />} />248 <Route path="/a-propos" element={<About />} />249 <Route path="/contact" element={<Contact />} />250 <Route path="*" element={<NotFound />} />251 </Routes>252 </main>253 {/* Pied de page commun Groupe KA (ka-ui) */}254 <KaFooter siteId="fabri-ka" localLegal={[{ label: 'Documentation', href: '/doc/' }]} />255 <BottomNav />256 </div>257 </FavoritesProvider>258 )259}260