Skip to content

Commit

Permalink
๐Ÿšง ใŠๅ•ใ„ๅˆใ‚ใ› API ใจใฎ้€šไฟก
Browse files Browse the repository at this point in the history
  • Loading branch information
wappon28dev committed Apr 1, 2024
1 parent 98a9af0 commit 2219ebb
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 26 deletions.
5 changes: 3 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"sourceType": "module",
"project": ["./api/tsconfig.json", "./client/tsconfig.json"]
},
"plugins": ["react", "unused-imports"],
"plugins": ["react", "unused-imports", "neverthrow"],
"overrides": [
{
"files": ["*.astro"],
Expand Down Expand Up @@ -48,7 +48,8 @@
"alphabetize": { "order": "asc" }
}
],
"unused-imports/no-unused-imports": "error"
"unused-imports/no-unused-imports": "error",
"neverthrow/must-use-result": "warn"
},
"settings": {
"@pandacss/configPath": "./client/panda.config.ts"
Expand Down
1 change: 1 addition & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[tools]
node = "21"
bun = "1.1.0"
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"mailchannels",
"microcms",
"MICROCMS",
"nanostores"
"nanostores",
"neverthrow"
],
"eslint.validate": ["astro"],
"vitest.enable": true,
Expand Down
6 changes: 3 additions & 3 deletions api/src/api/v1/contact/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { getModeName, type ENV, type HonoType } from "@api/lib/consts";
import { authGuard } from "@api/lib/middlewares/contact";
import { authGuard, configureCors } from "@api/lib/middlewares/contact";
import { type EmailAddress, getPersonalizationInfo } from "@api/lib/sender";
import { INFO } from "@client/lib/config";
import { formatDate, getEntries } from "@client/lib/consts";
Expand Down Expand Up @@ -167,7 +167,7 @@ async function sendDiscordWebhook(

export const contact = new Hono<HonoType>()
.options("*", cors())
.use("/*", authGuard)
.use("*", authGuard, configureCors)
.post("/", zValidator("json", zContactFormData), async (ctx) => {
const data = ctx.req.valid("json");
const acceptDate = new Date();
Expand Down Expand Up @@ -198,5 +198,5 @@ export const contact = new Hono<HonoType>()
});
}

return ctx.text("", 204);
return ctx.json({ acceptDate }, 201);
});
10 changes: 10 additions & 0 deletions api/src/lib/middlewares/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { type HonoType, getContactManifests } from "@api/lib/consts";
import { type MiddlewareHandler } from "hono";
import { bearerAuth } from "hono/bearer-auth";
import { cors } from "hono/cors";
import { HTTPException } from "hono/http-exception";

export const authGuard: MiddlewareHandler<HonoType> = async (ctx, next) => {
Expand All @@ -26,3 +27,12 @@ export const authGuard: MiddlewareHandler<HonoType> = async (ctx, next) => {

return await bearerAuth({ token: accessKey })(ctx, next);
};

export const configureCors: MiddlewareHandler<HonoType> = async (ctx, next) => {
const { allowedHosts } = getContactManifests(ctx.env);

return await cors({
origin: allowedHosts,
exposeHeaders: ["*"],
})(ctx, next);
};
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"microcms-js-sdk": "^2.7.0",
"microcms-typescript": "^1.0.14",
"nanostores": "^0.10.0",
"neverthrow": "^6.1.0",
"react": "^18.2.0",
"hono": "^4.1.5",
"react-dom": "^18.2.0",
Expand Down
26 changes: 22 additions & 4 deletions client/src/components/contact/ContactDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { waitMs } from "@client/lib/consts";
import { useEffect, useMemo, useState } from "react";
import type { ReactElement } from "react";
import { Dialog } from "../Dialog";
import { ContactDialogContent } from "./ContactDialogContent";
import { ContactDialogResult } from "./ContactDialogResult";
import { postContactFormData } from "src/lib/services/api";
import { $contactFormData } from "src/lib/store/ui";
import { useStore } from "@nanostores/react";
import type { InferAsyncErrTypes } from "src/lib/types/result";

export type SubmitState =
| {
Expand All @@ -18,7 +21,7 @@ export type SubmitState =
}
| {
state: "failure";
error: Error;
error: InferAsyncErrTypes<ReturnType<typeof postContactFormData>>;
};

export function ContactDialog({
Expand All @@ -34,15 +37,30 @@ export function ContactDialog({
submitState.state === "confirming" || submitState.state === "submitting",
[submitState],
);
const formData = useStore($contactFormData);

useEffect(() => {
setSubmitState({ state: "confirming" });
}, []);

async function handleSubmit(): Promise<void> {
setSubmitState({ state: "submitting" });
await waitMs(3000);
setSubmitState({ state: "success", acceptDate: new Date() });

console.log("submitting...");
const res = await postContactFormData(formData);

if (res.isErr()) {
setSubmitState({ state: "failure", error: res.error });
console.warn(res.error);
return;
}

console.log("success!");
console.log(res.value);
setSubmitState({
state: "success",
acceptDate: new Date(res.value.acceptDate),
});
}

return (
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/contact/ContactDialogContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function ContactDialogContent({
้€ไฟกใ—ใฆใ‚‚ใ‚ˆใ‚ใ—ใ„ใงใ™ใ‹๏ผŸ
<p.hr color="9u-brown" />
</AlertDialog.Title>
<AlertDialog.Description>
<AlertDialog.Description asChild>
<VStack alignItems="flex-start" className={formStyle} gap="5" py="2">
{getEntries(formSchema).map(
([key, { description, formType, inputType }]) => (
Expand Down
38 changes: 25 additions & 13 deletions client/src/components/contact/ContactDialogResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,13 @@ import {
} from "react";
import type { SubmitState } from "./ContactDialog";

export function ContactDialogResult({
function Success({
setSubmitState,
setHeight,
}: {
// eslint-disable-next-line react/no-unused-prop-types
submitState: SubmitState;
setSubmitState: Dispatch<SetStateAction<SubmitState>>;
setHeight: Dispatch<SetStateAction<number | undefined>>;
}): ReactElement {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
setHeight(ref.current?.offsetHeight);
}, []);

return (
<p.div ref={ref} h="fit-content">
<>
<AlertDialog.Title
className={css({
fontWeight: "bold",
Expand All @@ -37,7 +27,7 @@ export function ContactDialogResult({
้€ไฟกใŒๅฎŒไบ†ใ—ใพใ—ใŸ
<p.hr color="9u-brown" />
</AlertDialog.Title>
<AlertDialog.Description>
<AlertDialog.Description asChild>
<p.div h="200px" p="3">
<p.p textAlign="center">ๆœฌๆ–‡ใงใ™ใ€‚</p.p>
</p.div>
Expand Down Expand Up @@ -70,6 +60,28 @@ export function ContactDialogResult({
้–‰ใ˜ใ‚‹
</p.button>
</HStack>
</>
);
}

export function ContactDialogResult({
setSubmitState,
setHeight,
}: {
// eslint-disable-next-line react/no-unused-prop-types
submitState: SubmitState;
setSubmitState: Dispatch<SetStateAction<SubmitState>>;
setHeight: Dispatch<SetStateAction<number | undefined>>;
}): ReactElement {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
setHeight(ref.current?.offsetHeight);
}, []);

return (
<p.div ref={ref} h="fit-content">
<Success setSubmitState={setSubmitState} />
</p.div>
);
}
53 changes: 51 additions & 2 deletions client/src/lib/services/api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
/* eslint-disable no-console */
import type { AppType } from "@api/index";
import { hc } from "hono/client";
import { hc, type InferResponseType } from "hono/client";
import { ResultAsync, err } from "neverthrow";
import type { ContactFormData } from "./contact";

const ENDPOINT = import.meta.env.PUBLIC_CLIENT_API_ENDPOINT;
// const ACCESS_TOKEN = import.meta.env.PUBLIC_CLIENT_API_ACCESS_TOKEN;
const ACCESS_TOKEN = import.meta.env.PUBLIC_CLIENT_API_ACCESS_TOKEN;

if (ENDPOINT == null) {
throw new Error("PUBLIC_CLIENT_API_ENDPOINT is not defined");
}

export const api = hc<AppType>(ENDPOINT);

type ContactResponse = InferResponseType<
(typeof api)["v1"]["contact"]["$post"]
>;
export function postContactFormData(data: ContactFormData): ResultAsync<
ContactResponse,
{
code: "NETWORK_ERROR" | "API_ERROR";
error: Error;
}
> {
return ResultAsync.fromPromise(
api.v1.contact.$post(
{
json: data,
},
{
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
},
),
(e) => {
if (e instanceof Error) {
return { code: "NETWORK_ERROR", error: e } as const;
}
throw e;
},
).andThen((res) => {
if (!res.ok) {
return err({
code: "API_ERROR",
error: new Error(res.statusText),
} as const);
}

return ResultAsync.fromPromise(
res.json(),
() =>
({
code: "API_ERROR",
error: new Error("Failed to parse response"),
}) as const,
).map((json) => json);
});
}
9 changes: 9 additions & 0 deletions client/src/lib/types/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Result, ResultAsync } from "neverthrow";

// ref: neverthrow/dist/index.d.ts L53-56
export type InferOkTypes<R> = R extends Result<infer T, unknown> ? T : never;
export type InferErrTypes<R> = R extends Result<unknown, infer E> ? E : never;
export type InferAsyncOkTypes<R> =
R extends ResultAsync<infer T, unknown> ? T : never;
export type InferAsyncErrTypes<R> =
R extends ResultAsync<unknown, infer E> ? E : never;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-neverthrow": "^1.1.4",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-unused-imports": "^3.1.0",
Expand Down

0 comments on commit 2219ebb

Please sign in to comment.