diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bdca5c..fb25fe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,4 +83,5 @@ jobs: - name: Build env: DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy" + BETTER_AUTH_SECRET: "dummy_token" run: yarn admin:build 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/package.json b/apps/admin/package.json index e8547b7..b1c8d6b 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -27,6 +27,7 @@ "@hookform/resolvers": "5.2.2", "@prisma/client": "7.4.2", "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "2.1.8", @@ -51,7 +52,7 @@ "formidable": "3.5.4", "fs": "0.0.2", "jsonwebtoken": "9.0.3", - "lucide-react": "0.575.0", + "lucide-react": "0.576.0", "next": "16.1.6", "next-themes": "0.4.6", "nodemailer": "8.0.1", @@ -77,7 +78,6 @@ "resend": "6.9.3", "sass": "1.97.3", "sharp": "0.34.5", - "simplebar-react": "3.3.2", "smooth-scroll": "16.1.3", "sonner": "2.0.7", "tailwind-merge": "3.5.0", @@ -102,7 +102,7 @@ "@types/eslint-plugin-tailwindcss": "3.17.0", "@types/formidable": "3.4.7", "@types/jsonwebtoken": "9.0.10", - "@types/node": "25.0.3", + "@types/node": "25.3.3", "@types/nodemailer": "7.0.11", "@types/quill": "2.0.14", "@types/react": "19.2.14", diff --git a/apps/admin/src/app/(app)/_layout/app-layout.module.scss b/apps/admin/src/app/(app)/_layout/app-layout.module.scss deleted file mode 100644 index 3c9b8b1..0000000 --- a/apps/admin/src/app/(app)/_layout/app-layout.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use "styles/utils" as utils; - -.container { - display: flex; - flex-direction: row; -} - -.contents { - flex: 1 0 auto; - background-color: utils.$lightColor; -} - -.main { - margin: 1.5rem; -} diff --git a/apps/admin/src/app/(app)/_layout/app-layout.tsx b/apps/admin/src/app/(app)/_layout/app-layout.tsx deleted file mode 100644 index c91da84..0000000 --- a/apps/admin/src/app/(app)/_layout/app-layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { usePathname } from "next/navigation"; -import { useSession } from "@/lib/auth-client"; -import ss from "./app-layout.module.scss"; -import { Footer } from "./footer"; -import { Header } from "./header"; -import { ProtectedView } from "./protected-view"; -import { Sidebar } from "./sidebar"; - -type Props = { - children: React.ReactNode; -}; - -export const AppLayout = ({ children }: Props) => { - const pathname = usePathname() ?? ""; - const { data: session } = useSession(); - const userPrivilege = session?.user?.privilege; - - return ( - -
- -
-
-
-
{children}
-
-
-
-
- ); -}; diff --git a/apps/admin/src/app/(app)/_layout/footer/footer.module.scss b/apps/admin/src/app/(app)/_layout/footer/footer.module.scss deleted file mode 100644 index 69b3c77..0000000 --- a/apps/admin/src/app/(app)/_layout/footer/footer.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use "styles/utils" as utils; - -.container { - width: 100%; - text-align: end; - position: fixed; - bottom: 0; - right: 1rem; - padding: 1rem; -} - -.copyright { - margin-left: 1rem; - color: #6e6e6e; -} diff --git a/apps/admin/src/app/(app)/_layout/footer/footer.tsx b/apps/admin/src/app/(app)/_layout/footer/footer.tsx deleted file mode 100644 index 0c8b575..0000000 --- a/apps/admin/src/app/(app)/_layout/footer/footer.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import ss from "./footer.module.scss"; - -export const COPY_RIGHT = "©seri"; // TODO: change it to yours -export const LINK_TEXT = "next-admin"; // TODO: change it to yours -export const LINK_HREF = "https://github.com/seriwb/next-admin"; // TODO: change it to yours - -const Footer = () => { - return ( -
- - {LINK_TEXT} - - {COPY_RIGHT} -
- ); -}; - -export default React.memo(Footer); diff --git a/apps/admin/src/app/(app)/_layout/footer/index.ts b/apps/admin/src/app/(app)/_layout/footer/index.ts deleted file mode 100644 index a357e42..0000000 --- a/apps/admin/src/app/(app)/_layout/footer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Footer } from "./footer"; diff --git a/apps/admin/src/app/(app)/_layout/header/header.module.scss b/apps/admin/src/app/(app)/_layout/header/header.module.scss deleted file mode 100644 index 95ca22e..0000000 --- a/apps/admin/src/app/(app)/_layout/header/header.module.scss +++ /dev/null @@ -1,85 +0,0 @@ -@use "styles/utils" as utils; - -$spacing: 0.5rem; -$font-color: #7b7b7b; -$border-color: #d8dbe0; - -.container { - padding: $spacing; - background-color: #fefefe; - position: sticky; - top: 0; - z-index: 1029; - border-bottom: 1px solid $border-color; -} - -.nav { - height: 3.5rem; - display: flex; - justify-content: space-between; - - .navLeft { - } - - .navRight { - margin-right: 4rem; - } -} - -.dropdownIcon { - --var-color: #7b7b7b; - - width: 48px; - height: 48px; - border-radius: 50%; - color: var(--var-color); - border: 2px solid var(--var-color); - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; -} - -.dropdownMenu { - width: 240px; - padding: 0.5rem 1rem; - color: $font-color; - - .title { - margin-top: 0; - color: inherit; - } - - .menu { - color: inherit; - - .item { - width: calc(100% + 1rem); - height: 100%; - margin-left: -$spacing; - padding: calc($spacing / 2) $spacing; - display: inline-flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - - &:hover { - background-color: utils.$selectColor; - } - } - - .divider { - margin: 0.5rem -1rem; - border-top: 1px solid $border-color; - } - } -} - -.divider { - margin: $spacing calc($spacing * -1); - border-top: 1px solid $border-color; -} - -.breadcrumb { - margin-left: 1rem; -} diff --git a/apps/admin/src/app/(app)/_layout/header/header.tsx b/apps/admin/src/app/(app)/_layout/header/header.tsx deleted file mode 100644 index 1ca8310..0000000 --- a/apps/admin/src/app/(app)/_layout/header/header.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import { LogOut, User } from "lucide-react"; -import { Breadcrumb } from "@/components/breadcrumb"; -import { Dropdown, DropdownMenu, DropdownToggle } from "@/components/dropdown"; -import { signOut } from "@/lib/auth-client"; -import ss from "./header.module.scss"; - -export const Header = () => { - const handleSignout = async () => { - const ok = confirm("Are you sure you want to sign out?"); - if (ok) { - await signOut(); - window.location.href = "/"; - } - }; - - return ( -
-
-
-
- - -
- -
-
- -
-

Settings

-
- menu -
-
- - Sign out -
-
-
- - -
-
-
-
- -
-
- ); -}; diff --git a/apps/admin/src/app/(app)/_layout/header/index.ts b/apps/admin/src/app/(app)/_layout/header/index.ts deleted file mode 100644 index 49ac70f..0000000 --- a/apps/admin/src/app/(app)/_layout/header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./header"; diff --git a/apps/admin/src/app/(app)/_layout/index.ts b/apps/admin/src/app/(app)/_layout/index.ts deleted file mode 100644 index 07c56e2..0000000 --- a/apps/admin/src/app/(app)/_layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./app-layout"; diff --git a/apps/admin/src/app/(app)/_layout/index.tsx b/apps/admin/src/app/(app)/_layout/index.tsx new file mode 100644 index 0000000..b3569fb --- /dev/null +++ b/apps/admin/src/app/(app)/_layout/index.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { Breadcrumb } from "@/components/breadcrumb"; +import { Separator } from "@/components/ui/separator"; +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import type { Navigation } from "@/constants/navigation"; +import { ProtectedView } from "./protected-view"; +import { AppSidebar } from "./sidebar"; + +type Props = { + children: React.ReactNode; + navigation: Navigation[]; + userName: string; + userEmail: string; +}; + +export const AppLayout = ({ children, navigation, userName, userEmail }: Props) => { + const pathname = usePathname() ?? ""; + + return ( + + + + +
+
+ + + +
+
+
{children}
+ +
+
+
+ ); +}; diff --git a/apps/admin/src/app/(app)/_layout/protected-view.tsx b/apps/admin/src/app/(app)/_layout/protected-view.tsx index 2a2b9e2..5fc33b1 100644 --- a/apps/admin/src/app/(app)/_layout/protected-view.tsx +++ b/apps/admin/src/app/(app)/_layout/protected-view.tsx @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; +import { ACCOUNT_STATUS } from "@/constants/application"; import { useSession } from "@/lib/auth-client"; type Props = { @@ -11,7 +12,7 @@ type Props = { export const ProtectedView = (props: Props) => { const router = useRouter(); const { data: session, isPending } = useSession(); - const activated = session?.user?.status === "active"; + const activated = session?.user?.status === ACCOUNT_STATUS.active; useEffect(() => { if ((!isPending && !session) || (session && !activated)) { diff --git a/apps/admin/src/app/(app)/_layout/sidebar.tsx b/apps/admin/src/app/(app)/_layout/sidebar.tsx new file mode 100644 index 0000000..cc337ad --- /dev/null +++ b/apps/admin/src/app/(app)/_layout/sidebar.tsx @@ -0,0 +1,187 @@ +"use client"; + +import Link from "next/link"; +import { ChevronsUpDown, Gauge, LogOut, type LucideIcon, Shield, User, Users } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar"; +import { APP_NAME } from "@/constants/application"; +import type { Navigation } from "@/constants/navigation"; +import { signOut } from "@/lib/auth-client"; + +// アイコン名→コンポーネントのマッピング +const iconMap: Record = { + Gauge, + Users, +}; + +const NavIcon = ({ name }: { name: string }) => { + const Icon = iconMap[name]; + return Icon ? : null; +}; + +// ナビゲーション配列をグループに変換するヘルパー +// "title"アイテムでグループ境界を作り、"item"アイテムをグループ内に配置 +type NavGroup = { + label?: string; + items: Navigation[]; +}; + +const buildNavGroups = (navigation: Navigation[]): NavGroup[] => { + const groups: NavGroup[] = []; + let currentGroup: NavGroup = { items: [] }; + + for (const item of navigation) { + if (item.component === "title") { + // 前のグループにアイテムがあれば保存 + if (currentGroup.items.length > 0 || currentGroup.label) { + groups.push(currentGroup); + } + currentGroup = { label: item.name, items: [] }; + } else if (item.component === "item") { + currentGroup.items.push(item); + } + } + + // 最後のグループを追加 + if (currentGroup.items.length > 0 || currentGroup.label) { + groups.push(currentGroup); + } + + return groups; +}; + +type Props = { + pathname: string; + navigation: Navigation[]; + userName: string; + userEmail: string; +}; + +export const AppSidebar = ({ pathname, navigation, userName, userEmail }: Props) => { + const navGroups = buildNavGroups(navigation); + + const handleSignout = async () => { + const ok = confirm("ログアウトしてもよろしいですか?"); + if (ok) { + await signOut({ + fetchOptions: { + onSuccess: () => { + window.location.href = "/signin?code=SignOut"; + }, + }, + }); + } + }; + + return ( + + + + + + +
+ +
+
+ {APP_NAME} +
+ +
+
+
+
+ + + {navGroups.map((group, index) => ( + + {group.label && {group.label}} + + {group.items.map((item) => ( + + + + {item.icon && iconMap[item.icon] && } + {item.name} + + + + ))} + + + ))} + + + + + + + + +
+ +
+
+ {userName} + {userEmail} +
+ +
+
+ + +
+
+ +
+
+ {userName} + {userEmail} +
+
+
+ + + + ログアウト + +
+
+
+
+
+ + +
+ ); +}; diff --git a/apps/admin/src/app/(app)/_layout/sidebar/_nav.tsx b/apps/admin/src/app/(app)/_layout/sidebar/_nav.tsx deleted file mode 100644 index 847a2ab..0000000 --- a/apps/admin/src/app/(app)/_layout/sidebar/_nav.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Gauge, Users } from "lucide-react"; -import type { Privilege } from "@/types/app"; - -export type Navigation = { - component: "title" | "item" | "group"; - name: string; - to?: string; - icon?: React.ReactElement; - items?: Navigation[]; - privilege?: Privilege; -}; - -// ここに追加した場合は、パンくずコンポーネントも編集してください -const _nav: Navigation[] = [ - { - component: "item", - name: "Dashboard", - to: "/dashboard", - icon: , - }, - { - component: "title", - name: "System management", - privilege: "SuperAdmin", - }, - { - component: "item", - name: "Accounts", - to: "/system/accounts", - icon: , - privilege: "SuperAdmin", - }, -]; -export default _nav; diff --git a/apps/admin/src/app/(app)/_layout/sidebar/index.ts b/apps/admin/src/app/(app)/_layout/sidebar/index.ts deleted file mode 100644 index 01acaef..0000000 --- a/apps/admin/src/app/(app)/_layout/sidebar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sidebar"; diff --git a/apps/admin/src/app/(app)/_layout/sidebar/sidebar.module.scss b/apps/admin/src/app/(app)/_layout/sidebar/sidebar.module.scss deleted file mode 100644 index 31de3dd..0000000 --- a/apps/admin/src/app/(app)/_layout/sidebar/sidebar.module.scss +++ /dev/null @@ -1,49 +0,0 @@ -@use "styles/utils" as utils; - -.container { - width: 260px; - height: 100vh; - padding: 1rem 0; - background-color: #1b47ac; - - div { - color: #fff; - } -} - -.brand { - padding: 0.5rem 1rem 1rem; - margin-bottom: 0.5rem; - font-size: 1.5rem; -} - -.nav { - width: 100%; - border-top: 1px solid #fff; -} - -.navList { - margin-top: 1rem; -} - -.navLink { - width: 100%; - padding: 0.5rem 1rem; - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.active { - background-color: rgba($color: utils.$mainColor, $alpha: 40%); -} - -.title { - margin-top: 1rem; -} - -.selected { - &:hover { - background-color: rgba($color: utils.$mainColor, $alpha: 40%); - } -} diff --git a/apps/admin/src/app/(app)/_layout/sidebar/sidebar.tsx b/apps/admin/src/app/(app)/_layout/sidebar/sidebar.tsx deleted file mode 100644 index b9802e0..0000000 --- a/apps/admin/src/app/(app)/_layout/sidebar/sidebar.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from "react"; -import Link from "next/link"; -import clsx from "clsx"; -import SimpleBar from "simplebar-react"; -import { APP_NAME } from "@/constants/application"; -import navigation, { type Navigation } from "./_nav"; -import ss from "./sidebar.module.scss"; - -type Props = { - pathname: string; - userPrivilege: string; -}; - -export const Sidebar = (props: Props) => { - return ( -
- -
{APP_NAME}
- -
- -
- {navigation?.map((item, index) => navItem(item, index, props.pathname, props.userPrivilege))} -
-
-
-
- ); -}; - -const navItem = (item: Navigation, index: number, pathname: string, userPrivilege: string) => { - const { component, name, to, icon, items, privilege } = item; - if (privilege && privilege !== userPrivilege) { - return; - } - - const isActive = component === "item" && !items; - return ( - - {to ? ( - -
- {icon && icon} - {name && name} -
- - ) : ( -
- {icon && icon} - {name && name} -
- )} -
- ); -}; diff --git a/apps/admin/src/app/(app)/layout.tsx b/apps/admin/src/app/(app)/layout.tsx index ede0da9..7cd7696 100644 --- a/apps/admin/src/app/(app)/layout.tsx +++ b/apps/admin/src/app/(app)/layout.tsx @@ -1,6 +1,21 @@ import React from "react"; +import { ACCOUNT_STATUS } from "@/constants/application"; +import { NAVIGATION, filterNavigationByPrivilege } from "@/constants/navigation"; +import { getAppSession } from "@/lib/auth"; import { AppLayout as Layout } from "./_layout"; -export default function AppLayout({ children }: { children: React.ReactNode }) { - return {children}; +export default async function AppLayout({ children }: { children: React.ReactNode }) { + const session = await getAppSession(); + + if (session?.user.status !== ACCOUNT_STATUS.active) { + return null; + } + + const filteredNav = filterNavigationByPrivilege(NAVIGATION, session.user.privilege); + + return ( + + {children} + + ); } diff --git a/apps/admin/src/app/(app)/system/accounts/_components/account-list/condition/condition.tsx b/apps/admin/src/app/(app)/system/accounts/_components/account-list/condition/condition.tsx index b7c4612..c71c21e 100644 --- a/apps/admin/src/app/(app)/system/accounts/_components/account-list/condition/condition.tsx +++ b/apps/admin/src/app/(app)/system/accounts/_components/account-list/condition/condition.tsx @@ -23,7 +23,7 @@ export const Condition = (props: Props) => {
-
); 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..23a010b --- /dev/null +++ b/apps/admin/src/app/(auth)/signin/_components/signin.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +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 "../_lib/constants"; + +type SignInForm = { + username: string; + password: string; +}; + +export const SignIn = () => { + const [toastShown, setToastShown] = useState(false); + const [errorType, setErrorType] = useState(""); + const router = useRouter(); + const searchParams = useSearchParams(); + const errorCode = searchParams?.get("code") ?? ""; + + const form = useForm({ + mode: "onBlur", + defaultValues: { + username: "", + password: "", + }, + }); + const { errors, isSubmitting } = form.formState; + + useEffect(() => { + if (errorCode === "E401" && !toastShown) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setToastShown(true); + toast.error("認証が無効です。再度ログインしてください。", { + duration: 5000, + id: "session-invalid-toast", + }); + } + if (errorCode === "SignOut" && !toastShown) { + setToastShown(true); + toast.success("ログアウトしました", { + duration: 5000, + id: "signout-toast", + }); + } + }, [errorCode, toastShown]); + + 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/_components/constants.ts b/apps/admin/src/app/(auth)/signin/_lib/constants.ts similarity index 100% rename from apps/admin/src/app/(auth)/signin/_components/constants.ts rename to apps/admin/src/app/(auth)/signin/_lib/constants.ts diff --git a/apps/admin/src/app/(auth)/signin/_components/schemas.ts b/apps/admin/src/app/(auth)/signin/_lib/schemas.ts similarity index 100% rename from apps/admin/src/app/(auth)/signin/_components/schemas.ts rename to apps/admin/src/app/(auth)/signin/_lib/schemas.ts 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/api/accounts/create-first-account/route.ts b/apps/admin/src/app/api/accounts/create-first-account/route.ts index 3f8a2b4..be6d35c 100644 --- a/apps/admin/src/app/api/accounts/create-first-account/route.ts +++ b/apps/admin/src/app/api/accounts/create-first-account/route.ts @@ -1,5 +1,6 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import { ACCOUNT_PRIVILEGE } from "@/constants/application"; import { auth } from "@/lib/auth"; export const dynamic = "force-dynamic"; @@ -14,6 +15,7 @@ export async function POST(request: NextRequest) { email: body.email, password: body.password, image: "https://example.com/image.png", + privilege: ACCOUNT_PRIVILEGE.superAdmin, }, }); diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index da796d5..a47cf7c 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -2,7 +2,6 @@ import React from "react"; import type { Metadata } from "next"; import { Inter, Montserrat, Noto_Sans_JP, Noto_Sans_Mono } from "next/font/google"; import { Toaster } from "react-hot-toast"; -import "simplebar-react/dist/simplebar.min.css"; import { TITLE } from "@/constants/application"; import "@/styles/globals.css"; 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/app/signout/page.tsx b/apps/admin/src/app/signout/page.tsx index 304c37a..775e2ed 100644 --- a/apps/admin/src/app/signout/page.tsx +++ b/apps/admin/src/app/signout/page.tsx @@ -1,23 +1,19 @@ "use client"; import { useEffect } from "react"; -import { useRouter } from "next/navigation"; import { signOut } from "@/lib/auth-client"; export default function SignOutPage() { - const router = useRouter(); - useEffect(() => { (async () => { await signOut({ fetchOptions: { onSuccess: () => { - router.push("/signin?code=SignOut"); + window.location.href = "/signin?code=SignOut"; }, }, }); })(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return null; diff --git a/apps/admin/src/components/badge/badge.module.scss b/apps/admin/src/components/badge/badge.module.scss deleted file mode 100644 index fbbf8a1..0000000 --- a/apps/admin/src/components/badge/badge.module.scss +++ /dev/null @@ -1,19 +0,0 @@ -@use "styles/utils" as utils; - -.container { - padding: 0.5rem 1rem; - width: fit-content; -} - -.info { - background-color: utils.$subColor; - color: #fff; -} - -.round { - border-radius: 80px; -} - -.square { - border-radius: 5px; -} diff --git a/apps/admin/src/components/badge/badge.tsx b/apps/admin/src/components/badge/badge.tsx deleted file mode 100644 index f27db84..0000000 --- a/apps/admin/src/components/badge/badge.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { clsx } from "clsx"; -import ss from "./badge.module.scss"; - -export type Color = "info"; - -type Props = { - color?: Color; - text: string; - shape?: "round" | "square"; -}; - -export const Badge = ({ color = "info", text, shape = "round" }: Props) => { - return
{text}
; -}; diff --git a/apps/admin/src/components/badge/index.ts b/apps/admin/src/components/badge/index.ts deleted file mode 100644 index 80844a4..0000000 --- a/apps/admin/src/components/badge/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./badge"; diff --git a/apps/admin/src/components/breadcrumb/_routes.ts b/apps/admin/src/components/breadcrumb/_routes.ts deleted file mode 100644 index c62e34c..0000000 --- a/apps/admin/src/components/breadcrumb/_routes.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type RouteType = { - path: string; // routing path - name: string; // display name - disable?: boolean; // disabled to click -}; - -const ROUTES: RouteType[] = [ - { path: "/", name: "Home" }, - { path: "/dashboard", name: "Dashboard" }, - { path: "/articles", name: "Articles" }, - { path: "/analyses/new", name: "New" }, - { path: "/users", name: "Users" }, - { path: "/system", name: `System`, disable: true }, - { path: "/system/accounts", name: "Accounts" }, - { path: "/system/accounts/new", name: "New" }, - { path: "/system/accounts/[id]/edit", name: "Edit" }, -]; - -export default ROUTES; diff --git a/apps/admin/src/components/breadcrumb/breadcrumb.module.scss b/apps/admin/src/components/breadcrumb/breadcrumb.module.scss deleted file mode 100644 index 069acb2..0000000 --- a/apps/admin/src/components/breadcrumb/breadcrumb.module.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use "styles/utils" as utils; - -.container { - display: inline-flex; - gap: 0.5rem; -} - -.separator { - color: utils.$subColor; -} - -.item { - color: utils.$menuColor; -} diff --git a/apps/admin/src/components/breadcrumb/breadcrumb.tsx b/apps/admin/src/components/breadcrumb/breadcrumb.tsx index 0555f26..dd830e9 100644 --- a/apps/admin/src/components/breadcrumb/breadcrumb.tsx +++ b/apps/admin/src/components/breadcrumb/breadcrumb.tsx @@ -1,9 +1,17 @@ +"use client"; + import React, { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; -import clsx from "clsx"; -import ROUTES from "./_routes"; -import ss from "./breadcrumb.module.scss"; +import { + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + Breadcrumb as BreadcrumbRoot, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { BREADCRUMB_ROUTES } from "@/constants/navigation"; type BreadcrumbType = { name: string; @@ -21,7 +29,7 @@ export const Breadcrumb = () => { location.split("/").reduce((prev, curr, index, array) => { const currentPath = `${prev}/${curr}`; - const route = ROUTES.find((route) => route.path === currentPath); + const route = BREADCRUMB_ROUTES.find((route) => route.path === currentPath); const regexp = /\[(.*?)\]/g; // ex:[id] let realpath = route?.path || ""; @@ -62,24 +70,28 @@ export const Breadcrumb = () => { } return ( -
-
- Home -
- {breadcrumbs.map((breadcrumb, index) => { - return ( + + + + + Home + + + {breadcrumbs.map((breadcrumb, index) => ( - {">"} - {breadcrumb.active ? ( -
{breadcrumb.name}
- ) : ( -
- {breadcrumb.name} -
- )} + + + {breadcrumb.active ? ( + {breadcrumb.name} + ) : ( + + {breadcrumb.name} + + )} +
- ); - })} -
+ ))} + + ); }; 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/divider/divider.module.scss b/apps/admin/src/components/divider/divider.module.scss deleted file mode 100644 index 567a10d..0000000 --- a/apps/admin/src/components/divider/divider.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -.container { - width: 100%; -} - -.divider { - border: 1px solid rgb(255 255 255 / 11%); -} diff --git a/apps/admin/src/components/divider/divider.tsx b/apps/admin/src/components/divider/divider.tsx deleted file mode 100644 index 0a5eeb7..0000000 --- a/apps/admin/src/components/divider/divider.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import ss from "./styles.module.scss"; - -type Props = { - margin?: string; -}; - -export const Divider = (props: Props) => ( -
-
-
-); diff --git a/apps/admin/src/components/divider/index.ts b/apps/admin/src/components/divider/index.ts deleted file mode 100644 index 43f46a1..0000000 --- a/apps/admin/src/components/divider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./divider"; diff --git a/apps/admin/src/components/ui/button.tsx b/apps/admin/src/components/ui/button.tsx index 4b31984..2fd025d 100644 --- a/apps/admin/src/components/ui/button.tsx +++ b/apps/admin/src/components/ui/button.tsx @@ -5,28 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils/component" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", }, }, defaultVariants: { @@ -36,25 +34,24 @@ const buttonVariants = cva( } ) -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" - - return ( - - ) +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean } +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + export { Button, buttonVariants } diff --git a/apps/admin/src/components/ui/collapsible.tsx b/apps/admin/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..9fa4894 --- /dev/null +++ b/apps/admin/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } 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 ( +