diff --git a/client/src/app/features/user/components/UserCreateForm.tsx b/client/src/app/features/user/components/UserCreateForm.tsx new file mode 100644 index 0000000..0b8e3af --- /dev/null +++ b/client/src/app/features/user/components/UserCreateForm.tsx @@ -0,0 +1,131 @@ +import { useMemo } from "react"; +import type { FormEvent } from "react"; +import TextField from "@shared/uis/TextField.tsx"; +import Select from "@shared/uis/Select.tsx"; +import { Button } from "@shared/uis/Button.tsx"; +import { ErrorPanel } from "@shared/uis/ErrorPanel.tsx"; +import { useText } from "@shared/locale/ui-text.ts"; +import { AGE_RANGES, SUBSCRIPTION_TIERS } from "../types"; +import type { AgeRange, CreateUserFormValues, SubscriptionTier } from "../types"; +import type { ApiUserDto } from "../apis/user-api"; + +type Props = { + value: CreateUserFormValues; + onChange: (next: CreateUserFormValues) => void; + onSubmit: () => void; + isLoading: boolean; + error: unknown; + createdUser: ApiUserDto | null; +}; + +export function UserCreateForm({ + value, + onChange, + onSubmit, + isLoading, + error, + createdUser, +}: Props) { + const text = useText(); + + const ageRangeOptions = useMemo( + () => AGE_RANGES.map((v) => ({ value: v, label: text.userAgeRangeLabels[v] })), + [text], + ); + + const subscriptionTierOptions = useMemo( + () => SUBSCRIPTION_TIERS.map((v) => ({ value: v, label: text.userSubscriptionTierLabels[v] })), + [text], + ); + + const isPaid = value.subscriptionTier === "paid"; + + const handleField = ( + key: K, + next: CreateUserFormValues[K], + ) => { + onChange({ ...value, [key]: next }); + }; + + const handleSubscriptionTierChange = (next: SubscriptionTier) => { + onChange({ + ...value, + subscriptionTier: next, + subscriptionExpiresAt: next === "free" ? null : value.subscriptionExpiresAt, + }); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + onSubmit(); + }; + + return ( +
+

{text.userCreateTitle}

+ + {createdUser && ( +

+ {text.userCreateSuccess} +

+ )} + + {error != null && } + + handleField("firstName", e.target.value)} + required + /> + handleField("lastName", e.target.value)} + required + /> + handleField("email", e.target.value)} + required + /> + handleField("password", e.target.value)} + required + /> + + id="user-age-range" + label={text.userAgeRange} + value={value.ageRange} + onChange={(next) => handleField("ageRange", next)} + options={ageRangeOptions} + /> + + id="user-subscription-tier" + label={text.userSubscriptionTier} + value={value.subscriptionTier} + onChange={handleSubscriptionTierChange} + options={subscriptionTierOptions} + /> + {isPaid && ( + handleField("subscriptionExpiresAt", e.target.value || null)} + slotProps={{ inputLabel: { shrink: true } }} + required + /> + )} + + + + ); +} diff --git a/client/src/app/routes/AppRoutes.tsx b/client/src/app/routes/AppRoutes.tsx index 3ee64c7..9d935e2 100644 --- a/client/src/app/routes/AppRoutes.tsx +++ b/client/src/app/routes/AppRoutes.tsx @@ -4,6 +4,7 @@ import { WorldHeritageDetailContainer } from "@features/top/containers/world-her import { CriteriaDetailContainer } from "@features/top/containers/criteria-detail-container.tsx"; import { HeritageGalleryContainer } from "@features/top/containers/heritage-gallery-container.tsx"; import { SearchHeritageResultsContainer } from "@features/search/containers/search-heritage-result-container.tsx"; +import { UserCreateContainer } from "@features/user/containers/user-create-container.tsx"; import { BreadcrumbProvider } from "@features/breadcrumbs/BreadCrumbProvider.tsx"; import { LocaleProvider } from "@shared/locale/LocaleProvider.tsx"; import { AppLayout } from "@shared/layout/AppLayout.tsx"; @@ -19,6 +20,7 @@ export function AppRoutes() { } /> } /> } /> + } /> } /> diff --git a/client/src/locals/en/ui.json b/client/src/locals/en/ui.json index 93bea0e..df0c607 100644 --- a/client/src/locals/en/ui.json +++ b/client/src/locals/en/ui.json @@ -63,5 +63,28 @@ "viewAllResults": "View all results", "unescoCriteriaSource": "UNESCO official criteria page", "worldHeritageBasics": "World Heritage Basics", - "worldHeritageBasicsDescription": "Every site is inscribed under one or more of these 10 selection criteria. Explore what each one means." + "worldHeritageBasicsDescription": "Every site is inscribed under one or more of these 10 selection criteria. Explore what each one means.", + "userCreateTitle": "Create User", + "userFirstName": "First Name", + "userLastName": "Last Name", + "userEmail": "Email", + "userPassword": "Password", + "userAgeRange": "Age Range", + "userAgeRangeLabels": { + "teens": "Teens", + "20s": "20s", + "30s": "30s", + "40s": "40s", + "50s": "50s", + "60plus": "60+" + }, + "userSubscriptionTier": "Subscription", + "userSubscriptionTierLabels": { + "free": "Free", + "paid": "Paid" + }, + "userSubscriptionExpiresAt": "Subscription Expires At", + "userCreateSubmit": "Create", + "userCreateSuccess": "User created successfully.", + "userCreateError": "Failed to create user." } diff --git a/client/src/locals/ja/ui.json b/client/src/locals/ja/ui.json index 58b862e..f9929be 100644 --- a/client/src/locals/ja/ui.json +++ b/client/src/locals/ja/ui.json @@ -63,5 +63,28 @@ "viewAllResults": "すべての結果を見る", "unescoCriteriaSource": "UNESCO 公式の登録基準ページ", "worldHeritageBasics": "世界遺産の基礎知識", - "worldHeritageBasicsDescription": "すべての世界遺産は、以下の10個の登録基準のいずれか(または複数)を満たして登録されています。各基準の意味を見てみましょう。" + "worldHeritageBasicsDescription": "すべての世界遺産は、以下の10個の登録基準のいずれか(または複数)を満たして登録されています。各基準の意味を見てみましょう。", + "userCreateTitle": "ユーザー作成", + "userFirstName": "名", + "userLastName": "姓", + "userEmail": "メールアドレス", + "userPassword": "パスワード", + "userAgeRange": "年齢層", + "userAgeRangeLabels": { + "teens": "10代", + "20s": "20代", + "30s": "30代", + "40s": "40代", + "50s": "50代", + "60plus": "60代以上" + }, + "userSubscriptionTier": "サブスクリプション", + "userSubscriptionTierLabels": { + "free": "無料", + "paid": "有料" + }, + "userSubscriptionExpiresAt": "サブスクリプション有効期限", + "userCreateSubmit": "作成", + "userCreateSuccess": "ユーザーを作成しました。", + "userCreateError": "ユーザーの作成に失敗しました。" }