From 27992a21b81d993a538503a9aa91d95e831c2922 Mon Sep 17 00:00:00 2001 From: seri Date: Mon, 2 Mar 2026 18:23:26 +0900 Subject: [PATCH 1/7] wip init From e698d781a74542fdd3dfb5ef62819774d184aa17 Mon Sep 17 00:00:00 2001 From: seri Date: Mon, 2 Mar 2026 19:59:41 +0900 Subject: [PATCH 2/7] feat: update authentication components style --- apps/admin/.env.template | 1 + apps/admin/.prettierignore | 3 + .../(auth)/_layout/auth-layout.module.scss | 55 ---- .../src/app/(auth)/_layout/auth-layout.tsx | 10 +- apps/admin/src/app/(auth)/_layout/index.ts | 1 - apps/admin/src/app/(auth)/layout.tsx | 2 +- .../app/(auth)/signin/_components/signin.tsx | 86 ++++++ .../(auth)/signin/_components/signin/index.ts | 1 - .../_components/signin/signin.module.scss | 71 ----- .../signin/_components/signin/signin.tsx | 93 ------- .../app/(auth)/signin/_components/types.ts | 10 - apps/admin/src/app/(auth)/signin/page.tsx | 16 +- apps/admin/src/app/page.tsx | 11 +- apps/admin/src/components/buttons/button.tsx | 28 ++ .../buttons/button/button.module.scss | 40 --- .../src/components/buttons/button/button.tsx | 31 --- .../src/components/buttons/button/index.ts | 1 - apps/admin/src/components/ui/field.tsx | 244 ++++++++++++++++++ apps/admin/src/constants/application.ts | 14 +- apps/admin/src/lib/auth.ts | 5 +- apps/admin/src/types/app.ts | 10 + 21 files changed, 408 insertions(+), 325 deletions(-) create mode 100644 apps/admin/.prettierignore delete mode 100644 apps/admin/src/app/(auth)/_layout/auth-layout.module.scss delete mode 100644 apps/admin/src/app/(auth)/_layout/index.ts create mode 100644 apps/admin/src/app/(auth)/signin/_components/signin.tsx delete mode 100644 apps/admin/src/app/(auth)/signin/_components/signin/index.ts delete mode 100644 apps/admin/src/app/(auth)/signin/_components/signin/signin.module.scss delete mode 100644 apps/admin/src/app/(auth)/signin/_components/signin/signin.tsx delete mode 100644 apps/admin/src/app/(auth)/signin/_components/types.ts create mode 100644 apps/admin/src/components/buttons/button.tsx delete mode 100644 apps/admin/src/components/buttons/button/button.module.scss delete mode 100644 apps/admin/src/components/buttons/button/button.tsx delete mode 100644 apps/admin/src/components/buttons/button/index.ts create mode 100644 apps/admin/src/components/ui/field.tsx diff --git a/apps/admin/.env.template b/apps/admin/.env.template index 04871c4..bfa3c7e 100644 --- a/apps/admin/.env.template +++ b/apps/admin/.env.template @@ -1,6 +1,7 @@ NODE_ENV="development" NEXT_TELEMETRY_DISABLED=1 +INTERNAL_API_URL=http://localhost:3500 NEXT_PUBLIC_HOST=http://localhost:3500 # NEXT_PUBLIC_IMAGE_CDN_URL="https://img.xxxxxx.jp" NEXT_PUBLIC_IMAGE_CDN_URL="http://localhost:3500/static/images" diff --git a/apps/admin/.prettierignore b/apps/admin/.prettierignore new file mode 100644 index 0000000..7480370 --- /dev/null +++ b/apps/admin/.prettierignore @@ -0,0 +1,3 @@ +*.bk +src/components/ui/ +src/hooks/use-mobile.ts diff --git a/apps/admin/src/app/(auth)/_layout/auth-layout.module.scss b/apps/admin/src/app/(auth)/_layout/auth-layout.module.scss deleted file mode 100644 index a3df1c2..0000000 --- a/apps/admin/src/app/(auth)/_layout/auth-layout.module.scss +++ /dev/null @@ -1,55 +0,0 @@ -@use "styles/utils" as utils; - -.content { - height: 100vh; - display: flex; - font-style: normal; - font-weight: 500; - font-size: 13px; - line-height: 19px; - background-color: utils.$lightColor; - - @include utils.pc { - flex-direction: row; - justify-content: space-evenly; - align-items: center; - } - - @include utils.sp { - flex-direction: column; - align-items: center; - gap: 52px; - } -} - -.logo { - font-family: Montserrat; - font-weight: 800; - font-size: 2rem; - line-height: 2.5rem; - letter-spacing: 0.5px; - - @include utils.pc { - margin: 0 100px; - } - - @include utils.sp { - margin-top: 50px; - } - - .picture { - width: 203px; - height: 46px; - object-fit: contain; - } -} - -.main { - @include utils.pc { - margin-right: 100px; - } - - @include utils.sp { - width: 100%; - } -} diff --git a/apps/admin/src/app/(auth)/_layout/auth-layout.tsx b/apps/admin/src/app/(auth)/_layout/auth-layout.tsx index b362857..4484238 100644 --- a/apps/admin/src/app/(auth)/_layout/auth-layout.tsx +++ b/apps/admin/src/app/(auth)/_layout/auth-layout.tsx @@ -2,7 +2,6 @@ import Link from "next/link"; import { APP_NAME } from "@/constants/application"; -import ss from "./auth-layout.module.scss"; import { ProtectedView } from "./protected-view"; type Props = { @@ -12,11 +11,14 @@ type Props = { export const AuthLayout = ({ children }: Props) => { return ( -
- +
+ {APP_NAME} -
{children}
+
{children}
); diff --git a/apps/admin/src/app/(auth)/_layout/index.ts b/apps/admin/src/app/(auth)/_layout/index.ts deleted file mode 100644 index b6da60e..0000000 --- a/apps/admin/src/app/(auth)/_layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./auth-layout"; diff --git a/apps/admin/src/app/(auth)/layout.tsx b/apps/admin/src/app/(auth)/layout.tsx index 0a1e82b..e4ab0ab 100644 --- a/apps/admin/src/app/(auth)/layout.tsx +++ b/apps/admin/src/app/(auth)/layout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { AuthLayout as Layout } from "./_layout"; +import { AuthLayout as Layout } from "./_layout/auth-layout"; export default function AuthLayout({ children }: { children: React.ReactNode }) { return {children}; diff --git a/apps/admin/src/app/(auth)/signin/_components/signin.tsx b/apps/admin/src/app/(auth)/signin/_components/signin.tsx new file mode 100644 index 0000000..a99f23d --- /dev/null +++ b/apps/admin/src/app/(auth)/signin/_components/signin.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { Button } from "@/components/buttons"; +import { TextInput } from "@/components/forms"; +import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Form } from "@/components/ui/form"; +import { signIn } from "@/lib/auth-client"; +import { ErrorMessages } from "./constants"; + +type SignInForm = { + username: string; + password: string; +}; + +export const SignIn = () => { + const [errorType, setErrorType] = useState(""); + const router = useRouter(); + const form = useForm({ + mode: "onBlur", + defaultValues: { + username: "", + password: "", + }, + }); + const { errors, isSubmitting } = form.formState; + + const signInSubmit = async (data: SignInForm) => { + const result = await signIn.email({ + email: data.username, + password: data.password, + }); + + if (result.error) { + setErrorType(result.error.code || "CredentialsSignin"); + } else if (result.data) { + router.refresh(); + } + }; + + const error = errorType && (ErrorMessages[errorType] ?? ErrorMessages.default); + + return ( + + + Login to your account + +
+ + + + {error &&

{error}

} +
+
+ + + + + Forgot password? + + +
+ ); +}; diff --git a/apps/admin/src/app/(auth)/signin/_components/signin/index.ts b/apps/admin/src/app/(auth)/signin/_components/signin/index.ts deleted file mode 100644 index 717070c..0000000 --- a/apps/admin/src/app/(auth)/signin/_components/signin/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./signin"; diff --git a/apps/admin/src/app/(auth)/signin/_components/signin/signin.module.scss b/apps/admin/src/app/(auth)/signin/_components/signin/signin.module.scss deleted file mode 100644 index 65f523b..0000000 --- a/apps/admin/src/app/(auth)/signin/_components/signin/signin.module.scss +++ /dev/null @@ -1,71 +0,0 @@ -@use "styles/utils" as utils; - -.container { - width: 400px; - background-color: #fff; - border: 1px solid #d8dbe0; - padding: 1rem 2rem 2rem; - - @include utils.sp { - width: 90%; - margin: 1rem auto 0; - } -} - -.title { - font-size: 2rem; - margin-bottom: 1rem; - padding-bottom: 1rem; -} - -.suspend { - margin: 1rem 0; - - .message { - font-weight: 500; - font-size: 13px; - line-height: 1.25rem; - } -} - -.form { - margin-top: 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; - - @include utils.sp { - width: 100%; - margin-bottom: 2rem; - padding: 0; - } -} - -.login { - width: 130px; -} - -.box { - padding-top: 1.5rem; - - @include utils.sp { - width: 100%; - display: flex; - justify-content: center; - } -} - -.forget { - font-weight: 500; - font-size: 13px; - line-height: 19px; - color: utils.$accentColor; - - &:hover { - color: utils.$subColor; - } -} - -.error { - color: utils.$invalidColor; -} diff --git a/apps/admin/src/app/(auth)/signin/_components/signin/signin.tsx b/apps/admin/src/app/(auth)/signin/_components/signin/signin.tsx deleted file mode 100644 index 527ae42..0000000 --- a/apps/admin/src/app/(auth)/signin/_components/signin/signin.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { Button } from "@/components/buttons"; -import { TextInput } from "@/components/forms"; -import { signIn } from "@/lib/auth-client"; -import { ErrorMessages } from "../constants"; -import type { LoginUser } from "../types"; -import ss from "./signin.module.scss"; - -type SignInForm = { - username: string; - password: string; -}; - -type Props = { - user?: LoginUser; -}; - -export const SignIn = ({ user }: Props) => { - const [errorType, setErrorType] = useState(""); - const router = useRouter(); - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - mode: "onBlur", - }); - - const signInSubmit = handleSubmit(async (data: SignInForm) => { - const result = await signIn.email({ - email: data.username, - password: data.password, - }); - - if (result.error) { - setErrorType(result.error.code || "CredentialsSignin"); - } else if (result.data) { - router.refresh(); - } - }); - - const error = errorType && (ErrorMessages[errorType] ?? ErrorMessages.default); - - return ( -
-

Sign in

- {user?.status === "suspend" && ( -
-

Your account has been suspended for the following reasons:

-

f

-
- )} -
- - - {error &&

{error}

} -
-
-
-
- -
- - Forgot password? - -
-
- ); -}; diff --git a/apps/admin/src/app/(auth)/signin/_components/types.ts b/apps/admin/src/app/(auth)/signin/_components/types.ts deleted file mode 100644 index f35b6fa..0000000 --- a/apps/admin/src/app/(auth)/signin/_components/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Better Auth用のLoginUser型 -export type LoginUser = { - id: string; - email: string; - name?: string | null; - image?: string | null; - status: string; - privilege: string; - caution?: string | null; -}; diff --git a/apps/admin/src/app/(auth)/signin/page.tsx b/apps/admin/src/app/(auth)/signin/page.tsx index 9fc539d..a1582ed 100644 --- a/apps/admin/src/app/(auth)/signin/page.tsx +++ b/apps/admin/src/app/(auth)/signin/page.tsx @@ -1,9 +1,15 @@ -"use client"; - +import { Suspense } from "react"; +import type { Metadata } from "next"; import { SignIn } from "./_components/signin"; -const SigninPage = () => { - return ; +export const metadata: Metadata = { + title: "Sign In", }; -export default SigninPage; +export default function SigninPage() { + return ( + Loading...
}> + + + ); +} diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx index 9fbc220..7ae2bc3 100644 --- a/apps/admin/src/app/page.tsx +++ b/apps/admin/src/app/page.tsx @@ -2,18 +2,17 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; +import { ACCOUNT_STATUS, DEFAULT_VIEW } from "@/constants/application"; import { useSession } from "@/lib/auth-client"; -import { AuthLayout } from "./(auth)/_layout"; -import SigninPage from "./(auth)/signin/page"; - -const DEFAULT_VIEW = "/dashboard"; +import { AuthLayout } from "./(auth)/_layout/auth-layout"; +import { SignIn } from "./(auth)/signin/_components/signin"; const Page = () => { const router = useRouter(); const { data: session, isPending } = useSession(); const isUser = !!session?.user; - const activated = session?.user?.status === "active"; + const activated = session?.user?.status === ACCOUNT_STATUS.active; useEffect(() => { if (isPending) { @@ -31,7 +30,7 @@ const Page = () => { if (!isUser || !activated) { return ( - + ); } diff --git a/apps/admin/src/components/buttons/button.tsx b/apps/admin/src/components/buttons/button.tsx new file mode 100644 index 0000000..046572d --- /dev/null +++ b/apps/admin/src/components/buttons/button.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { ClipLoader } from "react-spinners"; +import { Button as BaseButton } from "@/components/ui/button"; +import { cn } from "@/lib/utils/component"; + +type Props = React.ComponentPropsWithoutRef<"button"> & { + label: string; + loading?: boolean; + loadingLabel?: string; +}; + +export const Button = (props: Props) => { + const { label, loading, loadingLabel, ...rest } = props; + return ( + + {loading !== undefined && loading && ( + + )} + {loading !== undefined && loading ? loadingLabel || label : label} + + ); +}; diff --git a/apps/admin/src/components/buttons/button/button.module.scss b/apps/admin/src/components/buttons/button/button.module.scss deleted file mode 100644 index 7b25742..0000000 --- a/apps/admin/src/components/buttons/button/button.module.scss +++ /dev/null @@ -1,40 +0,0 @@ -@use "styles/utils" as utils; - -.round { - border-radius: 24px; -} - -.square { - border-radius: 3px; -} - -.button { - width: 100%; - height: 38px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - white-space: nowrap; - font-size: 1rem; - font-weight: 700; - letter-spacing: 0.1em; - border: 2px solid utils.$accentColor; - color: #fff; - padding: 0 1rem; - background: utils.$accentColor; - // box-shadow: 0 2px 2px rgb(0 0 0 / 70%); - - &:hover, - &:focus { - border: 2px solid utils.$accentColor; - color: utils.$accentColor; - background: #fff; - opacity: 1; - } - - &:disabled { - opacity: 0.6; - cursor: initial; - } -} diff --git a/apps/admin/src/components/buttons/button/button.tsx b/apps/admin/src/components/buttons/button/button.tsx deleted file mode 100644 index 2ada4ee..0000000 --- a/apps/admin/src/components/buttons/button/button.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { ClipLoader } from "react-spinners"; -import ss from "./button.module.scss"; - -type Props = React.ComponentPropsWithoutRef<"button"> & { - label: string; - shape?: "round" | "square"; - loading?: boolean; - loadingLabel?: string; -}; - -export const Button = (props: Props) => { - const { shape = "round", label, loading, loadingLabel, ...rest } = props; - return ( -
- -
- ); -}; diff --git a/apps/admin/src/components/buttons/button/index.ts b/apps/admin/src/components/buttons/button/index.ts deleted file mode 100644 index 98d55ac..0000000 --- a/apps/admin/src/components/buttons/button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./button"; diff --git a/apps/admin/src/components/ui/field.tsx b/apps/admin/src/components/ui/field.tsx new file mode 100644 index 0000000..06ecd20 --- /dev/null +++ b/apps/admin/src/components/ui/field.tsx @@ -0,0 +1,244 @@ +"use client" + +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils/component" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva( + "group/field data-[invalid=true]:text-destructive flex w-full gap-3", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start", + ], + responsive: [ + "@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +