Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions client/src/app/features/user/components/UserCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -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 = <K extends keyof CreateUserFormValues>(
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 (
<form onSubmit={handleSubmit} className="mx-auto flex max-w-xl flex-col gap-4 px-4 py-8">
<h1 className="text-xl font-bold">{text.userCreateTitle}</h1>

{createdUser && (
<p className="rounded-lg bg-green-50 px-4 py-3 text-sm text-green-700">
{text.userCreateSuccess}
</p>
)}

{error != null && <ErrorPanel message={text.userCreateError} />}

<TextField
label={text.userFirstName}
value={value.firstName}
onChange={(e) => handleField("firstName", e.target.value)}
required
/>
<TextField
label={text.userLastName}
value={value.lastName}
onChange={(e) => handleField("lastName", e.target.value)}
required
/>
<TextField
label={text.userEmail}
type="email"
value={value.email}
onChange={(e) => handleField("email", e.target.value)}
required
/>
<TextField
label={text.userPassword}
type="password"
value={value.password}
onChange={(e) => handleField("password", e.target.value)}
required
/>
<Select<AgeRange>
id="user-age-range"
label={text.userAgeRange}
value={value.ageRange}
onChange={(next) => handleField("ageRange", next)}
options={ageRangeOptions}
/>
<Select<SubscriptionTier>
id="user-subscription-tier"
label={text.userSubscriptionTier}
value={value.subscriptionTier}
onChange={handleSubscriptionTierChange}
options={subscriptionTierOptions}
/>
{isPaid && (
<TextField
label={text.userSubscriptionExpiresAt}
type="date"
value={value.subscriptionExpiresAt ?? ""}
onChange={(e) => handleField("subscriptionExpiresAt", e.target.value || null)}
slotProps={{ inputLabel: { shrink: true } }}
required
/>
)}

<Button type="submit" variant="primary" isLoading={isLoading}>
{text.userCreateSubmit}
</Button>
</form>
);
}
2 changes: 2 additions & 0 deletions client/src/app/routes/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,6 +20,7 @@ export function AppRoutes() {
<Route path="/heritages/criteria/:code" element={<CriteriaDetailContainer />} />
<Route path="/heritages/:id/gallery" element={<HeritageGalleryContainer />} />
<Route path="/heritages/:id" element={<WorldHeritageDetailContainer />} />
<Route path="/users/new" element={<UserCreateContainer />} />
<Route path="*" element={<Navigate to="/heritages" replace />} />
</Routes>
</AppLayout>
Expand Down
25 changes: 24 additions & 1 deletion client/src/locals/en/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
25 changes: 24 additions & 1 deletion client/src/locals/ja/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "ユーザーの作成に失敗しました。"
}
Loading