Polish UI, harden company creation, and add error pages
This commit is contained in:
@@ -5,6 +5,7 @@ import { Box, Button, Chip, Paper, Typography } from "@mui/material";
|
||||
import { api } from "../api";
|
||||
import { clearAuthToken, decodeJwtPayload, getAuthToken, setAuthToken } from "../auth";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -46,6 +47,7 @@ function loadGoogleScript(): Promise<void> {
|
||||
|
||||
export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void }) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const [token, setToken] = useState<string | null>(() => getAuthToken());
|
||||
const [me, setMe] = useState<MeResponse | null>(null);
|
||||
const [working, setWorking] = useState(false);
|
||||
@@ -81,20 +83,20 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
|
||||
if (cancelled) return;
|
||||
setAuthToken(res.data.accessToken);
|
||||
setToken(res.data.accessToken);
|
||||
toast("Signed in with Google.", "success");
|
||||
toast(t("googleSignedIn"), "success");
|
||||
onSignedIn?.();
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
clearAuthToken();
|
||||
setToken(null);
|
||||
toast("This Google account is not linked yet. Sign in locally first to bind it.", "info");
|
||||
toast(t("googleNotLinkedYet"), "info");
|
||||
}
|
||||
};
|
||||
void exchange();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [token, isRawGoogleToken, onSignedIn, toast]);
|
||||
}, [token, isRawGoogleToken, onSignedIn, toast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const host = hostRef.current;
|
||||
@@ -102,9 +104,7 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
|
||||
|
||||
const shouldRenderButton = !token || isRawGoogleToken || (me?.provider === "local" && !me?.googleLink?.linked);
|
||||
host.replaceChildren();
|
||||
if (!shouldRenderButton) {
|
||||
return;
|
||||
}
|
||||
if (!shouldRenderButton) return;
|
||||
|
||||
let active = true;
|
||||
void loadGoogleScript()
|
||||
@@ -120,17 +120,17 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
|
||||
try {
|
||||
if (me?.provider === "local") {
|
||||
const res = await api.post<{ linked: boolean; email?: string | null }>("/auth/google/link", { token: credential });
|
||||
toast(res.data?.email ? `Linked Google account ${res.data.email}.` : "Google account linked.", "success");
|
||||
toast(res.data?.email ? t("googleLinkedSuccessWithEmail", { email: res.data.email }) : t("googleLinkedSuccess"), "success");
|
||||
await refreshMe();
|
||||
} else {
|
||||
const res = await api.post<{ accessToken: string }>("/auth/google/exchange", { token: credential });
|
||||
setAuthToken(res.data.accessToken);
|
||||
setToken(res.data.accessToken);
|
||||
toast("Signed in with Google.", "success");
|
||||
toast(t("googleSignedIn"), "success");
|
||||
onSignedIn?.();
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data || e?.message || "Google authentication failed.";
|
||||
const msg = e?.response?.data || e?.message || t("googleAuthFailed");
|
||||
toast(String(msg), "error");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
@@ -145,46 +145,48 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
|
||||
text: me?.provider === "local" ? "continue_with" : "signin_with",
|
||||
});
|
||||
})
|
||||
.catch(() => toast("Google auth script failed to load.", "error"));
|
||||
.catch(() => toast(t("googleScriptLoadFailed"), "error"));
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
host.replaceChildren();
|
||||
};
|
||||
}, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, isRawGoogleToken, token, toast]);
|
||||
}, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, isRawGoogleToken, token, toast, t]);
|
||||
|
||||
const signedInName = me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email || "";
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
Google account
|
||||
{t("googleAccountTitle")}
|
||||
</Typography>
|
||||
|
||||
{!clientId && (
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
Set `REACT_APP_GOOGLE_CLIENT_ID` in your UI environment to enable Google sign-in and account linking.
|
||||
{t("googleSetupHint")}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{clientId && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.25 }}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Chip size="small" label={me?.googleLink?.linked ? "Linked" : "Available to link"} color={me?.googleLink?.linked ? "success" : "default"} variant={me?.googleLink?.linked ? "filled" : "outlined"} />
|
||||
{me?.googleLink?.linkedAt ? <Chip size="small" variant="outlined" label={`Linked ${new Date(me.googleLink.linkedAt).toLocaleDateString()}`} /> : null}
|
||||
<Chip size="small" label={me?.googleLink?.linked ? t("googleLinked") : t("googleAvailableToLink")} color={me?.googleLink?.linked ? "success" : "default"} variant={me?.googleLink?.linked ? "filled" : "outlined"} />
|
||||
{me?.googleLink?.linkedAt ? <Chip size="small" variant="outlined" label={t("googleLinkedDate", { date: new Date(me.googleLink.linkedAt).toLocaleDateString() })} /> : null}
|
||||
</Box>
|
||||
|
||||
{!token ? (
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
Sign in with a Google account that has already been linked to your Job Tracker user.
|
||||
{t("googleSignInHint")}
|
||||
</Typography>
|
||||
) : me?.provider === "local" ? (
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
{me.googleLink?.linked
|
||||
? `Linked to ${me.googleLink.email || "your Google account"}.`
|
||||
: "Bind a Google account to this user so you can sign in with Google and still keep your normal app roles and data."}
|
||||
? t("googleLinkedTo", { email: me.googleLink.email || t("googleLinkedToYourAccount") })
|
||||
: t("googleBindHint")}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
Exchange your Google sign-in for a normal Job Tracker session.
|
||||
{t("googleExchangeHint")}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -198,10 +200,10 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
|
||||
clearAuthToken();
|
||||
setToken(null);
|
||||
setMe(null);
|
||||
toast("Signed out.", "info");
|
||||
toast(t("signedOut"), "info");
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
{t("signOut")}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
@@ -213,22 +215,22 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
|
||||
onClick={async () => {
|
||||
try {
|
||||
await api.delete("/auth/google/link");
|
||||
toast("Google account unlinked.", "info");
|
||||
toast(t("googleUnlinked"), "info");
|
||||
await refreshMe();
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data || e?.message || "Failed to unlink Google account.";
|
||||
const msg = e?.response?.data || e?.message || t("googleUnlinkFailed");
|
||||
toast(String(msg), "error");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlink Google
|
||||
{t("unlinkGoogle")}
|
||||
</Button>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
{token && me?.email ? (
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
Signed in as {me.displayName || [me.firstName, me.lastName].filter(Boolean).join(" ") || me.email}.
|
||||
{t("signedInAs", { name: signedInName })}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user