codex has reworked checkout, but frontend doesnt work
This commit is contained in:
@@ -1,98 +1,181 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
import { LoaderCircle, Mail, Lock } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import InputError from '@/components/input-error';
|
||||
import TextLink from '@/components/text-link';
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { usePage } from "@inertiajs/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import toast from "react-hot-toast";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import InputError from "@/components/input-error";
|
||||
import TextLink from "@/components/text-link";
|
||||
|
||||
interface LoginFormProps {
|
||||
onSuccess?: (userData: any) => void;
|
||||
canResetPassword?: boolean;
|
||||
declare const route: (name: string, params?: Record<string, unknown>) => string;
|
||||
|
||||
export interface AuthUserPayload {
|
||||
id?: number;
|
||||
email?: string;
|
||||
name?: string | null;
|
||||
pending_purchase?: boolean;
|
||||
}
|
||||
|
||||
export default function LoginForm({ onSuccess, canResetPassword = true }: LoginFormProps) {
|
||||
const [hasTriedSubmit, setHasTriedSubmit] = useState(false);
|
||||
const { t } = useTranslation('auth');
|
||||
const { props } = usePage<{ errors: Record<string, string> }>();
|
||||
interface LoginFormProps {
|
||||
onSuccess?: (userData: AuthUserPayload | null) => void;
|
||||
canResetPassword?: boolean;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
const { data, setData, post, processing, errors, clearErrors, reset } = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
type SharedPageProps = {
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
type FieldErrors = Record<string, string>;
|
||||
|
||||
const fallbackRoute = (locale: string) => `/${locale}/login`;
|
||||
|
||||
const csrfToken = () => (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)?.content ?? "";
|
||||
|
||||
export default function LoginForm({ onSuccess, canResetPassword = true, locale }: LoginFormProps) {
|
||||
const page = usePage<SharedPageProps>();
|
||||
const { t } = useTranslation("auth");
|
||||
const resolvedLocale = locale ?? page.props.locale ?? "de";
|
||||
|
||||
const loginEndpoint = useMemo(() => {
|
||||
if (typeof route === "function") {
|
||||
try {
|
||||
return route("checkout.login");
|
||||
} catch (error) {
|
||||
// Ziggy might not be booted yet; fall back to locale-aware path.
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackRoute(resolvedLocale);
|
||||
}, [resolvedLocale]);
|
||||
|
||||
const [values, setValues] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
remember: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasTriedSubmit && Object.keys(errors).length > 0) {
|
||||
toast.error(Object.values(errors).join(' '));
|
||||
}
|
||||
}, [errors, hasTriedSubmit]);
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setHasTriedSubmit(true);
|
||||
post('/login', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
if (onSuccess) {
|
||||
onSuccess({ user: { email: data.email } }); // Pass basic user info; full user from props in parent
|
||||
}
|
||||
reset();
|
||||
},
|
||||
});
|
||||
};
|
||||
const [errors, setErrors] = useState<FieldErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [hasTriedSubmit, setHasTriedSubmit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasTriedSubmit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorKeys = Object.keys(errors);
|
||||
if (errorKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = document.querySelector<HTMLInputElement>(`[name="${errorKeys[0]}"]`);
|
||||
|
||||
if (field) {
|
||||
field.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
field.focus();
|
||||
const collected = Object.values(errors).filter(Boolean);
|
||||
if (collected.length > 0) {
|
||||
toast.error(collected.join(" \u2022 "));
|
||||
}
|
||||
}, [errors, hasTriedSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasTriedSubmit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstKey = Object.keys(errors).find((key) => Boolean(errors[key]));
|
||||
if (!firstKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = document.querySelector<HTMLInputElement>(`[name="${firstKey}"]`);
|
||||
field?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
field?.focus();
|
||||
}, [errors, hasTriedSubmit]);
|
||||
|
||||
const updateValue = (key: keyof typeof values, value: string | boolean) => {
|
||||
setValues((current) => ({ ...current, [key]: value }));
|
||||
setErrors((current) => ({ ...current, [key as string]: "" }));
|
||||
};
|
||||
|
||||
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setHasTriedSubmit(true);
|
||||
setErrors({});
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(loginEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"X-CSRF-TOKEN": csrfToken(),
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
remember: values.remember,
|
||||
locale: resolvedLocale,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const payload = await response.json();
|
||||
const nextUser: AuthUserPayload | null = payload?.user ?? payload?.data ?? null;
|
||||
|
||||
toast.success(t("login.success_toast", "Login erfolgreich"));
|
||||
onSuccess?.(nextUser);
|
||||
setValues((current) => ({ ...current, password: "" }));
|
||||
setHasTriedSubmit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
const payload = await response.json();
|
||||
const fieldErrors: FieldErrors = {};
|
||||
const source = payload?.errors ?? {};
|
||||
Object.entries(source).forEach(([key, message]) => {
|
||||
if (Array.isArray(message) && message.length > 0) {
|
||||
fieldErrors[key] = String(message[0]);
|
||||
} else if (typeof message === "string") {
|
||||
fieldErrors[key] = message;
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(fieldErrors);
|
||||
toast.error(t("login.failed_generic", "Ungueltige Anmeldedaten"));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(t("login.unexpected_error", "Beim Login ist ein Fehler aufgetreten."));
|
||||
} catch (error) {
|
||||
console.error("Login request failed", error);
|
||||
toast.error(t("login.unexpected_error", "Beim Login ist ein Fehler aufgetreten."));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<form onSubmit={submit} className="flex flex-col gap-6" noValidate>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">{t('login.email')}</Label>
|
||||
<Label htmlFor="email">{t("login.email")}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
autoFocus
|
||||
placeholder={t('login.email_placeholder')}
|
||||
value={data.email}
|
||||
onChange={(e) => {
|
||||
setData('email', e.target.value);
|
||||
if (errors.email) {
|
||||
clearErrors('email');
|
||||
}
|
||||
}}
|
||||
placeholder={t("login.email_placeholder")}
|
||||
value={values.email}
|
||||
onChange={(event) => updateValue("email", event.target.value)}
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">{t('login.password')}</Label>
|
||||
<Label htmlFor="password">{t("login.password")}</Label>
|
||||
{canResetPassword && (
|
||||
<TextLink href="/forgot-password" className="ml-auto text-sm">
|
||||
{t('login.forgot')}
|
||||
<TextLink href={typeof route === "function" ? route("password.request") : "/forgot-password"} className="ml-auto text-sm">
|
||||
{t("login.forgot")}
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
@@ -101,14 +184,9 @@ export default function LoginForm({ onSuccess, canResetPassword = true }: LoginF
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
placeholder={t('login.password_placeholder')}
|
||||
value={data.password}
|
||||
onChange={(e) => {
|
||||
setData('password', e.target.value);
|
||||
if (errors.password) {
|
||||
clearErrors('password');
|
||||
}
|
||||
}}
|
||||
placeholder={t("login.password_placeholder")}
|
||||
value={values.password}
|
||||
onChange={(event) => updateValue("password", event.target.value)}
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
@@ -117,25 +195,24 @@ export default function LoginForm({ onSuccess, canResetPassword = true }: LoginF
|
||||
<Checkbox
|
||||
id="remember"
|
||||
name="remember"
|
||||
checked={data.remember}
|
||||
onCheckedChange={(checked) => setData('remember', Boolean(checked))}
|
||||
checked={values.remember}
|
||||
onCheckedChange={(checked) => updateValue("remember", Boolean(checked))}
|
||||
/>
|
||||
<Label htmlFor="remember">{t('login.remember')}</Label>
|
||||
<Label htmlFor="remember">{t("login.remember")}</Label>
|
||||
</div>
|
||||
|
||||
<Button type="button" onClick={submit} className="w-full" disabled={processing}>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin mr-2" />}
|
||||
{t('login.submit')}
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting && <LoaderCircle className="h-4 w-4 animate-spin mr-2" />}
|
||||
{t("login.submit")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{Object.keys(errors).length > 0 && (
|
||||
{Object.values(errors).some(Boolean) && (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-sm text-red-800">
|
||||
{Object.values(errors).join(' ')}
|
||||
</p>
|
||||
<p className="text-sm text-red-800">{Object.values(errors).filter(Boolean).join(" \u2022 ")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
import { LoaderCircle, User, Mail, Phone, Lock, MapPin } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
|
||||
interface RegisterFormProps {
|
||||
packageId?: number;
|
||||
onSuccess?: (userData: any) => void;
|
||||
privacyHtml: string;
|
||||
declare const route: (name: string, params?: Record<string, unknown>) => string;
|
||||
|
||||
export interface RegisterSuccessPayload {
|
||||
user: any | null;
|
||||
redirect?: string | null;
|
||||
pending_purchase?: boolean;
|
||||
}
|
||||
|
||||
export default function RegisterForm({ packageId, onSuccess, privacyHtml }: RegisterFormProps) {
|
||||
interface RegisterFormProps {
|
||||
packageId?: number;
|
||||
onSuccess?: (payload: RegisterSuccessPayload) => void;
|
||||
privacyHtml: string;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale }: RegisterFormProps) {
|
||||
const [privacyOpen, setPrivacyOpen] = useState(false);
|
||||
const [hasTriedSubmit, setHasTriedSubmit] = useState(false);
|
||||
const { t } = useTranslation(['auth', 'common']);
|
||||
const { props } = usePage<{ errors: Record<string, string> }>();
|
||||
const page = usePage<{ errors: Record<string, string>; locale?: string; auth?: { user?: any | null } }>();
|
||||
const resolvedLocale = locale ?? page.props.locale ?? 'de';
|
||||
|
||||
const { data, setData, post, processing, errors, clearErrors, reset } = useForm({
|
||||
const { data, setData, errors, clearErrors, reset, setError } = useForm({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -36,18 +46,76 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml }: Regi
|
||||
}
|
||||
}, [errors, hasTriedSubmit]);
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const registerEndpoint = useMemo(() => {
|
||||
if (typeof route === 'function') {
|
||||
try {
|
||||
return route('checkout.register');
|
||||
} catch (error) {
|
||||
// ignore ziggy errors and fall back
|
||||
}
|
||||
}
|
||||
|
||||
return `/${resolvedLocale}/register`;
|
||||
}, [resolvedLocale]);
|
||||
|
||||
const submit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setHasTriedSubmit(true);
|
||||
post('/register', {
|
||||
preserveScroll: true,
|
||||
onSuccess: (page) => {
|
||||
if (onSuccess) {
|
||||
onSuccess((page as any).props.auth.user);
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
clearErrors();
|
||||
|
||||
const body = {
|
||||
...data,
|
||||
locale: resolvedLocale,
|
||||
package_id: data.package_id ?? packageId ?? null,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(registerEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)?.content ?? '',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
toast.success(t('register.success_toast', 'Registrierung erfolgreich'));
|
||||
onSuccess?.({
|
||||
user: json?.user ?? null,
|
||||
redirect: json?.redirect ?? null,
|
||||
pending_purchase: json?.pending_purchase ?? json?.user?.pending_purchase ?? false,
|
||||
});
|
||||
reset();
|
||||
},
|
||||
});
|
||||
setHasTriedSubmit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
const json = await response.json();
|
||||
const fieldErrors = json?.errors ?? {};
|
||||
Object.entries(fieldErrors).forEach(([key, message]) => {
|
||||
if (Array.isArray(message) && message.length > 0) {
|
||||
setError(key, message[0] as string);
|
||||
} else if (typeof message === 'string') {
|
||||
setError(key, message);
|
||||
}
|
||||
});
|
||||
toast.error(t('register.validation_failed', 'Bitte pruefen Sie Ihre Eingaben.'));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(t('register.unexpected_error', 'Registrierung nicht moeglich.'));
|
||||
} catch (error) {
|
||||
console.error('Register request failed', error);
|
||||
toast.error(t('register.unexpected_error', 'Registrierung nicht moeglich.'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -323,10 +391,10 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml }: Regi
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={processing}
|
||||
disabled={isSubmitting}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-[#FFB6C1] hover:bg-[#FF69B4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#FFB6C1] transition duration-300 disabled:opacity-50"
|
||||
>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin mr-2" />}
|
||||
{isSubmitting && <LoaderCircle className="h-4 w-4 animate-spin mr-2" />}
|
||||
{t('register.submit')}
|
||||
</button>
|
||||
|
||||
@@ -341,4 +409,12 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml }: Regi
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user