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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
TURSO_DATABASE_URL=libsql://votre-base.turso.io
TURSO_AUTH_TOKEN=votre_token_ici

# ─── Google Authentification (Optionnel) ──────────────────────────────────────
# Clé client Google OAuth pour l'authentification et gestion des abonnements
GOOGLE_CLIENT_ID=788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com

# ─── API CIVIX (optionnel en développement local) ─────────────────────────────
# https://civix.fr
CIVIX_API_KEY=
Expand Down
24 changes: 24 additions & 0 deletions public/gtag-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}

Check failure on line 2 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `{dataLayer.push(arguments);` with `·{⏎··dataLayer.push(arguments);⏎`
gtag('js', new Date());

Check failure on line 3 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'js'` with `"js"`
gtag('consent', 'default', {

Check failure on line 4 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'consent',·'default'` with `"consent",·"default"`
'analytics_storage': 'denied',

Check failure on line 5 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'analytics_storage':·'denied'` with `analytics_storage:·"denied"`
'ad_storage': 'denied',

Check failure on line 6 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'ad_storage':·'denied'` with `ad_storage:·"denied"`
'ad_user_data': 'denied',

Check failure on line 7 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'ad_user_data':·'denied'` with `ad_user_data:·"denied"`
'ad_personalization': 'denied',

Check failure on line 8 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'ad_personalization':·'denied'` with `ad_personalization:·"denied"`
'wait_for_update': 500

Check failure on line 9 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'wait_for_update':·500` with `wait_for_update:·500,`
});
gtag('config', 'G-CMMWPQG5P6');

Check failure on line 11 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Replace `'config',·'G-CMMWPQG5P6'` with `"config",·"G-CMMWPQG5P6"`
(function() {

Check failure on line 12 in public/gtag-init.js

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Insert `·`
try {
var saved = localStorage.getItem('mandat_analytics_consent');
if (saved === 'granted') {
gtag('consent', 'update', {
'analytics_storage': 'granted',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied'
});
}
} catch(e) {}
})();
7 changes: 7 additions & 0 deletions public/sw-register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').catch(function(e) {
console.warn('[SW] Registration failed:', e);
});
});
}
2 changes: 1 addition & 1 deletion src/lib/google-auth.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// src/lib/google-auth.server.ts
// Validation serveur du JWT de connexion Google

const GOOGLE_CLIENT_ID = "788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com";
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || "788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com";

export interface GoogleUserSession {
email: string;
Expand Down
10 changes: 10 additions & 0 deletions src/lib/google-config.functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// src/lib/google-config.functions.ts
import { createServerFn } from "@tanstack/react-start";

export const getGoogleClientId = createServerFn({ method: "GET" }).handler(
async () => {
return {
googleClientId: process.env.GOOGLE_CLIENT_ID || "788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com"
};
}
);
25 changes: 25 additions & 0 deletions src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { QueryClient } from "@tanstack/react-query";
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
import { createIsomorphicFn, getGlobalStartContext } from "@tanstack/react-start";

export const getCspNonce = createIsomorphicFn()
.server(() => {
try {
const ctx = getGlobalStartContext();
return (ctx?.cspNonce as string) || "";
} catch (e) {
return "";
}
})
.client(() => {
if (typeof document !== "undefined") {
const el = document.querySelector(
"meta[property=csp-nonce]",
) as HTMLMetaElement | null;
return el?.content || "";
}
return "";
});

export const getRouter = () => {
// Query client partagé (nouveau à chaque requête SSR, réutilisé côté client).
Expand All @@ -17,6 +37,8 @@ export const getRouter = () => {
},
});

const nonce = getCspNonce();

const router = createRouter({
routeTree,
context: { queryClient },
Expand All @@ -27,6 +49,9 @@ export const getRouter = () => {
defaultPreloadStaleTime: 0,
// Cache le HTML rendu pendant 30s en mémoire pour retour arrière rapide.
defaultStaleTime: 1000 * 30,
ssr: {
nonce,
},
});

return router;
Expand Down
49 changes: 6 additions & 43 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { Analytics } from "@vercel/analytics/react";
import { Header } from "../components/Header";
import { Footer } from "../components/Footer";
import { getCspNonce } from "../router";
import { CookieBanner } from "../components/CookieBanner";
import { PWAInstallPrompt } from "../components/PWAInstallPrompt";
import { LoadingOverlay } from "../components/LoadingOverlay";
Expand Down Expand Up @@ -169,7 +170,7 @@

// ─── 404 / ERROR ────────────────────────────────────────────────────────

function NotFoundComponent() {

Check warning on line 173 in src/routes/__root.tsx

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the `extraHOCs` option
return (
<div className="container-app py-24 text-center animate-fade-up">
<Landmark
Expand All @@ -191,7 +192,7 @@
);
}

function ErrorComponent({ error, reset }: { error: Error; reset: () => void }) {

Check warning on line 195 in src/routes/__root.tsx

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the `extraHOCs` option
const router = useRouter();
useEffect(() => {
reportLovableError(error, { boundary: "tanstack_root_error_component" });
Expand Down Expand Up @@ -280,58 +281,20 @@

// ─── SHELL & COMPONENT ──────────────────────────────────────────────────

function RootShell({ children }: { children: ReactNode }) {

Check warning on line 284 in src/routes/__root.tsx

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the `extraHOCs` option
const nonce = getCspNonce();

return (
<html lang="fr">
<head>
<HeadContent />
{nonce && <meta property="csp-nonce" content={nonce} />}
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-CMMWPQG5P6"
/>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'wait_for_update': 500
});
gtag('config', 'G-CMMWPQG5P6');
(function() {
try {
var saved = localStorage.getItem('mandat_analytics_consent');
if (saved === 'granted') {
gtag('consent', 'update', {
'analytics_storage': 'granted',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied'
});
}
} catch(e) {}
})();
`,
}}
/>
<script
dangerouslySetInnerHTML={{
__html: `
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').catch(function(e) {
console.warn('[SW] Registration failed:', e);
});
});
}
`,
}}
/>
<script src="/gtag-init.js" defer />
<script src="/sw-register.js" defer />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
Expand Down Expand Up @@ -385,7 +348,7 @@
);
}

function RootComponent() {

Check warning on line 351 in src/routes/__root.tsx

View workflow job for this annotation

GitHub Actions / Lint, TypeCheck & Test

Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the `extraHOCs` option
const { queryClient } = Route.useRouteContext();
useLocation();

Expand Down
107 changes: 24 additions & 83 deletions src/routes/desabonnement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import {
Trash2,
ArrowLeft,
LogOut,
Mail,
Sparkles,
UserCheck,
Loader2,
Calendar,
ExternalLink
} from "lucide-react";
import { createSeoMeta, createSeoLinks, SITE_URL } from "./__root";
import { getGoogleClientId } from "../lib/google-config.functions";

// Types globaux pour Google Identity Services
declare global {
Expand All @@ -24,7 +23,7 @@ declare global {
accounts: {
id: {
initialize: (config: { client_id: string; callback: (response: { credential: string }) => void }) => void;
renderButton: (element: HTMLElement | null, options: { theme?: string; size?: string; width?: string }) => void;
renderButton: (element: HTMLElement | null, options: { theme?: string; size?: string; width?: string; shape?: string }) => void;
prompt: () => void;
};
};
Expand All @@ -41,6 +40,9 @@ export const Route = createFileRoute("/desabonnement")({
}),
links: createSeoLinks(`${SITE_URL}/desabonnement`),
}),
loader: async () => {
return await getGoogleClientId();
},
component: DesabonnementPage,
});

Expand Down Expand Up @@ -77,17 +79,14 @@ function decodeGoogleJwt(token: string): { email: string; name?: string; picture
}

function DesabonnementPage() {
const { googleClientId } = Route.useLoaderData();
const [user, setUser] = useState<UserProfile | null>(null);
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);

// Simulation de connexion pour les environnements de test / local ou si les clés Google manquent
const [simEmail, setSimEmail] = useState("");
const [simError, setSimError] = useState("");

// Charger l'API Google Identity Services
useEffect(() => {
// Vérifier si l'utilisateur est déjà stocké en local pour la session
Expand All @@ -107,19 +106,19 @@ function DesabonnementPage() {
script.async = true;
script.defer = true;
script.onload = () => {
initializeGoogleSignIn();
initializeGoogleSignIn(googleClientId);
};
document.body.appendChild(script);

return () => {
document.body.removeChild(script);
};
}, []);
}, [googleClientId]);

const initializeGoogleSignIn = () => {
const initializeGoogleSignIn = (clientIdToUse = googleClientId) => {
if (window.google?.accounts?.id) {
window.google.accounts.id.initialize({
client_id: "788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com",
client_id: clientIdToUse,
callback: (response) => {
const profile = decodeGoogleJwt(response.credential);
if (profile && profile.email) {
Expand All @@ -141,7 +140,7 @@ function DesabonnementPage() {

window.google.accounts.id.renderButton(
document.getElementById("google-signin-btn"),
{ theme: "outline", size: "large", width: "100%" }
{ theme: "filled_blue", size: "large", width: "280", shape: "pill" }
);
}
};
Expand All @@ -167,29 +166,6 @@ function DesabonnementPage() {
}
};

// Simuler la connexion avec un email de test
const handleSimulationLogin = () => {
if (!simEmail.trim() || !simEmail.includes("@")) {
setSimError("Veuillez saisir une adresse email valide.");
return;
}
setSimError("");

const email = simEmail.trim().toLowerCase();
const simToken = `simulation-token:${email}`;

const simProfile: UserProfile = {
email,
name: "Utilisateur Test",
picture: undefined,
credential: simToken,
};
setUser(simProfile);
localStorage.setItem("mandat_auth_user", JSON.stringify(simProfile));
setMessage(null);
fetchSubscriptions(simToken);
};

const handleLogout = () => {
setUser(null);
setSubscriptions([]);
Expand Down Expand Up @@ -299,55 +275,19 @@ function DesabonnementPage() {

{/* ── PHASE 1 : CONNEXION REQUISE ── */}
{!user ? (
<div className="space-y-6">
<div className="card-glass rounded-[2rem] p-8 border border-border/40 text-center space-y-6">
<h2 className="font-display text-xl tracking-tight">Authentification Google</h2>
<p className="text-xs text-muted-foreground leading-relaxed max-w-sm mx-auto">
Cliquez ci-dessous pour vous connecter. Cela nous permettra de retrouver instantanément tous les députés que vous suivez.
</p>

<div className="flex justify-center py-2 max-w-xs mx-auto">
<div id="google-signin-btn" className="w-full min-h-[44px]"></div>
</div>

<div className="flex items-center justify-center gap-2 text-[11px] text-muted-foreground">
<UserCheck className="w-3.5 h-3.5 text-primary" />
Connexion sécurisée par Google Identity Services
</div>
<div className="card-glass rounded-[2rem] p-8 border border-border/40 text-center space-y-6">
<h2 className="font-display text-xl tracking-tight">Authentification Google</h2>
<p className="text-xs text-muted-foreground leading-relaxed max-w-sm mx-auto">
Cliquez ci-dessous pour vous connecter. Cela nous permettra de retrouver instantanément tous les députés que vous suivez.
</p>

<div className="flex justify-center py-2 max-w-xs mx-auto">
<div id="google-signin-btn" className="w-full min-h-[44px]"></div>
</div>

{/* SIMULATEUR DE TEST POUR LOCAL / DÉVELOPPEMENT */}
<div className="card-glass rounded-[1.5rem] p-6 border border-primary/20 bg-primary/5 space-y-4">
<div className="flex items-center gap-2 text-xs font-semibold text-primary uppercase tracking-wider">
<Sparkles className="w-4 h-4 animate-pulse" />
Mode Simulation / Test de l'interface
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
Utile pour valider l'interface en local ou si vous n'avez pas de configuration Google active.
</p>
<div className="space-y-2">
<div className="flex gap-2">
<div className="relative flex-1">
<Mail className="absolute left-3 top-2.5 w-4 h-4 text-muted-foreground" />
<input
type="email"
value={simEmail}
onChange={(e) => setSimEmail(e.target.value)}
placeholder="Saisissez un email de test..."
className="w-full pl-9 pr-3 py-2 rounded-xl text-xs bg-background border border-border/60 outline-none focus:border-primary transition-colors"
/>
</div>
<button
onClick={handleSimulationLogin}
className="px-4 py-2 bg-primary text-white font-semibold text-xs rounded-xl hover:bg-primary/90 transition-colors shrink-0"
>
Se connecter (Simulation)
</button>
</div>
{simError && (
<p className="text-[11px] text-destructive">⚠️ {simError}</p>
)}
</div>
<div className="flex items-center justify-center gap-2 text-[11px] text-muted-foreground">
<UserCheck className="w-3.5 h-3.5 text-primary" />
Connexion sécurisée par Google Identity Services
</div>
</div>
) : (
Expand Down Expand Up @@ -456,7 +396,8 @@ function DesabonnementPage() {
</div>

<Link
to={`/depute/${sub.depute_slug}`}
to="/depute/$slug"
params={{ slug: sub.depute_slug }}
onClick={(e) => e.stopPropagation()} // Éviter de déclencher la sélection du checkbox
className="inline-flex items-center gap-1 text-xs text-primary font-semibold hover:underline glass border border-primary/20 px-3 py-1.5 rounded-xl transition-colors hover:bg-primary/5 shrink-0"
>
Expand Down
Loading
Loading