diff --git a/client/src/app/features/user/apis/index.ts b/client/src/app/features/user/apis/index.ts new file mode 100644 index 0000000..bf0c2ec --- /dev/null +++ b/client/src/app/features/user/apis/index.ts @@ -0,0 +1,11 @@ +import { createUserApi } from "./user-api.ts"; + +const apiBase = import.meta.env.VITE_API_BASE_URL; + +if (!apiBase) { + throw new Error("VITE_API_BASE_URL is not set"); +} + +const userApi = createUserApi({ apiBase }); + +export const createUser = userApi.createUser; diff --git a/client/src/app/features/user/apis/user-api.ts b/client/src/app/features/user/apis/user-api.ts new file mode 100644 index 0000000..414d6d9 --- /dev/null +++ b/client/src/app/features/user/apis/user-api.ts @@ -0,0 +1,73 @@ +import type { AgeRange, SubscriptionTier } from "../types"; + +export type UserApiDeps = { + apiBase: string; + fetchImpl?: typeof fetch; +}; + +export type CreateUserRequest = { + first_name: string; + last_name: string; + email: string; + password: string; + age_range: AgeRange; + subscription_tier: SubscriptionTier; + subscription_expires_at: string | null; +}; + +export type ApiUserDto = { + id: number; + first_name: string; + last_name: string; + email: string; + age_range: AgeRange; + subscription_tier: SubscriptionTier; + subscription_expires_at: string | null; +}; + +export type ApiCreateUserResponse = + | { status: "success"; data: ApiUserDto } + | { status: "error"; data: unknown }; + +const normalizeApiBase = (apiBase: string): string => apiBase.replace(/\/+$/, ""); + +export const createUserApi = ({ apiBase, fetchImpl = fetch }: UserApiDeps) => { + if (!apiBase) { + throw new Error("apiBase is required"); + } + + const base = normalizeApiBase(apiBase); + const endpoint = `${base}/api/v1/users`; + + const withCommonInit = (init?: RequestInit): RequestInit => ({ + ...init, + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + ...(init?.headers ?? {}), + }, + credentials: init?.credentials ?? "omit", + signal: init?.signal, + }); + + return { + async createUser(request: CreateUserRequest, init?: RequestInit): Promise { + const response = await fetchImpl(endpoint, { + ...withCommonInit(init), + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const json = (await response.json()) as ApiCreateUserResponse; + if (json.status !== "success") { + throw new Error(`API status is not success: ${json.status}`); + } + + return json.data; + }, + }; +}; diff --git a/client/src/app/features/user/types.ts b/client/src/app/features/user/types.ts new file mode 100644 index 0000000..b708150 --- /dev/null +++ b/client/src/app/features/user/types.ts @@ -0,0 +1,15 @@ +export const AGE_RANGES = ["teens", "20s", "30s", "40s", "50s", "60plus"] as const; +export type AgeRange = (typeof AGE_RANGES)[number]; + +export const SUBSCRIPTION_TIERS = ["free", "paid"] as const; +export type SubscriptionTier = (typeof SUBSCRIPTION_TIERS)[number]; + +export type CreateUserFormValues = { + firstName: string; + lastName: string; + email: string; + password: string; + ageRange: AgeRange; + subscriptionTier: SubscriptionTier; + subscriptionExpiresAt: string | null; +};