SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%

Harmonisation Groupe KA : design system ka-ui, badge + footer communs, page /contact, SSO KA ID au header

- ka-ui vendoré dans frontend/src/ka/ (tokens.css, ecosystem.json, KaFooter, GroupeKaBadge) ;
  tokens.css importé AVANT styles.css dans main.tsx
- styles.css : socle dédupliqué (palette papier/encre, grain, selection, focus
  viennent de ka/tokens.css) ; seule surcharge locale = accent CERISE Immo-Ka
  (--accent #e23744 + alias --lime/--lime-soft) ; classes métier conservées ;
  styles Profil (carte de membre KA-ID), Contact et pages légales ajoutés
- Header : badge « Un service Groupe KA » (desktop + menu mobile), bouton
  « Se connecter avec KA » + lien « Créer un compte » -> groupe-ka.com/connexion ;
  zone compte -> /profil quand connecté
- Footer local remplacé par KaFooter (siteId immo-ka, pages légales locales)
- Nouvelles routes SPA : /contact (ecosystem.json : 3 courriels + rôles,
  avertissement agrégateur, hub, écosystème), /profil, /conditions, /confidentialite
- api.ts : fetchMe/logout/User/Socials + favoris hub (fetchFavorites,
  toggleFavorite, favItemFromListing) — account.tsx et Profil.tsx enfin branchés
- seo.py : /contact ajouté aux pages statiques SSR
- index.html : title « Immo-Ka — Un service Groupe KA », theme-color encre KA
- tsconfig : exclusion du code mort non référencé (src/kamaps, pages/Category.tsx —
  dépendait de @groupe-ka/ka-maps absent de node_modules)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent 5a9aa9d

12 changed files +810 −86

modified frontend/index.html +3 −3
@@ -7,9 +7,9 @@
7 7 <head>
8 8 <meta charset="UTF-8" />
9 9 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
10 − <title>Immo-Ka — Propriétés à vendre au Québec</title>
11 − <meta name="description" content="Immo-Ka agrège les propriétés à vendre affichées par les agences de courtage immobilier du Québec (RE/MAX, Sutton, Via Capitale, Proprio Direct…), toujours à jour." />
12 − <meta name="theme-color" content="#1a1214" />
10 + <title>Immo-Ka — Un service Groupe KA</title>
11 + <meta name="description" content="Immo-Ka, un service Groupe KA, agrège les propriétés à vendre affichées par les agences de courtage immobilier du Québec (RE/MAX, Sutton, Via Capitale, Proprio Direct…), toujours à jour." />
12 + <meta name="theme-color" content="#141814" />
13 13 <meta name="mobile-web-app-capable" content="yes" />
14 14 <meta name="apple-mobile-web-app-capable" content="yes" />
15 15 <meta name="apple-mobile-web-app-title" content="Immo-Ka" />
modified frontend/src/App.tsx +98 −27
@@ -1,14 +1,21 @@
1 1 // -----------------------------------------------------------------------------
2 2 // Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)
3 3 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 −// App.tsx : layout global (header + ticker en direct + footer encre) et routage
4 +// App.tsx : layout global (header + ticker en direct + footer Groupe KA) et
5 +// routage. Connexion via KA ID (hub groupe-ka.com) — voir account.tsx.
5 6 // -----------------------------------------------------------------------------
6 7 import { Ico } from "./components/Icons";
7 8 import { useEffect, useState } from "react";
8 9 import { NavLink, Route, Routes, useLocation } from "react-router-dom";
9 10 import { fetchFacets, fetchSources, fetchStats, registerSourceNames, sourceName } from "./api";
11 +import { kaLogin, useAccount } from "./account";
12 +import GroupeKaBadge from "./ka/GroupeKaBadge";
13 +import KaFooter from "./ka/KaFooter";
14 +import ContactPage from "./pages/Contact";
10 15 import Home from "./pages/Home";
16 +import { PrivacyPage, TermsPage } from "./pages/Legal";
11 17 import ListingPage from "./pages/Listing";
18 +import ProfilPage from "./pages/Profil";
12 19 import SourcesPage from "./pages/Sources";
13 20 import StatsPage from "./pages/Stats";
14 21
@@ -48,8 +55,75 @@ const NAV_LINKS = [
48 55 { to: "/?view=carte", label: "Carte", icon: "map", end: false, force: true },
49 56 { to: "/stats", label: "Stats", icon: "chart", end: false },
50 57 { to: "/agences", label: "Agences", icon: "building", end: false },
58 + { to: "/contact", label: "Contact", icon: "arrow", end: false },
51 59 ];
52 60
61 +/** Zone compte du header : « Se connecter avec KA » ou lien vers le profil. */
62 +function AccountNav() {
63 + const { user, enabled, loaded } = useAccount();
64 + if (!loaded || !enabled) return null;
65 + if (user) {
66 + return (
67 + <NavLink
68 + to="/profil"
69 + className={({ isActive }) => `ka-acct ${isActive ? "active" : ""}`}
70 + >
71 + {user.picture ? (
72 + <img className="ka-acct-pic" src={user.picture} alt="" referrerPolicy="no-referrer" />
73 + ) : (
74 + <Ico name="people" size={16} />
75 + )}
76 + {user.name ? user.name.split(" ")[0] : "Profil"}
77 + </NavLink>
78 + );
79 + }
80 + return (
81 + <div className="ka-auth">
82 + <button className="ka-login" onClick={() => kaLogin()}>
83 + Se connecter avec <b>KA</b>
84 + </button>
85 + <a
86 + className="ka-signup"
87 + href="https://www.groupe-ka.com/connexion"
88 + target="_blank"
89 + rel="noopener noreferrer"
90 + >
91 + Créer un compte
92 + </a>
93 + </div>
94 + );
95 +}
96 +
97 +/** Même zone compte, version menu mobile. */
98 +function AccountMobile({ close }: { close: () => void }) {
99 + const { user, enabled, loaded } = useAccount();
100 + if (!loaded || !enabled) return null;
101 + if (user) {
102 + return (
103 + <NavLink to="/profil" className="mm-link" onClick={close}>
104 + <span className="mm-ico" aria-hidden="true"><Ico name="people" size={19} /></span>
105 + Mon profil {user.name ? `— ${user.name.split(" ")[0]}` : ""}
106 + <span className="mm-arrow" aria-hidden="true"><Ico name="arrow" size={15} /></span>
107 + </NavLink>
108 + );
109 + }
110 + return (
111 + <div className="mm-auth">
112 + <button className="ka-login" onClick={() => kaLogin()}>
113 + Se connecter avec <b>KA</b>
114 + </button>
115 + <a
116 + className="ka-signup"
117 + href="https://www.groupe-ka.com/connexion"
118 + target="_blank"
119 + rel="noopener noreferrer"
120 + >
121 + Créer un compte
122 + </a>
123 + </div>
124 + );
125 +}
126 +
53 127 function Header() {
54 128 const [open, setOpen] = useState(false);
55 129 const location = useLocation();
@@ -66,8 +140,8 @@ function Header() {
66 140 <div className="container header-inner">
67 141 <NavLink to="/" className="brand" aria-label="Immo-Ka — accueil">
68 142 Immo<span className="ka">Ka</span>
69 − <span className="brand-tag">Toutes les agences du Québec — toujours à jour</span>
70 143 </NavLink>
144 + <GroupeKaBadge />
71 145 <nav className="nav" aria-label="Navigation principale">
72 146 <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>
73 147 Propriétés
@@ -78,7 +152,13 @@ function Header() {
78 152 <NavLink to="/agences" className={({ isActive }) => (isActive ? "active" : "")}>
79 153 Agences
80 154 </NavLink>
155 + <NavLink to="/contact" className={({ isActive }) => (isActive ? "active" : "")}>
156 + Contact
157 + </NavLink>
81 158 </nav>
159 + <div className="header-acct">
160 + <AccountNav />
161 + </div>
82 162 <button
83 163 className={`menu-btn ${open ? "open" : ""}`}
84 164 aria-expanded={open}
@@ -104,9 +184,13 @@ function Header() {
104 184 <span className="mm-arrow" aria-hidden="true"><Ico name="arrow" size={15} /></span>
105 185 </NavLink>
106 186 ))}
187 + <AccountMobile close={() => setOpen(false)} />
107 188 <div className="mm-foot">
108 − Agrégateur indépendant — mis à jour automatiquement, chaque fiche
109 − renvoie à l'annonce originale de l'agence.
189 + <GroupeKaBadge />
190 + <p>
191 + Agrégateur indépendant — mis à jour automatiquement, chaque fiche
192 + renvoie à l'annonce originale de l'agence.
193 + </p>
110 194 </div>
111 195 </div>
112 196 </header>
@@ -116,28 +200,11 @@ function Header() {
116 200 );
117 201 }
118 202
119 −function Footer() {
120 − return (
121 − <footer className="footer">
122 − <div className="container">
123 − <div className="fbrand">
124 − Immo<span className="ka">Ka</span>
125 − </div>
126 − <div className="frow">
127 − <div>
128 − <b>Agrégateur indépendant</b> des propriétés à vendre dans la province de Québec.
129 − Les annonces proviennent des sites publics des agences de courtage (RE/MAX, Sutton,
130 − Via Capitale, Proprio Direct, Barnes, M Immobilier…) et sont rafraîchies
131 − automatiquement — chaque fiche renvoie vers l'annonce originale.
132 − </div>
133 − </div>
134 − <div className="fmono">
135 − © {new Date().getFullYear()} Simon-Pierre Boucher — contact@spboucher.ai
136 − </div>
137 − </div>
138 − </footer>
139 − );
140 −}
203 +const LOCAL_LEGAL = [
204 + { label: "Conditions (Immo-Ka)", href: "/conditions" },
205 + { label: "Confidentialité (Immo-Ka)", href: "/confidentialite" },
206 + { label: "Contact", href: "/contact" },
207 +];
141 208
142 209 export default function App() {
143 210 return (
@@ -149,6 +216,10 @@ export default function App() {
149 216 <Route path="/propriete/:uid" element={<ListingPage />} />
150 217 <Route path="/stats" element={<StatsPage />} />
151 218 <Route path="/agences" element={<SourcesPage />} />
219 + <Route path="/contact" element={<ContactPage />} />
220 + <Route path="/profil" element={<ProfilPage />} />
221 + <Route path="/conditions" element={<TermsPage />} />
222 + <Route path="/confidentialite" element={<PrivacyPage />} />
152 223 <Route
153 224 path="*"
154 225 element={
@@ -161,7 +232,7 @@ export default function App() {
161 232 />
162 233 </Routes>
163 234 </main>
164 − <Footer />
235 + <KaFooter siteId="immo-ka" localLegal={LOCAL_LEGAL} />
165 236 </>
166 237 );
167 238 }
modified frontend/src/api.ts +70 −0
@@ -219,3 +219,73 @@ export interface Quartier {
219 219 | { type: "points"; rayon_m: number; douze_mois: number; douze_mois_precedents: number }
220 220 | { type: "igc"; ville: string; annee: number; indice: number; indice_canada: number | null };
221 221 }
222 +
223 +// -----------------------------------------------------------------------------
224 +// Compte KA ID (SSO hub groupe-ka.com) + favoris « Mon univers Ka »
225 +// -----------------------------------------------------------------------------
226 +export type Socials = Partial<Record<
227 + "instagram" | "facebook" | "x" | "linkedin" | "tiktok" | "youtube", string
228 +>>;
229 +
230 +export interface User {
231 + sub: string;
232 + email: string;
233 + name: string;
234 + picture?: string;
235 + ka_id?: string;
236 + provider?: string; // "ka-id" | "google"
237 + created_at?: number | null; // epoch (s)
238 + last_login?: number | null; // epoch (s)
239 + // profil enrichi par le HUB Groupe KA (source de vérité — groupe-ka.com/compte)
240 + bio?: string;
241 + city?: string;
242 + phone?: string;
243 + website?: string;
244 + socials?: Socials;
245 + public?: boolean;
246 + role_label?: string;
247 + job_title?: string;
248 + company?: string;
249 + age?: number | null;
250 + public_url?: string;
251 + profile_source?: string; // "groupe-ka" | "local"
252 +}
253 +
254 +/** Profil de session ({user: null} si non connecté ; `enabled` = SSO configuré). */
255 +export const fetchMe = () => get<{ user: User | null; enabled: boolean }>("/api/auth/me");
256 +
257 +export const logout = () => fetch("/api/auth/logout", { method: "POST" });
258 +
259 +/** Item favori tel qu'attendu par le hub (voir immoka/favorites.py). */
260 +export interface FavItem {
261 + item_id: string;
262 + title: string;
263 + subtitle: string;
264 + price_label: string;
265 + image_url: string;
266 + url: string;
267 +}
268 +
269 +export function favItemFromListing(l: Listing): FavItem {
270 + return {
271 + item_id: l.uid,
272 + title: l.title || l.address || l.property_type || "Propriété",
273 + subtitle: [l.city, l.region].filter(Boolean).join(", "),
274 + price_label: l.price_label || fmtPrice(l.price),
275 + image_url: l.images?.[0] ?? "",
276 + url: `/propriete/${l.uid}`,
277 + };
278 +}
279 +
280 +export const fetchFavorites = () =>
281 + get<{ ids: string[]; items: FavItem[] }>("/api/favorites");
282 +
283 +export async function toggleFavorite(on: boolean, item: FavItem) {
284 + const res = await fetch("/api/favorites/toggle", {
285 + method: "POST",
286 + headers: { "Content-Type": "application/json" },
287 + body: JSON.stringify({ on, item }),
288 + });
289 + if (!res.ok) throw new Error(`favoris ${res.status}`);
290 + return (await res.json()) as { ok: boolean; on: boolean };
291 +}
added frontend/src/ka/GroupeKaBadge.tsx +21 −0
@@ -0,0 +1,21 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// ka-ui/react/GroupeKaBadge.tsx — badge « Un service Groupe KA », cliquable
3 +// vers le hub. À placer dans le header de chaque site (à côté du logo de la
4 +// marque). Variante dark pour les surfaces encre. Zone de protection : le
5 +// padding interne du badge suffit ; ne rien coller à moins de 8 px du badge.
6 +export default function GroupeKaBadge({ dark = false }: { dark?: boolean }) {
7 + return (
8 + <a
9 + className={`gk-badge${dark ? " gk-badge--dark" : ""}`}
10 + href="https://www.groupe-ka.com"
11 + target="_blank"
12 + rel="noopener noreferrer"
13 + aria-label="Un service Groupe KA — visiter le portail de l'écosystème"
14 + >
15 + Un service
16 + <b>
17 + Groupe<span className="ka">KA</span>
18 + </b>
19 + </a>
20 + );
21 +}
added frontend/src/ka/KaFooter.tsx +91 −0
@@ -0,0 +1,91 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// ka-ui/react/KaFooter.tsx — footer commun Groupe KA (fond encre), à vendorer
3 +// dans chaque app Vite/React sous frontend/src/ka/. Consomme ecosystem.json :
4 +// toute modification des infos de contact se fait dans ka-ui/ecosystem.json
5 +// puis se resynchronise sur les 12 sites (voir ka-ui/README.md).
6 +import eco from "./ecosystem.json";
7 +
8 +type LocalLink = { label: string; href: string };
9 +
10 +export default function KaFooter({
11 + siteId,
12 + localLegal = [],
13 +}: {
14 + /** id du site courant dans ecosystem.json (ex. "resto-ka") */
15 + siteId: string;
16 + /** pages légales PROPRES au site (ex. /conditions), affichées avant celles du hub */
17 + localLegal?: LocalLink[];
18 +}) {
19 + const year = new Date().getFullYear();
20 + const site = eco.sites.find((s) => s.id === siteId);
21 + const others = eco.sites.filter((s) => s.id !== siteId);
22 + return (
23 + <footer className="ka-footer" id="contact">
24 + <div className="container">
25 + <a
26 + className="wordmark"
27 + href={eco.hub.url}
28 + target={siteId === "groupe-ka" ? undefined : "_blank"}
29 + rel="noopener noreferrer"
30 + aria-label="Groupe KA — le portail de l'écosystème"
31 + >
32 + Groupe <span className="ka">KA</span>
33 + </a>
34 + <p className="desc">
35 + {eco.org.tagline} Des centaines de connecteurs lisent les sites à la
36 + source, normalisent la donnée et se resynchronisent seuls.{" "}
37 + {site ? (
38 + <>
39 + <b style={{ color: "var(--paper)" }}>{site.wordmark}</b> —{" "}
40 + {site.tagline} — est un service Groupe KA.
41 + </>
42 + ) : null}
43 + </p>
44 + <p className="notice">
45 + <b>{eco.org.disclaimer}</b>
46 + </p>
47 + <ul className="sites">
48 + {others.map((s) => (
49 + <li key={s.id}>
50 + <a href={`https://${s.domain}`} target="_blank" rel="noopener noreferrer">
51 + {s.wordmark}
52 + </a>
53 + </li>
54 + ))}
55 + {eco.extraFooterLinks.map((l) => (
56 + <li key={l.href}>
57 + <a href={l.href} target="_blank" rel="noopener noreferrer">
58 + {l.label}
59 + </a>
60 + </li>
61 + ))}
62 + </ul>
63 + <div className="contacts">
64 + {eco.contacts.map((c) => (
65 + <p key={c.email} style={{ margin: 0 }}>
66 + <a href={`mailto:${c.email}`}>{c.email}</a>
67 + <span>{c.role}</span>
68 + </p>
69 + ))}
70 + </div>
71 + <p className="legal">
72 + © {year} {eco.org.copyrightHolder}
73 + {localLegal.map((l) => (
74 + <span key={l.href}>
75 + {" · "}
76 + <a href={l.href}>{l.label}</a>
77 + </span>
78 + ))}
79 + {eco.legal.map((l) => (
80 + <span key={l.href}>
81 + {" · "}
82 + <a href={l.href} target="_blank" rel="noopener noreferrer">
83 + {l.label}
84 + </a>
85 + </span>
86 + ))}
87 + </p>
88 + </div>
89 + </footer>
90 + );
91 +}
added frontend/src/ka/ecosystem.json +45 −0
@@ -0,0 +1,45 @@
1 +{
2 + "org": {
3 + "name": "Groupe KA",
4 + "legalName": "Groupe KA — Simon-Pierre Boucher",
5 + "tagline": "Holding québécois d'agrégateurs de produits et services entièrement automatisés.",
6 + "disclaimer": "Groupe KA est un agrégateur de contenu : nous ne vendons rien, ne louons rien et ne sommes partie à aucune transaction.",
7 + "copyrightHolder": "Groupe KA — Simon-Pierre Boucher"
8 + },
9 + "hub": {
10 + "url": "https://www.groupe-ka.com",
11 + "loginPath": "/connexion",
12 + "signupNote": "La création de compte KA ID se fait sur le hub groupe-ka.com ; chaque site délègue sa connexion via /api/auth/ka/login."
13 + },
14 + "contacts": [
15 + { "email": "contact@groupe-ka.com", "role": "Projets, partenariats & données" },
16 + { "email": "info@groupe-ka.com", "role": "Médias & questions générales" },
17 + { "email": "admin@groupe-ka.com", "role": "Légal, vie privée & Loi 25" }
18 + ],
19 + "legal": [
20 + { "label": "Conditions d'utilisation", "href": "https://www.groupe-ka.com/conditions" },
21 + { "label": "Politique de confidentialité", "href": "https://www.groupe-ka.com/confidentialite" },
22 + { "label": "Protection des renseignements personnels (Loi 25)", "href": "https://www.groupe-ka.com/loi-25" },
23 + { "label": "Transparence des robots d'indexation", "href": "https://www.groupe-ka.com/bots" }
24 + ],
25 + "sites": [
26 + { "id": "groupe-ka", "wordmark": "Groupe KA", "domain": "www.groupe-ka.com", "accent": "#d9f26b", "accentSoft": "#f0f9d2", "accentDeep": "#123f2e", "onAccent": "#141814", "tagline": "Le portail de l'écosystème ·Ka" },
27 + { "id": "trouve-ka", "wordmark": "Trouve·Ka", "domain": "www.trouve-ka.com", "accent": "#1c7ed6", "accentSoft": "#e7f2fd", "accentDeep": "#14508f", "onAccent": "#ffffff", "tagline": "Le moteur de recherche du web québécois" },
28 + { "id": "lou-ka", "wordmark": "Lou·Ka", "domain": "www.lou-ka.com", "accent": "#ff6a00", "accentSoft": "#fff1e6", "accentDeep": "#cc5500", "onAccent": "#ffffff", "tagline": "Tous les logements à louer" },
29 + { "id": "immo-ka", "wordmark": "Immo·Ka", "domain": "www.immo-ka.com", "accent": "#e23744", "accentSoft": "#fbe0e2", "accentDeep": "#a8232e", "onAccent": "#ffffff", "tagline": "Toutes les propriétés à vendre" },
30 + { "id": "vrai-prix", "wordmark": "Vrai-Prix", "domain": "www.vrai-prix.com", "accent": "#ff5148", "accentSoft": "#ffe3e0", "accentDeep": "#9e2a25", "onAccent": "#ffffff", "tagline": "La valeur réelle de chaque propriété" },
31 + { "id": "auto-ka", "wordmark": "Auto·Ka", "domain": "www.auto-ka.com", "accent": "#ff5a2a", "accentSoft": "#ffe8de", "accentDeep": "#cc3f16", "onAccent": "#ffffff", "tagline": "Les voitures usagées du Québec" },
32 + { "id": "fabri-ka", "wordmark": "Fabri·Ka", "domain": "www.fabri-ka.com", "accent": "#c4532e", "accentSoft": "#f7e3da", "accentDeep": "#a94525", "onAccent": "#ffffff", "tagline": "Les produits fabriqués au Québec" },
33 + { "id": "food-ka", "wordmark": "Food·Ka", "domain": "www.food-ka.com", "accent": "#1f9d55", "accentSoft": "#e2f5ea", "accentDeep": "#157a40", "onAccent": "#ffffff", "tagline": "Les prix d'épicerie, suivis à la source" },
34 + { "id": "resto-ka", "wordmark": "Resto·Ka", "domain": "www.resto-ka.com", "accent": "#f08c00", "accentSoft": "#fdeed7", "accentDeep": "#b96a00", "onAccent": "#141814", "tagline": "Chaque resto, chaque plat, chaque prix" },
35 + { "id": "sorti-ka", "wordmark": "Sorti·Ka", "domain": "www.sorti-ka.com", "accent": "#d6336c", "accentSoft": "#fbe0eb", "accentDeep": "#a12551", "onAccent": "#ffffff", "tagline": "Toutes les sorties, dans les 17 régions" },
36 + { "id": "crea-ka", "wordmark": "Créa·Ka", "domain": "www.crea-ka.com", "accent": "#7048e8", "accentSoft": "#ece5fc", "accentDeep": "#5433b8", "onAccent": "#ffffff", "tagline": "Les créateurs d'ici, tous leurs liens" },
37 + { "id": "api-ka", "wordmark": "API·Ka", "domain": "www.api-ka.com", "accent": "#3b5bdb", "accentSoft": "#e4eafb", "accentDeep": "#2b44a8", "onAccent": "#ffffff", "tagline": "La donnée de l'écosystème, par API" }
38 + ],
39 + "extraFooterLinks": [
40 + { "label": "ValoPlex", "href": "https://www.valoplex.com" },
41 + { "label": "Ka2", "href": "https://www.ka2.bot" },
42 + { "label": "Ka4", "href": "https://www.ka4.bot" },
43 + { "label": "Ka6", "href": "https://www.ka6.bot" }
44 + ]
45 +}
added frontend/src/ka/tokens.css +229 −0
@@ -0,0 +1,229 @@
1 +/* -----------------------------------------------------------------------------
2 + Auteur : Simon-Pierre Boucher — contact@spboucher.ai
3 + Fichier : ka-ui/tokens.css — SOURCE CANONIQUE (repo ka-ui sur spbgit)
4 + Desc. : Design system commun GROUPE KA « éditorial sharp » — tokens + socle
5 + de composants partagés par les 12 plateformes. Chaque site importe ce
6 + fichier PUIS surcharge uniquement son accent (voir accents.css).
7 +
8 + · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
9 + · Palette: papier #f5f3ee · encre #141814 · vert profond #1c5c41 + ACCENT du site
10 + · Signature : bordures encre 1,5–2 px + ombres décalées dures, grain de film,
11 + surlignés d'accent inclinés (néo-brutalisme raffiné)
12 + · Breakpoints communs : 360 / 768 / 1024 / 1440 px (mobile-first)
13 + · Zones tactiles ≥ 44×44 px, safe-areas iOS, typographie fluide (clamp)
14 +----------------------------------------------------------------------------- */
15 +
16 +:root {
17 + /* ---- Palette fixe Groupe KA (identique sur les 12 sites) ---- */
18 + --paper: #f5f3ee;
19 + --surface: #ffffff;
20 + --surface-2: #faf9f5;
21 + --ink: #141814;
22 + --ink-2: #4d5551;
23 + --ink-3: #8b928c;
24 + --line: rgba(20, 24, 20, 0.14);
25 + --line-strong: rgba(20, 24, 20, 0.85);
26 + --green: #1c5c41;
27 + --green-deep: #123f2e;
28 + --amber: #e8a33d;
29 + --amber-soft: #fdf3e2;
30 + --danger: #b3423a;
31 + --danger-soft: #fbe9e7;
32 +
33 + /* ---- ACCENT du site — SEULES variables surchargées par marque ---- */
34 + --accent: #d9f26b; /* défaut : lime Groupe KA */
35 + --accent-soft: #f0f9d2;
36 + --accent-deep: #123f2e;
37 + --on-accent: var(--ink); /* couleur du texte posé SUR l'accent */
38 +
39 + /* alias rétro-compatibles (les satellites historiques utilisent --lime) */
40 + --lime: var(--accent);
41 + --lime-soft: var(--accent-soft);
42 +
43 + /* ---- Géométrie sharp ---- */
44 + --r-card: 10px;
45 + --r-ctl: 6px;
46 + --r-pill: 999px;
47 +
48 + /* ---- Ombres décalées — la signature du groupe ---- */
49 + --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05);
50 + --shadow-off: 6px 6px 0 var(--ink);
51 + --shadow-off-soft: 8px 8px 0 rgba(20, 24, 20, 0.08);
52 + --shadow-off-mid: 4px 4px 0 rgba(20, 24, 20, 0.18);
53 +
54 + /* ---- Typographie ---- */
55 + --font-display: "Space Grotesk", system-ui, sans-serif;
56 + --font-body: "Inter", system-ui, sans-serif;
57 + --font-mono: "JetBrains Mono", ui-monospace, monospace;
58 +
59 + /* Échelle fluide (mobile-first, clamp) */
60 + --fs-h1: clamp(30px, 3.2vw + 18px, 54px);
61 + --fs-h2: clamp(23px, 1.8vw + 14px, 34px);
62 + --fs-h3: clamp(17px, 0.9vw + 12px, 22px);
63 + --fs-body: 15px;
64 + --fs-small: 13px;
65 +
66 + /* Espacements */
67 + --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
68 + --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
69 +
70 + /* Cible tactile minimale */
71 + --touch: 44px;
72 +}
73 +
74 +/* ---- Socle ---- */
75 +* { box-sizing: border-box; }
76 +html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
77 +body {
78 + margin: 0;
79 + background: var(--paper);
80 + color: var(--ink);
81 + font-family: var(--font-body);
82 + font-size: var(--fs-body);
83 + line-height: 1.55;
84 + -webkit-font-smoothing: antialiased;
85 + padding-bottom: env(safe-area-inset-bottom);
86 + overflow-x: clip; /* jamais de débordement horizontal */
87 +}
88 +img, svg, video { max-width: 100%; height: auto; display: block; }
89 +h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.03em; margin: 0 0 0.5em; }
90 +h1 { font-size: var(--fs-h1); line-height: 1.06; }
91 +h2 { font-size: var(--fs-h2); line-height: 1.15; }
92 +h3 { font-size: var(--fs-h3); line-height: 1.25; }
93 +a { color: inherit; }
94 +::selection { background: var(--accent); color: var(--on-accent); }
95 +:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
96 +
97 +/* Grain de film pleine page — ⚠️ jamais de z-index sur body > * (position:
98 + relative seulement), sinon les utilitaires z-* sont écrasés. */
99 +body::before {
100 + content: "";
101 + position: fixed; inset: 0; pointer-events: none; opacity: 0.35; z-index: 9999;
102 + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
103 +}
104 +body > * { position: relative; }
105 +
106 +/* ---- Conteneur commun ---- */
107 +.container { max-width: 1152px; margin: 0 auto; padding: 0 var(--sp-4); }
108 +@media (min-width: 768px) { .container { padding: 0 var(--sp-5); } }
109 +
110 +/* ---- Micro-typographie signature ---- */
111 +.kicker {
112 + display: inline-flex; align-items: center; gap: 10px;
113 + font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
114 + text-transform: uppercase; letter-spacing: 0.12em; color: var(--green);
115 +}
116 +.kicker::before { content: ""; width: 22px; height: 2px; background: var(--green); }
117 +.klabel {
118 + font-family: var(--font-mono); font-size: 10px; font-weight: 700;
119 + text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);
120 +}
121 +.hl {
122 + display: inline-block; background: var(--accent); color: var(--on-accent);
123 + border-radius: 8px; padding: 0 10px 2px; transform: rotate(-1deg);
124 +}
125 +.outline-txt { color: transparent; -webkit-text-stroke: 2px var(--ink); }
126 +
127 +/* ---- Boutons (cible ≥ 44 px) ---- */
128 +.btn {
129 + display: inline-flex; align-items: center; justify-content: center; gap: 8px;
130 + min-height: var(--touch); padding: 10px 18px;
131 + font-family: var(--font-display); font-weight: 700; font-size: 14px;
132 + border: 1.5px solid var(--ink); border-radius: var(--r-ctl);
133 + background: var(--surface); color: var(--ink);
134 + cursor: pointer; text-decoration: none;
135 + transition: transform 0.15s, box-shadow 0.15s, background 0.15s, color 0.15s;
136 +}
137 +.btn:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off-mid); }
138 +.btn:active { transform: translate(0, 0); box-shadow: none; }
139 +.btn:disabled { opacity: 0.45; pointer-events: none; }
140 +.btn-primary { background: var(--ink); color: var(--accent); }
141 +.btn-primary:hover { background: var(--accent-deep); }
142 +.btn-accent { background: var(--accent); color: var(--on-accent); }
143 +.btn-ghost { background: transparent; border-color: var(--line); }
144 +.btn-ghost:hover { border-color: var(--ink); }
145 +
146 +/* ---- Cartes ---- */
147 +.card {
148 + background: var(--surface); border: 1.5px solid var(--ink);
149 + border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);
150 + overflow: hidden;
151 +}
152 +.card-hover { transition: transform 0.15s, box-shadow 0.15s; }
153 +.card-hover:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }
154 +
155 +/* ---- Chips / badges ---- */
156 +.chip {
157 + display: inline-flex; align-items: center; gap: 6px;
158 + padding: 4px 11px; font-family: var(--font-mono); font-size: 11px; font-weight: 700;
159 + text-transform: uppercase; letter-spacing: 0.06em;
160 + border: 1.5px solid var(--ink); border-radius: var(--r-pill);
161 + background: var(--surface); color: var(--ink);
162 +}
163 +.chip-accent { background: var(--accent); color: var(--on-accent); }
164 +.chip-soft { background: var(--accent-soft); border-color: var(--line); }
165 +
166 +/* ---- Champs ---- */
167 +.input, .select, .textarea {
168 + width: 100%; min-height: var(--touch); padding: 10px 14px;
169 + font: inherit; color: var(--ink); background: var(--surface);
170 + border: 1.5px solid var(--ink); border-radius: var(--r-ctl);
171 +}
172 +.input:focus, .select:focus, .textarea:focus {
173 + outline: none; box-shadow: 3px 3px 0 var(--accent); border-color: var(--ink);
174 +}
175 +.input::placeholder { color: var(--ink-3); }
176 +
177 +/* ---- Badge « Un service Groupe KA » (header, cliquable → hub) ---- */
178 +.gk-badge {
179 + display: inline-flex; align-items: center; gap: 7px;
180 + min-height: 30px; padding: 3px 10px 4px;
181 + font-family: var(--font-mono); font-size: 10px; font-weight: 700;
182 + letter-spacing: 0.08em; text-transform: uppercase; text-decoration: none;
183 + border: 1.5px solid var(--ink); border-radius: var(--r-pill);
184 + background: var(--surface); color: var(--ink-2); white-space: nowrap;
185 +}
186 +.gk-badge b {
187 + font-family: var(--font-display); font-size: 12px; letter-spacing: -0.02em;
188 + text-transform: none; color: var(--ink);
189 +}
190 +.gk-badge b .ka {
191 + display: inline-block; background: var(--ink); color: var(--accent);
192 + border-radius: 5px; padding: 0 5px 1px; margin-left: 3px; transform: rotate(-2deg);
193 +}
194 +.gk-badge:hover .ka { transform: rotate(0); }
195 +.gk-badge--dark { background: transparent; border-color: rgba(245,243,238,0.4); color: rgba(245,243,238,0.75); }
196 +.gk-badge--dark b { color: var(--paper); }
197 +
198 +/* ---- Footer commun (fond encre — voir react/KaFooter.tsx) ---- */
199 +.ka-footer { margin-top: var(--sp-8); background: var(--ink); padding: 44px 0; font-size: 13px; color: rgba(245,243,238,0.75); }
200 +.ka-footer a { text-decoration: none; }
201 +.ka-footer .wordmark { font-family: var(--font-display); font-weight: 700; font-size: 30px; letter-spacing: -0.04em; color: var(--paper); text-decoration: none; }
202 +.ka-footer .wordmark .ka { color: var(--accent); }
203 +.ka-footer .desc { max-width: 640px; margin-top: var(--sp-4); }
204 +.ka-footer .notice { max-width: 640px; margin-top: var(--sp-4); border-left: 2px solid var(--accent); padding-left: var(--sp-4); }
205 +.ka-footer .notice b { color: var(--paper); }
206 +.ka-footer .sites { list-style: none; display: flex; flex-wrap: wrap; gap: 10px 26px; margin: 26px 0 0; padding: 0; font-family: var(--font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
207 +.ka-footer .sites a { color: rgba(245,243,238,0.65); }
208 +.ka-footer .sites a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; }
209 +.ka-footer .contacts { display: grid; gap: 14px 32px; border-top: 1px solid rgba(245,243,238,0.15); margin-top: 30px; padding-top: 26px; }
210 +@media (min-width: 768px) { .ka-footer .contacts { grid-template-columns: repeat(3, 1fr); } }
211 +.ka-footer .contacts a { font-family: var(--font-mono); font-weight: 700; font-size: 12px; color: rgba(245,243,238,0.85); }
212 +.ka-footer .contacts a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; }
213 +.ka-footer .contacts span { display: block; margin-top: 3px; font-size: 11px; color: rgba(245,243,238,0.5); }
214 +.ka-footer .legal { margin-top: 30px; font-family: var(--font-mono); font-size: 11px; color: rgba(245,243,238,0.45); }
215 +.ka-footer .legal a { color: inherit; }
216 +.ka-footer .legal a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; }
217 +
218 +/* ---- Tableaux → cartes empilées sous 768 px ---- */
219 +.tbl-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
220 +@media (max-width: 767px) {
221 + .tbl-stack thead { display: none; }
222 + .tbl-stack tr { display: block; border: 1.5px solid var(--ink); border-radius: var(--r-card); margin-bottom: var(--sp-3); background: var(--surface); }
223 + .tbl-stack td { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 8px 12px; border: 0; }
224 + .tbl-stack td::before { content: attr(data-label); font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); }
225 +}
226 +
227 +/* ---- Utilitaires responsive ---- */
228 +.only-mobile { display: initial; } .only-desktop { display: none; }
229 +@media (min-width: 768px) { .only-mobile { display: none; } .only-desktop { display: initial; } }
modified frontend/src/main.tsx +7 −2
@@ -1,18 +1,23 @@
1 1 // -----------------------------------------------------------------------------
2 2 // Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)
3 3 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 −// main.tsx : point d'entrée React
4 +// main.tsx : point d'entrée React — tokens Groupe KA (ka/tokens.css) AVANT le
5 +// CSS local, puis contexte de compte KA ID (AccountProvider).
5 6 // -----------------------------------------------------------------------------
6 7 import React from "react";
7 8 import ReactDOM from "react-dom/client";
8 9 import { BrowserRouter } from "react-router-dom";
9 10 import App from "./App";
11 +import { AccountProvider } from "./account";
12 +import "./ka/tokens.css";
10 13 import "./styles.css";
11 14
12 15 ReactDOM.createRoot(document.getElementById("root")!).render(
13 16 <React.StrictMode>
14 17 <BrowserRouter>
15 − <App />
18 + <AccountProvider>
19 + <App />
20 + </AccountProvider>
16 21 </BrowserRouter>
17 22 </React.StrictMode>
18 23 );
added frontend/src/pages/Contact.tsx +71 −0
@@ -0,0 +1,71 @@
1 +// -----------------------------------------------------------------------------
2 +// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)
3 +// Une application du Groupe-Ka — contact@groupe-ka.com
4 +// pages/Contact.tsx : page contact commune Groupe KA — consomme ka/ecosystem.json
5 +// (courriels + rôles, avertissement agrégateur, hub, liste des sites).
6 +// -----------------------------------------------------------------------------
7 +import eco from "../ka/ecosystem.json";
8 +
9 +export default function ContactPage() {
10 + const sites = eco.sites.filter((s) => s.id !== "immo-ka");
11 + return (
12 + <div className="container contact">
13 + <span className="kicker">Contact</span>
14 + <h1>
15 + Écrire au <span className="hl">Groupe KA</span>
16 + </h1>
17 + <p className="lede">
18 + Immo-Ka est un service du <b>{eco.org.name}</b> — {eco.org.tagline.toLowerCase()}{" "}
19 + Toutes les plateformes partagent les mêmes canaux de contact.
20 + </p>
21 +
22 + <div className="contact-cards">
23 + {eco.contacts.map((c) => (
24 + <a key={c.email} className="contact-card" href={`mailto:${c.email}`}>
25 + <span className="contact-role">{c.role}</span>
26 + <span className="contact-mail">{c.email}</span>
27 + </a>
28 + ))}
29 + </div>
30 +
31 + <p className="contact-disclaimer">
32 + <b>{eco.org.disclaimer}</b> Chaque fiche renvoie vers l'annonce originale
33 + de l'agence ou de la plateforme source : pour une propriété précise,
34 + contactez directement le courtier indiqué sur la fiche.
35 + </p>
36 +
37 + <div className="contact-hub">
38 + <a
39 + className="btn btn-primary"
40 + href={eco.hub.url}
41 + target="_blank"
42 + rel="noopener noreferrer"
43 + >
44 + Visiter le portail groupe-ka.com ↗
45 + </a>
46 + <a
47 + className="btn btn-ghost"
48 + href={`${eco.hub.url}${eco.hub.loginPath}`}
49 + target="_blank"
50 + rel="noopener noreferrer"
51 + >
52 + Créer un compte KA ID
53 + </a>
54 + </div>
55 +
56 + <section className="contact-sites">
57 + <h2>L'écosystème ·Ka</h2>
58 + <ul>
59 + {sites.map((s) => (
60 + <li key={s.id}>
61 + <a href={`https://${s.domain}`} target="_blank" rel="noopener noreferrer">
62 + <b>{s.wordmark}</b>
63 + <span>{s.tagline}</span>
64 + </a>
65 + </li>
66 + ))}
67 + </ul>
68 + </section>
69 + </div>
70 + );
71 +}
modified frontend/src/styles.css +168 −53
@@ -1,57 +1,28 @@
1 1 /* -----------------------------------------------------------------------------
2 2 Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)
3 3 Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 − styles.css : système de design « éditorial sharp » — thème clair
5 − · Typo display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
6 − · Bordures encre + ombres décalées (néo-brutalisme raffiné)
7 − · Accent rouge vif sur encre chocolat · 100 % adaptatif mobile
4 + styles.css : classes métier Immo-Ka par-dessus le design system commun
5 + Groupe KA (ka/tokens.css, importé AVANT ce fichier dans main.tsx).
6 + · Le socle (palette papier/encre, typo, grain, selection, focus, socle
7 + de composants) vient de ka/tokens.css — NE PAS le dupliquer ici.
8 + · SEUL l'accent de marque est surchargé : CERISE Immo-Ka.
8 9 ----------------------------------------------------------------------------- */
9 10 :root {
10 − /* Palette ROUGE — distincte de Lou-Ka (lime/vert). Noms de tokens conservés :
11 − `--lime` = accent vif (rouge), `--green` = rouge profond (secondaire). */
12 − --paper: #f7f1ef;
13 − --surface: #ffffff;
14 − --surface-2: #fdf6f5;
15 − --ink: #1a1214; /* encre chocolat/near-black chaud */
16 − --ink-2: #5a4d50;
17 − --ink-3: #9b8b8e;
18 − --line: rgba(26, 18, 20, 0.14);
19 − --line-strong: rgba(26, 18, 20, 0.85);
20 − --green: #b3202b; /* rouge profond (accent secondaire / kicker) */
21 − --green-deep: #7f1620;
22 − --lime: #e23744; /* rouge vif (accent principal) */
23 − --lime-soft: #fbe0e2;
24 − --amber: #e8a33d;
25 − --amber-soft: #fdf3e2;
26 − --danger: #b3202b;
27 − --r-card: 14px;
28 − --r-ctl: 8px;
29 − --shadow-flat: 0 1px 2px rgba(26, 18, 20, 0.06);
30 − --shadow-off: 6px 6px 0 var(--ink);
31 − --shadow-off-soft: 8px 8px 0 rgba(226, 55, 68, 0.14);
32 − --font-display: "Space Grotesk", system-ui, sans-serif;
33 − --font-body: "Inter", system-ui, sans-serif;
34 − --font-mono: "JetBrains Mono", ui-monospace, monospace;
11 + /* ---- Accent CERISE Immo-Ka (seule surcharge de ka/tokens.css) ---- */
12 + --accent: #e23744;
13 + --accent-soft: #fbe0e2;
14 + --accent-deep: #a8232e;
15 + --on-accent: #ffffff;
16 + /* alias historiques — tout le CSS métier ci-dessous utilise --lime */
17 + --lime: var(--accent);
18 + --lime-soft: var(--accent-soft);
35 19 }
36 20
37 −* { box-sizing: border-box; }
38 −html { scroll-behavior: smooth; scroll-padding-top: 76px; }
39 −body {
40 − margin: 0; background: var(--paper); color: var(--ink);
41 − font-family: var(--font-body); font-size: 15px; line-height: 1.55;
42 − -webkit-font-smoothing: antialiased; padding-bottom: env(safe-area-inset-bottom);
43 −}
44 −body::before {
45 − content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0.35;
46 − background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
47 −}
48 −#root { position: relative; z-index: 1; }
49 −
50 −h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.03em; margin: 0; }
51 −a { color: inherit; text-decoration: none; }
21 +/* Ancre sous le header collant (spécifique Immo-Ka) */
22 +html { scroll-padding-top: 76px; }
23 +h1, h2, h3, h4 { margin: 0; }
24 +a { text-decoration: none; }
52 25 button { font-family: inherit; }
53 −img { display: block; }
54 −::selection { background: var(--lime); color: #fff; }
55 26
56 27 .mono { font-family: var(--font-mono); }
57 28 .kicker {
@@ -474,13 +445,9 @@ table.rooms tr:nth-child(even) td { background: var(--surface-2); }
474 445 .mm-backdrop { position: fixed; inset: 0; z-index: 40; background: rgba(26, 18, 20, 0.35); backdrop-filter: blur(2px); }
475 446 @media (max-width: 760px) { .menu-btn { display: flex; } .nav { display: none; } .mobile-menu { display: block; } }
476 447
477 −/* ================= Footer ================= */
478 −.footer { background: var(--ink); color: rgba(247, 241, 239, 0.75); margin-top: 20px; padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px; position: relative; z-index: 1; }
479 −.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 34px; color: var(--paper); letter-spacing: -0.04em; margin-bottom: 12px; }
480 −.footer .fbrand .ka { color: var(--lime); }
481 −.footer b { color: var(--paper); }
482 −.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 760px; }
483 −.footer .fmono { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(247, 241, 239,0.45); margin-top: 18px; }
448 +/* ================= Footer =================
449 + Remplacé par le footer commun Groupe KA (ka/KaFooter.tsx — styles .ka-footer
450 + dans ka/tokens.css). */
484 451
485 452 /* ================= Mobile : feuille de filtres + FAB ================= */
486 453 .sheet-head { display: none; }
@@ -629,3 +596,151 @@ table.rooms tr:nth-child(even) td { background: var(--surface-2); }
629 596 font-size: 13px; padding: 8px; background: var(--green-deep); color: #fff;
630 597 text-decoration: none; }
631 598 .vp-link:hover { background: var(--green); }
599 +
600 +/* =============================================================================
601 + Harmonisation Groupe KA — header (badge + compte KA ID), pages Profil,
602 + Contact et légales. Le socle vient de ka/tokens.css.
603 +============================================================================= */
604 +
605 +/* ---- Header : badge « Un service Groupe KA » + zone compte ---- */
606 +.header-inner .gk-badge { flex: none; }
607 +@media (max-width: 900px) { .header-inner .gk-badge { display: none; } }
608 +.header-acct { display: flex; align-items: center; gap: 10px; flex: none; }
609 +.ka-auth { display: flex; align-items: center; gap: 10px; }
610 +.ka-login {
611 + display: inline-flex; align-items: center; gap: 6px; min-height: 40px;
612 + padding: 8px 15px; border: 1.5px solid var(--ink); border-radius: 999px;
613 + background: var(--surface); color: var(--ink); cursor: pointer;
614 + font-family: var(--font-display); font-weight: 600; font-size: 13.5px;
615 + box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.15);
616 + transition: transform 0.13s ease, box-shadow 0.13s ease, background 0.15s ease;
617 +}
618 +.ka-login b { background: var(--ink); color: var(--accent); border-radius: 5px; padding: 0 6px 1px; transform: rotate(-2deg); transition: transform 0.15s ease; }
619 +.ka-login:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 rgba(20, 24, 20, 0.2); }
620 +.ka-login:hover b { transform: rotate(0); }
621 +.ka-login:active { transform: translate(2px, 2px); box-shadow: none; }
622 +.ka-signup { font-family: var(--font-mono); font-size: 11px; color: var(--ink-2); text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }
623 +.ka-signup:hover { color: var(--accent-deep); }
624 +@media (max-width: 1100px) { .header-acct .ka-signup { display: none; } }
625 +.ka-acct {
626 + display: inline-flex; align-items: center; gap: 8px; min-height: 40px;
627 + padding: 6px 14px; border: 1.5px solid var(--ink); border-radius: 999px;
628 + background: var(--surface); font-family: var(--font-display);
629 + font-weight: 600; font-size: 13.5px; color: var(--ink);
630 + max-width: 160px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
631 +}
632 +.ka-acct:hover, .ka-acct.active { background: var(--ink); color: var(--accent); }
633 +.ka-acct-pic { width: 24px; height: 24px; border-radius: 50%; flex: none; border: 1px solid var(--line); }
634 +@media (max-width: 760px) {
635 + .header-acct { margin-left: auto; }
636 + .menu-btn { margin-left: 0; }
637 + .ka-login { min-height: 44px; }
638 +}
639 +@media (max-width: 380px) { .header-acct { display: none; } } /* → menu mobile */
640 +
641 +/* ---- Menu mobile : compte + badge groupe ---- */
642 +.mm-auth { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 15px 10px; border-bottom: 1.5px dashed var(--line); }
643 +.mm-auth .ka-login { min-height: 44px; }
644 +.mm-foot p { margin: 10px 0 0; }
645 +
646 +/* ---- Générique : lede (Profil / Contact) ---- */
647 +.lede { color: var(--ink-2); font-size: 15.5px; max-width: 640px; }
648 +
649 +/* ================= Page Profil (KA ID) ================= */
650 +.profil { padding: 44px 0 90px; }
651 +.profil h1 { font-size: clamp(28px, 4.6vw, 44px); margin: 10px 0 24px; letter-spacing: -0.03em; }
652 +.profil .lede { margin-bottom: 22px; }
653 +.profil .notice { padding: 60px 0; }
654 +
655 +/* Carte de membre — encre + cerise */
656 +.pc {
657 + position: relative; overflow: hidden; max-width: 460px;
658 + background: var(--ink); color: var(--paper);
659 + border: 1.5px solid var(--ink); border-radius: var(--r-card);
660 + box-shadow: var(--shadow-off-soft); padding: 22px 26px 0;
661 +}
662 +.pc-watermark {
663 + position: absolute; right: -18px; bottom: 14px; font-family: var(--font-display);
664 + font-weight: 700; font-size: 150px; line-height: 1; letter-spacing: -0.06em;
665 + color: rgba(245, 243, 238, 0.05); pointer-events: none; user-select: none;
666 +}
667 +.pc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
668 +.pc-brand { font-family: var(--font-display); font-weight: 700; font-size: 24px; letter-spacing: -0.03em; }
669 +.pc-ka { display: inline-block; background: var(--accent); color: var(--on-accent); border-radius: 6px; padding: 0 7px 2px; margin-left: 3px; transform: rotate(-2deg); }
670 +.pc-label { font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(245, 243, 238, 0.5); }
671 +.pc-id-block { margin: 26px 0 22px; display: flex; flex-direction: column; gap: 4px; }
672 +.pc-id-label { font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.2em; color: var(--accent); }
673 +.pc-id { font-family: var(--font-mono); font-weight: 700; font-size: clamp(17px, 4.4vw, 22px); letter-spacing: 0.04em; word-break: break-all; }
674 +.pc-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; padding-bottom: 18px; }
675 +.pc-holder { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
676 +.pc-holder-name { font-family: var(--font-display); font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
677 +.pc-role-badge { align-self: flex-start; font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 2px 9px; }
678 +.pc-holder-since { font-family: var(--font-mono); font-size: 10.5px; color: rgba(245, 243, 238, 0.5); letter-spacing: 0.06em; }
679 +.pc-avatar { width: 52px; height: 52px; border-radius: 50%; border: 2px solid var(--accent); flex: none; object-fit: cover; }
680 +.pc-strip { display: flex; align-items: flex-end; gap: 5px; margin: 0 -26px; padding: 9px 18px; background: rgba(226, 55, 68, 0.14); border-top: 1px solid rgba(226, 55, 68, 0.4); overflow: hidden; }
681 +.pc-strip i { display: block; width: 3px; border-radius: 1px; background: var(--accent); opacity: 0.75; }
682 +.pc-strip i:nth-child(3n) { height: 14px; opacity: 0.4; }
683 +.pc-strip i:nth-child(3n+1) { height: 9px; }
684 +.pc-strip i:nth-child(3n+2) { height: 17px; opacity: 0.95; }
685 +.pc-copy { margin-top: 16px; }
686 +.pc-copy.ok { background: var(--ink); color: var(--accent); }
687 +
688 +.profil-grid {
689 + display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
690 + gap: 12px; margin-top: 28px; max-width: 720px;
691 +}
692 +.pg-item { background: var(--surface); border: 1.5px solid var(--line-strong); border-radius: var(--r-ctl); padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
693 +.pg-label { font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); }
694 +.pg-value { font-weight: 600; font-size: 14.5px; word-break: break-word; }
695 +.pg-value.mono { font-family: var(--font-mono); color: var(--accent-deep); }
696 +.profil-note { max-width: 640px; color: var(--ink-2); font-size: 13.5px; margin-top: 22px; }
697 +.profil-note a { text-decoration: underline; text-underline-offset: 3px; }
698 +.profil-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 26px; }
699 +
700 +/* Profil hub Groupe KA (lecture seule) */
701 +.hub-profile { margin-top: 34px; padding-top: 24px; border-top: 1.5px solid var(--line-strong); max-width: 720px; display: flex; flex-direction: column; gap: 14px; }
702 +.hub-profile h3 { font-size: 18px; }
703 +.hub-profile .hub-bio { color: var(--ink-2); font-size: 14px; margin: 0; max-width: 560px; }
704 +.hub-profile .pub-meta { display: flex; gap: 8px; flex-wrap: wrap; }
705 +.hub-profile .stat-chip { display: inline-flex; align-items: center; gap: 6px; }
706 +.hub-profile a.stat-chip:hover { background: var(--accent-soft); }
707 +.hub-hint { color: var(--ink-3); font-size: 12.5px; margin: 0; }
708 +
709 +/* ================= Pages légales ================= */
710 +.legal { padding: 48px 0 80px; max-width: 780px; }
711 +.legal h1 { font-size: clamp(28px, 4.6vw, 44px); margin: 8px 0 6px; }
712 +.legal .legal-meta { color: var(--ink-3); font-family: var(--font-mono); font-size: 12px; margin: 0 0 26px; }
713 +.legal h2 { font-size: 20px; margin: 30px 0 8px; border-left: 4px solid var(--accent); padding-left: 10px; }
714 +.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; }
715 +.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }
716 +.legal a { text-decoration: underline; text-underline-offset: 3px; }
717 +.legal b { color: var(--ink); }
718 +
719 +/* ================= Page Contact (écosystème Groupe KA) ================= */
720 +.contact { padding: 48px 0 90px; }
721 +.contact h1 { font-size: clamp(30px, 5vw, 52px); margin: 12px 0 14px; text-transform: uppercase; letter-spacing: -0.03em; }
722 +.contact-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 14px; margin: 28px 0 6px; max-width: 900px; }
723 +.contact-card {
724 + display: flex; flex-direction: column; gap: 8px; min-height: var(--touch, 44px);
725 + background: var(--surface); border: 1.5px solid var(--ink);
726 + border-radius: var(--r-card); padding: 18px 20px;
727 + box-shadow: var(--shadow-off-soft);
728 + transition: transform 0.15s ease, box-shadow 0.15s ease;
729 +}
730 +.contact-card:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }
731 +.contact-role { font-family: var(--font-mono); font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--green); }
732 +.contact-mail { font-family: var(--font-display); font-weight: 700; font-size: clamp(14px, 2vw, 17px); word-break: break-all; border-bottom: 2px solid var(--accent); align-self: flex-start; }
733 +.contact-disclaimer { max-width: 720px; margin: 24px 0 0; padding: 14px 16px; border-left: 3px solid var(--accent); background: var(--accent-soft); color: var(--ink-2); font-size: 13.5px; }
734 +.contact-disclaimer b { color: var(--ink); }
735 +.contact-hub { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 26px; }
736 +.contact-sites { margin-top: 46px; }
737 +.contact-sites h2 { font-size: 21px; text-transform: uppercase; margin-bottom: 14px; }
738 +.contact-sites ul { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
739 +.contact-sites a { display: flex; flex-direction: column; gap: 3px; min-height: var(--touch, 44px); background: var(--surface); border: 1.5px solid var(--line-strong); border-radius: var(--r-ctl); padding: 12px 14px; transition: transform 0.14s ease, box-shadow 0.14s ease; }
740 +.contact-sites a:hover { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--ink); }
741 +.contact-sites b { font-family: var(--font-display); font-size: 15px; }
742 +.contact-sites span { font-size: 12px; color: var(--ink-3); }
743 +@media (max-width: 480px) {
744 + .header-inner { gap: 10px; }
745 + .ka-login { font-size: 12px; padding: 6px 10px; gap: 4px; min-height: 44px; }
746 +}
modified frontend/tsconfig.json +2 −1
@@ -16,5 +16,6 @@
16 16 "noUnusedParameters": false,
17 17 "noFallthroughCasesInSwitch": true
18 18 },
19 − "include": ["src"]
19 + "include": ["src"],
20 + "exclude": ["src/kamaps", "src/pages/Category.tsx"]
20 21 }
modified immoka/seo.py +5 −0
@@ -610,6 +610,11 @@ _STATIC_META = {
610 610 "/confidentialite": ("Politique de confidentialité",
611 611 "Politique de confidentialité d'Immo-Ka (Groupe-Ka) — Loi 25.", False),
612 612 "/profil": ("Mon profil", "Votre compte Groupe KA sur Immo-Ka.", True),
613 + "/contact": ("Contact — Groupe KA",
614 + "Écrire au Groupe KA : contact@groupe-ka.com (projets et données), "
615 + "info@groupe-ka.com (médias), admin@groupe-ka.com (légal et Loi 25). "
616 + "Immo-Ka est un service Groupe KA — https://www.groupe-ka.com.",
617 + False),
613 618 }
614 619
615 620
616 621