packages/
mizan-runtime/ Framework-agnostic state engine (~150 lines)
Context registry, batched invalidation, fetch primitives
mizan-django/ Django server adapter (was packages/mizan-rpc/adapters/django/)
Codegen moved to mizan-django/generate/
mizan-react/ React adapter (was packages/mizan-csr/adapters/react/)
Removed premature abstractions: mizan-ast, mizan-schema, mizan-rpc,
mizan-csr, mizan-ssr stub packages. The actual architecture is three
concrete packages, not five abstract layers.
mizan-runtime implements the v1 spec: registerContext with params,
scoped invalidation via microtask batching, server-driven invalidation
from mutation responses, mizanFetch for context bundles, mizanCall for
mutations.
264 Django + 33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
4.4 KiB
TypeScript
121 lines
4.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useAllauthAPI } from '../../contexts/APIContext'
|
|
import { useStyles } from '../../contexts/StylesContext'
|
|
import { useDjangoFormCore } from 'mizan'
|
|
import { SettingsSection, SettingsItem, SettingsList, Badge, Button } from './SettingsComponents'
|
|
|
|
interface Email {
|
|
email: string
|
|
primary: boolean
|
|
verified: boolean
|
|
}
|
|
|
|
export function EmailsSection() {
|
|
const api = useAllauthAPI()
|
|
const styles = useStyles()
|
|
const [emails, setEmails] = useState<Email[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const addEmailForm = useDjangoFormCore<Record<string, unknown>>({ name: 'add_email' })
|
|
|
|
const fetchEmails = async () => {
|
|
const res = await api.account.emails.list()
|
|
if (res.status === 200 && res.data) {
|
|
setEmails(res.data)
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
useEffect(() => { fetchEmails() }, [])
|
|
|
|
const handleAdd = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
const result = await addEmailForm.submit()
|
|
if (result.success) {
|
|
addEmailForm.reset()
|
|
fetchEmails()
|
|
}
|
|
}
|
|
|
|
const handleRemove = async (email: string) => {
|
|
if (!confirm(`Remove ${email}?`)) return
|
|
await api.account.emails.remove(email)
|
|
fetchEmails()
|
|
}
|
|
|
|
const handleSetPrimary = async (email: string) => {
|
|
await api.account.emails.setPrimary(email)
|
|
fetchEmails()
|
|
}
|
|
|
|
const handleResendVerification = async (email: string) => {
|
|
await api.account.emails.verification.dispatch(email)
|
|
alert('Verification email sent!')
|
|
}
|
|
|
|
if (loading) return null
|
|
|
|
return (
|
|
<SettingsSection title="Email Addresses">
|
|
<SettingsList>
|
|
{emails.map(email => (
|
|
<SettingsItem
|
|
key={email.email}
|
|
label={
|
|
<>
|
|
{email.email}
|
|
{email.primary && <Badge variant="primary">Primary</Badge>}
|
|
{!email.verified && <Badge variant="warning">Unverified</Badge>}
|
|
</>
|
|
}
|
|
actions={
|
|
<>
|
|
{!email.verified && (
|
|
<Button variant="secondary" onClick={() => handleResendVerification(email.email)}>
|
|
Verify
|
|
</Button>
|
|
)}
|
|
{!email.primary && email.verified && (
|
|
<Button onClick={() => handleSetPrimary(email.email)}>
|
|
Make Primary
|
|
</Button>
|
|
)}
|
|
{!email.primary && (
|
|
<Button variant="danger" onClick={() => handleRemove(email.email)}>
|
|
Remove
|
|
</Button>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
))}
|
|
</SettingsList>
|
|
|
|
{!addEmailForm.loading && (
|
|
<form onSubmit={handleAdd} className={styles.inlineForm}>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>
|
|
{addEmailForm.schema?.fields.email?.label || 'Add Email'}
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={(addEmailForm.data.email as string) || ''}
|
|
onChange={(e) => addEmailForm.set('email', e.target.value)}
|
|
onBlur={() => addEmailForm.touch('email')}
|
|
className={styles.fieldInput}
|
|
required
|
|
/>
|
|
{addEmailForm.getFieldErrors('email').map((err, i) => (
|
|
<p key={i} className={styles.fieldError}>{err.message}</p>
|
|
))}
|
|
</div>
|
|
<Button type="submit">
|
|
{addEmailForm.schema?.submit_label || 'Add'}
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</SettingsSection>
|
|
)
|
|
}
|