Skip to content
Merged
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
43 changes: 27 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
# admin application template
# next-admin

Admin application template using Next.js 16 (App Router) with yarn workspaces monorepo structure.
Next.jsを使用した管理画面アプリケーションのテンプレートプロジェクトで、
Yarn Workspacesによるモノレポ構成になっています。

## base libraries
- Next.js v16
- Better Auth
- TailwindCSS
- prisma
- yarn workspace
- quill
- resend
## Base Libraries

- Next.js 16
- Better Auth 1.4
- TailwindCSS 7
- prisma 4
- yarn 4

### Infrastructures

### infrastructures
- docker compose
- PostgreSQL
- mailhog
- localstack
- S3
- SES
- PostgreSQL 18
- oven(SES)


## リポジトリ構成

```
next-admin/
├── apps/
│ ├── admin/ # 管理画面アプリケーション (Next.js)
│ └── web/ # サービスアプリケーション用フレイスホルダー
├── packages/
│ └── db/ # マイグレーション管理 (Prisma)
├── infra/ # インフラ設定
└── docker-compose.yml # ローカル開発用インフラ管理
```
2 changes: 2 additions & 0 deletions apps/admin/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ AWS_S3_IMAGE_BUCKET='local-image'
AWS_S3_RESION='us-east-1'
# for local only
AWS_S3_ENDPOINT=http://localhost:4566
AWS_SES_ENDPOINT=http://localhost:58005
MAIL_FROM="Next Admin <no-reply@example.com>"

# add packages/db environments
72 changes: 59 additions & 13 deletions apps/admin/README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,82 @@
# Admin

# for Developers
Next.jsを使用した管理画面アプリケーション

## 0. start containers
## セットアップ手順

```
$ docker compose up -d
```
### 1. インフラの起動

## 1. install
プロジェクトルートでDockerコンテナを起動する。

```bash
docker compose up -d
```
$ cat .node-version | nodenv install
$ yarn

### 2. Node.js のインストール

`.node-version`に記載のバージョンを使用する。

```bash
cat .node-version | nodenv install
```

## 2. create .env.local
### 3. 依存パッケージのインストール

create .env.local from .env.template and edit it.
プロジェクトルートで実行する。

```bash
yarn
```

### 4. 環境変数の設定

`.env.template` を元に `.env.local` を作成し、必要な値を設定する。

```bash
cp .env.template .env.local
```

## 3. start local application
主な設定項目:
- `BETTER_AUTH_SECRET` - 認証用シークレットキー(要生成)
- `JWT_SECRET` - JWT署名用キー(要生成)
- AWS関連 - ローカル開発ではデフォルト値で動作

`packages/db` の環境変数も `.env.local` に含める(テンプレート内のコメント参照)。

### 5. データベースのセットアップ

```bash
# Prisma クライアント生成
yarn db:generate

# マイグレーション実行
yarn db:deploy
```
$ yarn dev

### 6. 開発サーバーの起動

```bash
# ルートからの場合
yarn admin:dev

# apps/adminからの場合
yarn dev
```

- http://localhost:3500/

## 4. create first account
### 7. 初回アカウントの作成

ブラウザで以下にアクセスし、最初の管理者アカウントを作成する。

- http://localhost:3500/firstuser


## 開発用Tips

### メールの受信確認

ローカル環境で送信したメールは、実際のアドレスには送信されません。
内容は以下のURLで確認することができます。

- http://localhost:58005/
51 changes: 27 additions & 24 deletions apps/admin/src/app/(app)/_layout/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { usePathname } from "next/navigation";
import { Separator } from "@/components/ui/separator";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { NavigationProgressProvider } from "@/hooks/use-navigation-router";
import { Breadcrumb } from "./breadcrumb";
import type { Navigation } from "./navigation";
import { ProtectedView } from "./protected-view";
Expand All @@ -20,30 +21,32 @@ export const AppLayout = ({ children, navigation, userName, userEmail }: Props)

return (
<ProtectedView>
<SidebarProvider>
<AppSidebar pathname={pathname} navigation={navigation} userName={userName} userEmail={userEmail} />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
<Breadcrumb />
</div>
</header>
<main className="flex-1 p-6">{children}</main>
<footer className="p-4 text-right text-sm text-muted-foreground">
<a
href="https://github.com/seriwb/next-admin"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
next-admin
</a>
<span className="ml-4">©seri</span>
</footer>
</SidebarInset>
</SidebarProvider>
<NavigationProgressProvider>
<SidebarProvider>
<AppSidebar pathname={pathname} navigation={navigation} userName={userName} userEmail={userEmail} />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
<Breadcrumb />
</div>
</header>
<main className="flex-1 p-6">{children}</main>
<footer className="p-4 text-right text-sm text-muted-foreground">
<a
href="https://github.com/seriwb/next-admin"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
next-admin
</a>
<span className="ml-4">©seri</span>
</footer>
</SidebarInset>
</SidebarProvider>
</NavigationProgressProvider>
</ProtectedView>
);
};
2 changes: 1 addition & 1 deletion apps/admin/src/app/(app)/_layout/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"use client";

import React, { useMemo } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { BREADCRUMB_ROUTES } from "@/app/(app)/_layout/navigation";
import { Link } from "@/components/link";
import {
BreadcrumbItem,
BreadcrumbLink,
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/app/(app)/_layout/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import Link from "next/link";
import { ChevronsUpDown, Gauge, LogOut, type LucideIcon, Shield, User, Users } from "lucide-react";
import { Link } from "@/components/link";
import {
DropdownMenu,
DropdownMenuContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { useForm } from "react-hook-form";
Expand All @@ -21,6 +20,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useNavigationRouter } from "@/hooks/use-navigation-router";
import { deleteAccountAction, updateAccountAction } from "./actions";
import { type EditAccountInput, editAccountSchema } from "./lib";

Expand All @@ -30,7 +30,7 @@ type Props = {
};

export const EditAccount = ({ account, currentUserId }: Props) => {
const router = useRouter();
const { push } = useNavigationRouter();
const [errorMessage, setErrorMessage] = useState("");
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
Expand All @@ -54,7 +54,7 @@ export const EditAccount = ({ account, currentUserId }: Props) => {
const result = await updateAccountAction(account.id, data);
if (result.success) {
toast.success("アカウントを更新しました");
router.push("/system/accounts");
push("/system/accounts");
} else {
setErrorMessage(result.error || "");
if (result.fieldErrors) {
Expand All @@ -75,7 +75,7 @@ export const EditAccount = ({ account, currentUserId }: Props) => {
const result = await deleteAccountAction(account.id);
if (result.success) {
toast.success("アカウントを削除しました");
router.push("/system/accounts");
push("/system/accounts");
} else {
toast.error(result.error || "削除に失敗しました");
setShowDeleteDialog(false);
Expand Down Expand Up @@ -199,7 +199,7 @@ export const EditAccount = ({ account, currentUserId }: Props) => {
<Button
type="button"
variant="outline"
onClick={() => router.push("/system/accounts")}
onClick={() => push("/system/accounts")}
disabled={isSubmitting}
>
キャンセル
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { PER_PAGE } from "@/constants/application";
import { useNavigationRouter } from "@/hooks/use-navigation-router";
import dayjs from "@/lib/utils/date";
import { AccountSearch } from "./account-search";
import { deleteAccountAction } from "./actions";
Expand Down Expand Up @@ -48,6 +49,7 @@ const getStatusBadgeVariant = (status: string): "default" | "secondary" | "destr

export const AccountList = ({ data, total, page, query, sort, currentUserId }: Props) => {
const router = useRouter();
const { push } = useNavigationRouter();
const [deleteTarget, setDeleteTarget] = useState<AccountSummary | null>(null);
const [isDeleting, setIsDeleting] = useState(false);

Expand All @@ -56,7 +58,7 @@ export const AccountList = ({ data, total, page, query, sort, currentUserId }: P
params.set("page", String(newPage + 1));
if (query) params.set("query", query);
if (sort) params.set("sort", sort);
router.push(`/system/accounts?${params.toString()}`);
push(`/system/accounts?${params.toString()}`);
};

const handleDelete = async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"use client";

import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useNavigationRouter } from "@/hooks/use-navigation-router";

type Props = {
query: string;
sort: string;
};

export const AccountSearch = ({ query, sort }: Props) => {
const router = useRouter();
const { push } = useNavigationRouter();
const [searchValue, setSearchValue] = useState(query);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

Expand All @@ -20,7 +20,7 @@ export const AccountSearch = ({ query, sort }: Props) => {
params.set("page", "1");
if (value) params.set("query", value);
if (newSort) params.set("sort", newSort);
router.push(`/system/accounts?${params.toString()}`);
push(`/system/accounts?${params.toString()}`);
};

const handleSearchChange = (value: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const createAccountAction = async (input: CreateAccountInput): Promise<Se
password: parsed.data.password,
name: parsed.data.name || undefined,
privilege: parsed.data.privilege,
sendInvite: parsed.data.sendInvite,
});
return result;
} catch (error) {
Expand Down
Loading