Complete family planning application with: - React frontend with TypeScript - Node.js/Express backend with TypeScript - Python ingestion service for document processing - Planning ingestion service with LLM integration - Shared UI components and type definitions - OAuth integration for calendar synchronization - Comprehensive documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
152 lines
6.0 KiB
JavaScript
152 lines
6.0 KiB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
import styled from "styled-components";
|
|
import { completeCalendarOAuth } from "../services/api-client";
|
|
import { consumePendingOAuthState, peekPendingOAuthState, storeOAuthResult } from "../utils/calendar-oauth";
|
|
const Container = styled.main `
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: radial-gradient(circle at top, rgba(32, 45, 112, 0.55), rgba(9, 13, 28, 0.95));
|
|
color: #f4f5ff;
|
|
padding: 32px;
|
|
`;
|
|
const Card = styled.div `
|
|
max-width: 520px;
|
|
width: 100%;
|
|
border-radius: 20px;
|
|
border: 1px solid rgba(126, 136, 180, 0.3);
|
|
background: rgba(16, 22, 52, 0.88);
|
|
padding: 32px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
box-shadow: 0 32px 68px rgba(0, 0, 0, 0.45);
|
|
`;
|
|
const Title = styled.h1 `
|
|
margin: 0;
|
|
font-size: 1.6rem;
|
|
`;
|
|
const Message = styled.p `
|
|
margin: 0;
|
|
line-height: 1.6;
|
|
color: rgba(197, 202, 240, 0.85);
|
|
`;
|
|
const Details = styled.pre `
|
|
margin: 0;
|
|
padding: 12px;
|
|
border-radius: 12px;
|
|
background: rgba(9, 13, 28, 0.75);
|
|
border: 1px solid rgba(126, 136, 180, 0.25);
|
|
font-size: 0.85rem;
|
|
color: rgba(197, 202, 240, 0.75);
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
`;
|
|
const Button = styled.button `
|
|
align-self: flex-start;
|
|
padding: 10px 18px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(85, 98, 255, 0.5);
|
|
background: rgba(85, 98, 255, 0.25);
|
|
color: #f4f5ff;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease, transform 0.15s ease;
|
|
|
|
&:hover {
|
|
background: rgba(85, 98, 255, 0.35);
|
|
transform: translateY(-1px);
|
|
}
|
|
`;
|
|
const providerLabel = (provider) => (provider === "google" ? "Google" : "Outlook");
|
|
export const CalendarOAuthCallbackScreen = () => {
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const [status, setStatus] = useState("processing");
|
|
const [statusMessage, setStatusMessage] = useState("Initialisation de la connexion securisee...");
|
|
const [details, setDetails] = useState({});
|
|
const stateParam = searchParams.get("state") ?? "";
|
|
const codeParam = searchParams.get("code") ?? undefined;
|
|
const errorParam = searchParams.get("error") ?? undefined;
|
|
const providerParam = (searchParams.get("provider") ?? undefined);
|
|
const pendingInfo = useMemo(() => (stateParam ? peekPendingOAuthState(stateParam) : null), [stateParam]);
|
|
useEffect(() => {
|
|
const execute = async () => {
|
|
if (!stateParam) {
|
|
setStatus("error");
|
|
setStatusMessage("Parametre 'state' manquant. Impossible de finaliser la connexion.");
|
|
return;
|
|
}
|
|
const inferredProvider = providerParam ?? pendingInfo?.provider ?? "google";
|
|
const payload = {
|
|
provider: inferredProvider,
|
|
state: stateParam,
|
|
code: codeParam,
|
|
error: errorParam,
|
|
profileId: pendingInfo?.profileId
|
|
};
|
|
try {
|
|
const response = await completeCalendarOAuth(payload);
|
|
storeOAuthResult({ success: response?.success, provider: inferredProvider, state: stateParam });
|
|
if (window.opener) {
|
|
window.opener.postMessage({
|
|
type: "fp-calendar-oauth",
|
|
success: response?.success ?? !errorParam,
|
|
state: stateParam,
|
|
provider: inferredProvider,
|
|
email: response?.email,
|
|
label: response?.label
|
|
}, window.location.origin);
|
|
}
|
|
setDetails({
|
|
provider: providerLabel(inferredProvider),
|
|
state: stateParam,
|
|
profileId: pendingInfo?.profileId,
|
|
result: response
|
|
});
|
|
if (response?.success && !errorParam) {
|
|
setStatus("success");
|
|
setStatusMessage("Connexion validee ! Vous pouvez revenir sur Family Planner.");
|
|
}
|
|
else {
|
|
setStatus("error");
|
|
setStatusMessage("La connexion n'a pas pu etre finalisee. Merci de reessayer.");
|
|
}
|
|
}
|
|
catch (error) {
|
|
setStatus("error");
|
|
setStatusMessage("Une erreur est survenue pendant la finalisation de l'autorisation.");
|
|
setDetails({
|
|
provider: providerLabel(inferredProvider),
|
|
state: stateParam,
|
|
error: String(error)
|
|
});
|
|
if (window.opener) {
|
|
window.opener.postMessage({
|
|
type: "fp-calendar-oauth",
|
|
success: false,
|
|
state: stateParam,
|
|
provider: inferredProvider,
|
|
error: String(error)
|
|
}, window.location.origin);
|
|
}
|
|
}
|
|
finally {
|
|
if (!window.opener) {
|
|
consumePendingOAuthState(stateParam);
|
|
}
|
|
setTimeout(() => window.close(), 2500);
|
|
}
|
|
};
|
|
void execute();
|
|
}, [codeParam, errorParam, pendingInfo, providerParam, stateParam]);
|
|
return (_jsx(Container, { children: _jsxs(Card, { children: [_jsx(Title, { children: status === "processing"
|
|
? "Connexion a l'agenda..."
|
|
: status === "success"
|
|
? "Agenda connecte !"
|
|
: "Connexion interrompue" }), _jsx(Message, { children: statusMessage }), Object.keys(details).length > 0 ? _jsx(Details, { children: JSON.stringify(details, null, 2) }) : null, status !== "processing" ? (_jsx(Button, { type: "button", onClick: () => navigate("/", { replace: true }), children: "Retourner au tableau de bord" })) : null] }) }));
|
|
};
|