'use client' import { FormEvent, useEffect, useState } from 'react' import { useDjangoFormCore, type DjangoFormState, type FormOptions, type FormErrors, } from 'mizan' import { useAuthContext } from '../contexts/AuthContext' import { useStyles } from '../contexts/StylesContext' import { getAuthDetails, AuthDetails } from '../api' interface FooterLink { label: string href?: string onClick?: () => void } interface AuthDjangoFormProps { /** Form name (e.g., "login", "signup", "change_password") */ formName: string /** Callback after successful form submission */ onSuccess?: (result: any, authDetails: AuthDetails) => void /** Callback after failed form submission */ onError?: (errors: any) => void /** Links to show in footer (e.g., "Forgot password?") */ footerLinks?: FooterLink[] /** Content to render before form fields */ preFields?: React.ReactNode /** Content to render after form fields (before submit button) */ postFields?: React.ReactNode /** Override the submit button label from schema */ submitLabel?: string /** Override the title from schema */ title?: string /** Override the subtitle from schema */ subtitle?: string /** Options for form behavior (validation, schema refetch, etc.) */ formOptions?: FormOptions } /** * AuthDjangoForm renders a form from the mizan server functions * with styling consistent with the auth UI. * * It fetches the form schema (including title, subtitle, fields, submit label) * from the backend and renders it dynamically with real-time validation. */ export function AuthDjangoForm({ formName, onSuccess, onError, footerLinks, preFields, postFields, submitLabel, title, subtitle, formOptions, }: AuthDjangoFormProps) { const form = useDjangoFormCore>({ name: formName, options: formOptions, }) const { refresh } = useAuthContext() const styles = useStyles() const [mounted, setMounted] = useState(false) // Hydration safety: only render inputs after mount useEffect(() => { setMounted(true) }, []) const handleSubmit = async (e: FormEvent) => { e.preventDefault() const result = await form.submit() if (result.success) { // Refresh auth state and get the updated auth for callbacks const newAuth = await refresh() onSuccess?.(result.data, getAuthDetails(newAuth)) } else { onError?.(result.errors) } } // Loading state if (form.loading) { return (
) } // Get form-level errors (non-field errors like "Invalid credentials") // These only appear after submission due to 'field-only' default const formErrors = form.getFormErrors() // Use prop overrides or schema values const displayTitle = title ?? form.schema?.title const displaySubtitle = subtitle ?? form.schema?.subtitle const displaySubmitLabel = submitLabel ?? form.schema?.submit_label ?? 'Submit' return (
{displayTitle && (

{displayTitle}

)} {displaySubtitle && (

{displaySubtitle}

)} {/* Form-level errors (shown after submission) */} {formErrors.length > 0 && (
{formErrors.map((err, i) => (

{err.message}

))}
)}
{preFields}
{form.schema?.fieldOrder.map(fieldName => { const field = form.schema!.fields[fieldName] return ( form.set(fieldName, value)} onBlur={() => form.touch(fieldName)} /> ) })}
{postFields}
{footerLinks && footerLinks.length > 0 && (
{footerLinks.map((link, i) => ( link.onClick ? ( ) : link.href ? ( {link.label} ) : null ))}
)}
) } /** * Internal field component with hydration-safe rendering */ interface AuthFieldProps { field: { name: string label: string type: string widget: string required: boolean disabled: boolean help_text: string max_length?: number | null choices?: Array<{ value: string; label: string }> | null } value: any mounted: boolean touched: boolean errors: Array<{ message: string }> onChange: (value: any) => void onBlur: () => void } function AuthField({ field, value, mounted, touched, errors, onChange, onBlur }: AuthFieldProps) { const styles = useStyles() const renderInput = () => { // Select dropdown if (field.choices && (field.widget === 'Select' || field.type === 'select')) { return ( ) } // Radio buttons if (field.choices && field.widget === 'RadioSelect') { return (
{field.choices.map((choice) => ( ))}
) } // Checkbox if (field.type === 'checkbox') { return ( onChange(e.target.checked)} onBlur={onBlur} required={field.required} disabled={field.disabled} className={styles.checkbox} /> ) } // Textarea if (field.widget === 'Textarea') { return (