SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
37 min agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
4.5 KB · 150 lines tsx
Raw Blame History
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur  : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/pages/Ville.tsx7 * Rôle    : Pages programmatiques SEO — /villes (répertoire) et /ville/:ville.8 *           Le serveur rend le même contenu en HTML (jobka/seo.py) ; ici, la9 *           version interactive avec les cartes d'offres de l'app.10 * Créé    : 2026-08-23   Modifié : 2026-08-2311 * =============================================================================12 */13import { useEffect, useState } from "react";14import { Link, useParams } from "react-router-dom";15import { fetchJobs, Job } from "../api";16import JobCard from "../components/JobCard";1718interface VilleRow {19  city: string;20  slug: string;21  n: number;22  employers: number;23  with_salary: number;24}2526interface VilleData {27  city: string;28  slug: string;29  n: number;30  employers: number;31  with_salary: number;32  neighbors: VilleRow[];33}3435async function get<T>(path: string): Promise<T> {36  const res = await fetch(path);37  if (!res.ok) throw new Error(`${res.status}`);38  return res.json();39}4041export function VillesPage() {42  const [villes, setVilles] = useState<VilleRow[] | null>(null);43  const [error, setError] = useState<string | null>(null);4445  useEffect(() => {46    get<{ villes: VilleRow[] }>("/api/seo/villes")47      .then((r) => setVilles(r.villes))48      .catch((e) => setError(String(e)));49    window.scrollTo(0, 0);50  }, []);5152  return (53    <div className="container">54      <h1>Offres d'emploi par ville</h1>55      <p className="sub">56        Toutes les villes du Québec où Job-Ka recense des offres d'emploi,57        avec le nombre de postes ouverts et d'employeurs qui recrutent.58      </p>59      {error && <div className="empty">⚠️ {error}</div>}60      {!villes && !error && <div className="empty">Chargement…</div>}61      {villes && (62        <ul style={{ marginTop: 24, lineHeight: 2, paddingLeft: 20 }}>63          {villes.map((v) => (64            <li key={v.slug}>65              <Link to={`/ville/${v.slug}`}>{v.city}</Link>66              {" — "}67              {v.n} offre{v.n > 1 ? "s" : ""} · {v.employers} employeur68              {v.employers > 1 ? "s" : ""}69            </li>70          ))}71        </ul>72      )}73    </div>74  );75}7677export default function VillePage() {78  const { ville = "" } = useParams();79  const [data, setData] = useState<VilleData | null>(null);80  const [jobs, setJobs] = useState<Job[] | null>(null);81  const [error, setError] = useState<string | null>(null);8283  useEffect(() => {84    setData(null);85    setJobs(null);86    setError(null);87    get<VilleData>(`/api/seo/ville/${encodeURIComponent(ville)}`)88      .then((d) => {89        setData(d);90        return fetchJobs({ city: d.city }, 60).then((r) => setJobs(r.jobs));91      })92      .catch((e) =>93        setError(e.message === "404" ? "Ville inconnue." : String(e)));94    window.scrollTo(0, 0);95  }, [ville]);9697  if (error)98    return (99      <div className="container">100        <h1>Page introuvable</h1>101        <div className="empty">102          ⚠️ {error} <Link to="/villes">Voir toutes les villes</Link>103        </div>104      </div>105    );106  if (!data)107    return (108      <div className="container">109        <div className="empty">Chargement…</div>110      </div>111    );112113  return (114    <div className="container">115      <p style={{ margin: "0 0 4px" }}>116        <Link to="/villes">Villes</Link> — {data.city}117      </p>118      <h1>Offres d'emploi à {data.city}</h1>119      <p className="sub">120        {data.n} poste{data.n > 1 ? "s" : ""} chez {data.employers} employeur121        {data.employers > 1 ? "s" : ""}122        {data.with_salary > 0 && (123          <> · {data.with_salary} avec salaire affiché</>124        )}125      </p>126127      <div className="job-list" style={{ marginTop: 24 }}>128        {!jobs && <div className="empty">Chargement des offres…</div>}129        {jobs && jobs.map((j) => <JobCard key={j.uid} job={j} />)}130      </div>131132      {data.neighbors.length > 0 && (133        <>134          <h2 style={{ marginTop: 40 }}>Autres villes</h2>135          <p style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>136            {data.neighbors.map((v) => (137              <Link key={v.slug} to={`/ville/${v.slug}`}>138                {v.city} ({v.n})139              </Link>140            ))}141          </p>142        </>143      )}144      <p style={{ marginTop: 24 }}>145        <Link to="/">← Toutes les offres d'emploi au Québec</Link>146      </p>147    </div>148  );149}150