Initial commit: Family Planner application
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>
This commit is contained in:
151
frontend/src/screens/CalendarOAuthCallbackScreen.js
Normal file
151
frontend/src/screens/CalendarOAuthCallbackScreen.js
Normal file
@@ -0,0 +1,151 @@
|
||||
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] }) }));
|
||||
};
|
||||
Reference in New Issue
Block a user