@open-mercato/core 0.6.6-develop.6408.1.0cbfbd7476 → 0.6.6-develop.6409.1.4bfed821c0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +29 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +31 -5
- package/src/modules/portal/i18n/de.json +3 -0
- package/src/modules/portal/i18n/en.json +3 -0
- package/src/modules/portal/i18n/es.json +3 -0
- package/src/modules/portal/i18n/pl.json +3 -0
|
@@ -24,12 +24,14 @@ function PortalSignupPage({ params }) {
|
|
|
24
24
|
const [email, setEmail] = useState("");
|
|
25
25
|
const [password, setPassword] = useState("");
|
|
26
26
|
const [error, setError] = useState(null);
|
|
27
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
27
28
|
const [success, setSuccess] = useState(false);
|
|
28
29
|
const [submitting, setSubmitting] = useState(false);
|
|
29
30
|
const handleSubmit = useCallback(
|
|
30
31
|
async (event) => {
|
|
31
32
|
event.preventDefault();
|
|
32
33
|
setError(null);
|
|
34
|
+
setFieldErrors({});
|
|
33
35
|
if (!tenant.organizationId) {
|
|
34
36
|
setError(t("portal.org.invalid", "Organization not found."));
|
|
35
37
|
return;
|
|
@@ -45,7 +47,27 @@ function PortalSignupPage({ params }) {
|
|
|
45
47
|
setSuccess(true);
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
const details = result.result?.details;
|
|
51
|
+
const detailEntries = details ? Object.entries(details).filter(([, messages]) => (messages?.length ?? 0) > 0) : [];
|
|
52
|
+
if (detailEntries.length > 0) {
|
|
53
|
+
const mapped = {};
|
|
54
|
+
for (const [fieldKey] of detailEntries) {
|
|
55
|
+
if (fieldKey === "displayName") {
|
|
56
|
+
mapped.displayName = t("portal.signup.error.displayName.required", "Full name is required.");
|
|
57
|
+
} else if (fieldKey === "email") {
|
|
58
|
+
mapped.email = t("portal.signup.error.email.invalid", "Please enter a valid email address.");
|
|
59
|
+
} else if (fieldKey === "password") {
|
|
60
|
+
mapped.password = t("portal.signup.error.password.minLength", "Password must be at least 8 characters.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const hasUnmappedField = detailEntries.some(([fieldKey]) => !(fieldKey in mapped));
|
|
64
|
+
setFieldErrors(mapped);
|
|
65
|
+
if (hasUnmappedField || Object.keys(mapped).length === 0) {
|
|
66
|
+
setError(result.result?.error || t("portal.signup.error.generic", "Signup failed. Please try again."));
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
setError(result.result?.error || t("portal.signup.error.generic", "Signup failed. Please try again."));
|
|
70
|
+
}
|
|
49
71
|
} catch {
|
|
50
72
|
setError(t("portal.signup.error.generic", "Signup failed. Please try again."));
|
|
51
73
|
} finally {
|
|
@@ -93,15 +115,18 @@ function PortalSignupPage({ params }) {
|
|
|
93
115
|
error ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null,
|
|
94
116
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
95
117
|
/* @__PURE__ */ jsx(Label, { htmlFor: "signup-name", className: "text-overline font-semibold uppercase tracking-wider text-muted-foreground/70", children: t("portal.signup.displayName", "Full Name") }),
|
|
96
|
-
/* @__PURE__ */ jsx(Input, { id: "signup-name", type: "text", autoComplete: "name", required: true, placeholder: t("portal.signup.displayName.placeholder", "Jane Smith"), value: displayName, onChange: (e) => setDisplayName(e.target.value), disabled: submitting, className: "rounded-lg" })
|
|
118
|
+
/* @__PURE__ */ jsx(Input, { id: "signup-name", type: "text", autoComplete: "name", required: true, placeholder: t("portal.signup.displayName.placeholder", "Jane Smith"), value: displayName, onChange: (e) => setDisplayName(e.target.value), disabled: submitting, "aria-invalid": fieldErrors.displayName ? true : void 0, "aria-describedby": fieldErrors.displayName ? "signup-name-error" : void 0, className: "rounded-lg" }),
|
|
119
|
+
fieldErrors.displayName && /* @__PURE__ */ jsx("p", { id: "signup-name-error", role: "alert", className: "text-sm text-destructive", children: fieldErrors.displayName })
|
|
97
120
|
] }),
|
|
98
121
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
99
122
|
/* @__PURE__ */ jsx(Label, { htmlFor: "signup-email", className: "text-overline font-semibold uppercase tracking-wider text-muted-foreground/70", children: t("portal.signup.email", "Email") }),
|
|
100
|
-
/* @__PURE__ */ jsx(EmailInput, { id: "signup-email", required: true, placeholder: t("portal.signup.email.placeholder", "you@example.com"), value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting, className: "rounded-lg" })
|
|
123
|
+
/* @__PURE__ */ jsx(EmailInput, { id: "signup-email", required: true, placeholder: t("portal.signup.email.placeholder", "you@example.com"), value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting, "aria-invalid": fieldErrors.email ? true : void 0, "aria-describedby": fieldErrors.email ? "signup-email-error" : void 0, className: "rounded-lg" }),
|
|
124
|
+
fieldErrors.email && /* @__PURE__ */ jsx("p", { id: "signup-email-error", role: "alert", className: "text-sm text-destructive", children: fieldErrors.email })
|
|
101
125
|
] }),
|
|
102
126
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
103
127
|
/* @__PURE__ */ jsx(Label, { htmlFor: "signup-password", className: "text-overline font-semibold uppercase tracking-wider text-muted-foreground/70", children: t("portal.signup.password", "Password") }),
|
|
104
|
-
/* @__PURE__ */ jsx(PasswordInput, { id: "signup-password", autoComplete: "new-password", required: true, placeholder: t("portal.signup.password.placeholder", "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"), value: password, onChange: (e) => setPassword(e.target.value), disabled: submitting, className: "rounded-lg" })
|
|
128
|
+
/* @__PURE__ */ jsx(PasswordInput, { id: "signup-password", autoComplete: "new-password", required: true, placeholder: t("portal.signup.password.placeholder", "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"), value: password, onChange: (e) => setPassword(e.target.value), disabled: submitting, "aria-invalid": fieldErrors.password ? true : void 0, "aria-describedby": fieldErrors.password ? "signup-password-error" : void 0, className: "rounded-lg" }),
|
|
129
|
+
fieldErrors.password && /* @__PURE__ */ jsx("p", { id: "signup-password-error", role: "alert", className: "text-sm text-destructive", children: fieldErrors.password })
|
|
105
130
|
] }),
|
|
106
131
|
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: submitting, className: "mt-1 w-full rounded-lg", children: submitting ? t("portal.signup.submitting", "Creating account...") : t("portal.signup.submit", "Create Account") }),
|
|
107
132
|
/* @__PURE__ */ jsxs("p", { className: "text-center text-sm text-muted-foreground", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/signup/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useCallback, useMemo, useState } from 'react'\nimport Link from 'next/link'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { SearchX, Check } from 'lucide-react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'\n\ntype Props = { params: { orgSlug: string } }\ntype SignupResponse = { ok: boolean; error?: string }\n\nexport default function PortalSignupPage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const { tenant } = usePortalContext()\n\n const [displayName, setDisplayName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [success, setSuccess] = useState(false)\n const [submitting, setSubmitting] = useState(false)\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent) => {\n event.preventDefault()\n setError(null)\n\n if (!tenant.organizationId) {\n setError(t('portal.org.invalid', 'Organization not found.'))\n return\n }\n\n setSubmitting(true)\n try {\n const result = await apiCall<SignupResponse>('/api/customer_accounts/signup', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password, displayName, organizationId: tenant.organizationId }),\n })\n\n if (result.status === 202 && result.result?.ok) {\n setSuccess(true)\n return\n }\n\n setError(result.result?.error || t('portal.signup.error.generic', 'Signup failed. Please try again.'))\n } catch {\n setError(t('portal.signup.error.generic', 'Signup failed. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n },\n [displayName, email, password, tenant.organizationId, t],\n )\n\n const injectionContext = useMemo(\n () => ({ orgSlug }),\n [orgSlug],\n )\n\n if (tenant.loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (tenant.error) {\n return (\n <div className=\"mx-auto w-full max-w-md py-12\">\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<SearchX className=\"h-6 w-6\" aria-hidden />}\n title={t('portal.org.invalid', 'Organization not found.')}\n />\n </div>\n )\n }\n\n if (success) {\n return (\n <div className=\"mx-auto w-full max-w-sm text-center\">\n <div className=\"mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-foreground text-background\">\n <Check className=\"size-6\" />\n </div>\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.signup.success.title', 'Check your email')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">{t(\n 'portal.signup.success.description',\n 'If your registration was accepted, check your email for next steps before signing in. Some organizations require an administrator to activate new accounts.',\n )}</p>\n <Button asChild className=\"mt-6 w-full rounded-lg\">\n <Link href={`/${orgSlug}/portal/login`}>{t('portal.signup.success.loginLink', 'Sign In')}</Link>\n </Button>\n </div>\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-sm\">\n <div className=\"mb-8 text-center\">\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.signup.title', 'Create Account')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">{t('portal.signup.description', 'Sign up for a portal account.')}</p>\n </div>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageBefore('signup')} context={injectionContext} />\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\">\n {error ? (\n <Alert variant=\"destructive\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"signup-name\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.signup.displayName', 'Full Name')}</Label>\n <Input id=\"signup-name\" type=\"text\" autoComplete=\"name\" required placeholder={t('portal.signup.displayName.placeholder', 'Jane Smith')} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting} className=\"rounded-lg\" />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"signup-email\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.signup.email', 'Email')}</Label>\n <EmailInput id=\"signup-email\" required placeholder={t('portal.signup.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} className=\"rounded-lg\" />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"signup-password\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.signup.password', 'Password')}</Label>\n <PasswordInput id=\"signup-password\" autoComplete=\"new-password\" required placeholder={t('portal.signup.password.placeholder', '\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} className=\"rounded-lg\" />\n </div>\n\n <Button type=\"submit\" disabled={submitting} className=\"mt-1 w-full rounded-lg\">\n {submitting ? t('portal.signup.submitting', 'Creating account...') : t('portal.signup.submit', 'Create Account')}\n </Button>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n {t('portal.signup.hasAccount', 'Already have an account?')}{' '}\n <Link href={`/${orgSlug}/portal/login`} className=\"font-medium text-foreground underline underline-offset-4 hover:opacity-80\">\n {t('portal.signup.loginLink', 'Sign in')}\n </Link>\n </p>\n </form>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageAfter('signup')} context={injectionContext} />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useCallback, useMemo, useState } from 'react'\nimport Link from 'next/link'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { SearchX, Check } from 'lucide-react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'\n\ntype Props = { params: { orgSlug: string } }\ntype SignupResponse = { ok: boolean; error?: string; details?: Record<string, string[]> }\n\nexport default function PortalSignupPage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const { tenant } = usePortalContext()\n\n const [displayName, setDisplayName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})\n const [success, setSuccess] = useState(false)\n const [submitting, setSubmitting] = useState(false)\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent) => {\n event.preventDefault()\n setError(null)\n setFieldErrors({})\n\n if (!tenant.organizationId) {\n setError(t('portal.org.invalid', 'Organization not found.'))\n return\n }\n\n setSubmitting(true)\n try {\n const result = await apiCall<SignupResponse>('/api/customer_accounts/signup', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password, displayName, organizationId: tenant.organizationId }),\n })\n\n if (result.status === 202 && result.result?.ok) {\n setSuccess(true)\n return\n }\n\n const details = result.result?.details\n const detailEntries = details ? Object.entries(details).filter(([, messages]) => (messages?.length ?? 0) > 0) : []\n if (detailEntries.length > 0) {\n // API detail values are raw untranslated validator strings; each known field maps to one translated message instead.\n const mapped: Record<string, string> = {}\n for (const [fieldKey] of detailEntries) {\n if (fieldKey === 'displayName') {\n mapped.displayName = t('portal.signup.error.displayName.required', 'Full name is required.')\n } else if (fieldKey === 'email') {\n mapped.email = t('portal.signup.error.email.invalid', 'Please enter a valid email address.')\n } else if (fieldKey === 'password') {\n mapped.password = t('portal.signup.error.password.minLength', 'Password must be at least 8 characters.')\n }\n }\n const hasUnmappedField = detailEntries.some(([fieldKey]) => !(fieldKey in mapped))\n setFieldErrors(mapped)\n if (hasUnmappedField || Object.keys(mapped).length === 0) {\n setError(result.result?.error || t('portal.signup.error.generic', 'Signup failed. Please try again.'))\n }\n } else {\n setError(result.result?.error || t('portal.signup.error.generic', 'Signup failed. Please try again.'))\n }\n } catch {\n setError(t('portal.signup.error.generic', 'Signup failed. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n },\n [displayName, email, password, tenant.organizationId, t],\n )\n\n const injectionContext = useMemo(\n () => ({ orgSlug }),\n [orgSlug],\n )\n\n if (tenant.loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (tenant.error) {\n return (\n <div className=\"mx-auto w-full max-w-md py-12\">\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<SearchX className=\"h-6 w-6\" aria-hidden />}\n title={t('portal.org.invalid', 'Organization not found.')}\n />\n </div>\n )\n }\n\n if (success) {\n return (\n <div className=\"mx-auto w-full max-w-sm text-center\">\n <div className=\"mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-foreground text-background\">\n <Check className=\"size-6\" />\n </div>\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.signup.success.title', 'Check your email')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">{t(\n 'portal.signup.success.description',\n 'If your registration was accepted, check your email for next steps before signing in. Some organizations require an administrator to activate new accounts.',\n )}</p>\n <Button asChild className=\"mt-6 w-full rounded-lg\">\n <Link href={`/${orgSlug}/portal/login`}>{t('portal.signup.success.loginLink', 'Sign In')}</Link>\n </Button>\n </div>\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-sm\">\n <div className=\"mb-8 text-center\">\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.signup.title', 'Create Account')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">{t('portal.signup.description', 'Sign up for a portal account.')}</p>\n </div>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageBefore('signup')} context={injectionContext} />\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\">\n {error ? (\n <Alert variant=\"destructive\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"signup-name\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.signup.displayName', 'Full Name')}</Label>\n <Input id=\"signup-name\" type=\"text\" autoComplete=\"name\" required placeholder={t('portal.signup.displayName.placeholder', 'Jane Smith')} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting} aria-invalid={fieldErrors.displayName ? true : undefined} aria-describedby={fieldErrors.displayName ? 'signup-name-error' : undefined} className=\"rounded-lg\" />\n {fieldErrors.displayName && <p id=\"signup-name-error\" role=\"alert\" className=\"text-sm text-destructive\">{fieldErrors.displayName}</p>}\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"signup-email\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.signup.email', 'Email')}</Label>\n <EmailInput id=\"signup-email\" required placeholder={t('portal.signup.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} aria-invalid={fieldErrors.email ? true : undefined} aria-describedby={fieldErrors.email ? 'signup-email-error' : undefined} className=\"rounded-lg\" />\n {fieldErrors.email && <p id=\"signup-email-error\" role=\"alert\" className=\"text-sm text-destructive\">{fieldErrors.email}</p>}\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"signup-password\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.signup.password', 'Password')}</Label>\n <PasswordInput id=\"signup-password\" autoComplete=\"new-password\" required placeholder={t('portal.signup.password.placeholder', '\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} aria-invalid={fieldErrors.password ? true : undefined} aria-describedby={fieldErrors.password ? 'signup-password-error' : undefined} className=\"rounded-lg\" />\n {fieldErrors.password && <p id=\"signup-password-error\" role=\"alert\" className=\"text-sm text-destructive\">{fieldErrors.password}</p>}\n </div>\n\n <Button type=\"submit\" disabled={submitting} className=\"mt-1 w-full rounded-lg\">\n {submitting ? t('portal.signup.submitting', 'Creating account...') : t('portal.signup.submit', 'Create Account')}\n </Button>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n {t('portal.signup.hasAccount', 'Already have an account?')}{' '}\n <Link href={`/${orgSlug}/portal/login`} className=\"font-medium text-foreground underline underline-offset-4 hover:opacity-80\">\n {t('portal.signup.loginLink', 'Sign in')}\n </Link>\n </p>\n </form>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageAfter('signup')} context={injectionContext} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6409.1.4bfed821c0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -253,16 +253,16 @@
|
|
|
253
253
|
"zod": "^4.4.3"
|
|
254
254
|
},
|
|
255
255
|
"peerDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6409.1.4bfed821c0",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6409.1.4bfed821c0",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6409.1.4bfed821c0",
|
|
259
259
|
"react": "^19.0.0",
|
|
260
260
|
"react-dom": "^19.0.0"
|
|
261
261
|
},
|
|
262
262
|
"devDependencies": {
|
|
263
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
264
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
265
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
263
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6409.1.4bfed821c0",
|
|
264
|
+
"@open-mercato/shared": "0.6.6-develop.6409.1.4bfed821c0",
|
|
265
|
+
"@open-mercato/ui": "0.6.6-develop.6409.1.4bfed821c0",
|
|
266
266
|
"@testing-library/dom": "^10.4.1",
|
|
267
267
|
"@testing-library/jest-dom": "^6.9.1",
|
|
268
268
|
"@testing-library/react": "^16.3.1",
|
|
@@ -17,7 +17,7 @@ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
|
17
17
|
import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
|
|
18
18
|
|
|
19
19
|
type Props = { params: { orgSlug: string } }
|
|
20
|
-
type SignupResponse = { ok: boolean; error?: string }
|
|
20
|
+
type SignupResponse = { ok: boolean; error?: string; details?: Record<string, string[]> }
|
|
21
21
|
|
|
22
22
|
export default function PortalSignupPage({ params }: Props) {
|
|
23
23
|
const t = useT()
|
|
@@ -28,6 +28,7 @@ export default function PortalSignupPage({ params }: Props) {
|
|
|
28
28
|
const [email, setEmail] = useState('')
|
|
29
29
|
const [password, setPassword] = useState('')
|
|
30
30
|
const [error, setError] = useState<string | null>(null)
|
|
31
|
+
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
|
|
31
32
|
const [success, setSuccess] = useState(false)
|
|
32
33
|
const [submitting, setSubmitting] = useState(false)
|
|
33
34
|
|
|
@@ -35,6 +36,7 @@ export default function PortalSignupPage({ params }: Props) {
|
|
|
35
36
|
async (event: React.FormEvent) => {
|
|
36
37
|
event.preventDefault()
|
|
37
38
|
setError(null)
|
|
39
|
+
setFieldErrors({})
|
|
38
40
|
|
|
39
41
|
if (!tenant.organizationId) {
|
|
40
42
|
setError(t('portal.org.invalid', 'Organization not found.'))
|
|
@@ -54,7 +56,28 @@ export default function PortalSignupPage({ params }: Props) {
|
|
|
54
56
|
return
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
const details = result.result?.details
|
|
60
|
+
const detailEntries = details ? Object.entries(details).filter(([, messages]) => (messages?.length ?? 0) > 0) : []
|
|
61
|
+
if (detailEntries.length > 0) {
|
|
62
|
+
// API detail values are raw untranslated validator strings; each known field maps to one translated message instead.
|
|
63
|
+
const mapped: Record<string, string> = {}
|
|
64
|
+
for (const [fieldKey] of detailEntries) {
|
|
65
|
+
if (fieldKey === 'displayName') {
|
|
66
|
+
mapped.displayName = t('portal.signup.error.displayName.required', 'Full name is required.')
|
|
67
|
+
} else if (fieldKey === 'email') {
|
|
68
|
+
mapped.email = t('portal.signup.error.email.invalid', 'Please enter a valid email address.')
|
|
69
|
+
} else if (fieldKey === 'password') {
|
|
70
|
+
mapped.password = t('portal.signup.error.password.minLength', 'Password must be at least 8 characters.')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const hasUnmappedField = detailEntries.some(([fieldKey]) => !(fieldKey in mapped))
|
|
74
|
+
setFieldErrors(mapped)
|
|
75
|
+
if (hasUnmappedField || Object.keys(mapped).length === 0) {
|
|
76
|
+
setError(result.result?.error || t('portal.signup.error.generic', 'Signup failed. Please try again.'))
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
setError(result.result?.error || t('portal.signup.error.generic', 'Signup failed. Please try again.'))
|
|
80
|
+
}
|
|
58
81
|
} catch {
|
|
59
82
|
setError(t('portal.signup.error.generic', 'Signup failed. Please try again.'))
|
|
60
83
|
} finally {
|
|
@@ -122,17 +145,20 @@ export default function PortalSignupPage({ params }: Props) {
|
|
|
122
145
|
|
|
123
146
|
<div className="flex flex-col gap-1.5">
|
|
124
147
|
<Label htmlFor="signup-name" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.displayName', 'Full Name')}</Label>
|
|
125
|
-
<Input id="signup-name" type="text" autoComplete="name" required placeholder={t('portal.signup.displayName.placeholder', 'Jane Smith')} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
148
|
+
<Input id="signup-name" type="text" autoComplete="name" required placeholder={t('portal.signup.displayName.placeholder', 'Jane Smith')} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting} aria-invalid={fieldErrors.displayName ? true : undefined} aria-describedby={fieldErrors.displayName ? 'signup-name-error' : undefined} className="rounded-lg" />
|
|
149
|
+
{fieldErrors.displayName && <p id="signup-name-error" role="alert" className="text-sm text-destructive">{fieldErrors.displayName}</p>}
|
|
126
150
|
</div>
|
|
127
151
|
|
|
128
152
|
<div className="flex flex-col gap-1.5">
|
|
129
153
|
<Label htmlFor="signup-email" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.email', 'Email')}</Label>
|
|
130
|
-
<EmailInput id="signup-email" required placeholder={t('portal.signup.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
154
|
+
<EmailInput id="signup-email" required placeholder={t('portal.signup.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} aria-invalid={fieldErrors.email ? true : undefined} aria-describedby={fieldErrors.email ? 'signup-email-error' : undefined} className="rounded-lg" />
|
|
155
|
+
{fieldErrors.email && <p id="signup-email-error" role="alert" className="text-sm text-destructive">{fieldErrors.email}</p>}
|
|
131
156
|
</div>
|
|
132
157
|
|
|
133
158
|
<div className="flex flex-col gap-1.5">
|
|
134
159
|
<Label htmlFor="signup-password" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.password', 'Password')}</Label>
|
|
135
|
-
<PasswordInput id="signup-password" autoComplete="new-password" required placeholder={t('portal.signup.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} className="rounded-lg" />
|
|
160
|
+
<PasswordInput id="signup-password" autoComplete="new-password" required placeholder={t('portal.signup.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} aria-invalid={fieldErrors.password ? true : undefined} aria-describedby={fieldErrors.password ? 'signup-password-error' : undefined} className="rounded-lg" />
|
|
161
|
+
{fieldErrors.password && <p id="signup-password-error" role="alert" className="text-sm text-destructive">{fieldErrors.password}</p>}
|
|
136
162
|
</div>
|
|
137
163
|
|
|
138
164
|
<Button type="submit" disabled={submitting} className="mt-1 w-full rounded-lg">
|
|
@@ -98,7 +98,10 @@
|
|
|
98
98
|
"portal.signup.displayName.placeholder": "Ihr Name",
|
|
99
99
|
"portal.signup.email": "E-Mail",
|
|
100
100
|
"portal.signup.email.placeholder": "sie@example.com",
|
|
101
|
+
"portal.signup.error.displayName.required": "Der vollständige Name ist erforderlich.",
|
|
102
|
+
"portal.signup.error.email.invalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
|
101
103
|
"portal.signup.error.generic": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
|
104
|
+
"portal.signup.error.password.minLength": "Das Passwort muss mindestens 8 Zeichen lang sein.",
|
|
102
105
|
"portal.signup.hasAccount": "Bereits ein Konto?",
|
|
103
106
|
"portal.signup.loginLink": "Anmelden",
|
|
104
107
|
"portal.signup.password": "Passwort",
|
|
@@ -98,7 +98,10 @@
|
|
|
98
98
|
"portal.signup.displayName.placeholder": "Your name",
|
|
99
99
|
"portal.signup.email": "Email",
|
|
100
100
|
"portal.signup.email.placeholder": "you@example.com",
|
|
101
|
+
"portal.signup.error.displayName.required": "Full name is required.",
|
|
102
|
+
"portal.signup.error.email.invalid": "Please enter a valid email address.",
|
|
101
103
|
"portal.signup.error.generic": "Registration failed. Please try again.",
|
|
104
|
+
"portal.signup.error.password.minLength": "Password must be at least 8 characters.",
|
|
102
105
|
"portal.signup.hasAccount": "Already have an account?",
|
|
103
106
|
"portal.signup.loginLink": "Log in",
|
|
104
107
|
"portal.signup.password": "Password",
|
|
@@ -98,7 +98,10 @@
|
|
|
98
98
|
"portal.signup.displayName.placeholder": "Tu nombre",
|
|
99
99
|
"portal.signup.email": "Correo electrónico",
|
|
100
100
|
"portal.signup.email.placeholder": "tu@ejemplo.com",
|
|
101
|
+
"portal.signup.error.displayName.required": "El nombre completo es obligatorio.",
|
|
102
|
+
"portal.signup.error.email.invalid": "Introduce una dirección de correo electrónico válida.",
|
|
101
103
|
"portal.signup.error.generic": "Error en el registro. Inténtalo de nuevo.",
|
|
104
|
+
"portal.signup.error.password.minLength": "La contraseña debe tener al menos 8 caracteres.",
|
|
102
105
|
"portal.signup.hasAccount": "¿Ya tienes cuenta?",
|
|
103
106
|
"portal.signup.loginLink": "Inicia sesión",
|
|
104
107
|
"portal.signup.password": "Contraseña",
|
|
@@ -98,7 +98,10 @@
|
|
|
98
98
|
"portal.signup.displayName.placeholder": "Twoje imię",
|
|
99
99
|
"portal.signup.email": "Email",
|
|
100
100
|
"portal.signup.email.placeholder": "ty@example.com",
|
|
101
|
+
"portal.signup.error.displayName.required": "Imię i nazwisko jest wymagane.",
|
|
102
|
+
"portal.signup.error.email.invalid": "Podaj prawidłowy adres e-mail.",
|
|
101
103
|
"portal.signup.error.generic": "Rejestracja nie powiodła się. Spróbuj ponownie.",
|
|
104
|
+
"portal.signup.error.password.minLength": "Hasło musi mieć co najmniej 8 znaków.",
|
|
102
105
|
"portal.signup.hasAccount": "Masz już konto?",
|
|
103
106
|
"portal.signup.loginLink": "Zaloguj się",
|
|
104
107
|
"portal.signup.password": "Hasło",
|