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
11 changes: 11 additions & 0 deletions client/src/app/features/user/apis/index.ts
Original file line number Diff line number Diff line change
@@ -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;
73 changes: 73 additions & 0 deletions client/src/app/features/user/apis/user-api.ts
Original file line number Diff line number Diff line change
@@ -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<ApiUserDto> {
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;
},
};
};
12 changes: 12 additions & 0 deletions client/src/app/features/user/mapper/to-create-user-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { CreateUserFormValues } from "../types";
import type { CreateUserRequest } from "../apis/user-api";

export const toCreateUserRequest = (values: CreateUserFormValues): CreateUserRequest => ({
first_name: values.firstName.trim(),
last_name: values.lastName.trim(),
email: values.email.trim(),
password: values.password,
age_range: values.ageRange,
subscription_tier: values.subscriptionTier,
subscription_expires_at: values.subscriptionTier === "paid" ? values.subscriptionExpiresAt : null,
});
15 changes: 15 additions & 0 deletions client/src/app/features/user/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Loading