Skip to content

Commit dc947ec

Browse files
committed
feat: add Next.js App Router example
1 parent 829a284 commit dc947ec

22 files changed

+803
-2
lines changed

Diff for: deploy/docker-compose/todo-nextjs-app.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
services:
2+
todo-backend:
3+
build: ../../frontend/examples/express
4+
ports:
5+
- "8002:8002"
6+
environment:
7+
- HANKO_API_URL=http://hanko:8000
8+
networks:
9+
- intranet
10+
todo-frontend:
11+
build: ../../frontend/examples/nextjs-app
12+
ports:
13+
- "8889:8889"
14+
networks:
15+
- intranet

Diff for: frontend/examples/README.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,54 @@ It contains:
1010
- an example [express](express) backend - this is a simple version of the well-known todo app
1111
- example frontend applications using the following frameworks:
1212
- [Angular](angular)
13-
- [Next.js](nextjs)
13+
- [Next.js](nextjs) (Pages Router)
14+
- [Next.js App Router](nextjs-app)
1415
- [React](react)
1516
- [Vue](vue)
1617
- [Svelte](svelte)
1718

1819
## How to run
20+
1921
### Manual
22+
2023
1. Start the Hanko API (see the instructions on how to run the API [in Docker](../../backend/README.md#Docker) or [from Source](../../backend/README.md#from-source))
2124
2. Start the express backend (see the [README](express) for the express backend)
2225
3. Start one of the frontend applications (see the README for the app of your choice)
2326

2427
### Docker Compose
2528

2629
#### React
30+
2731
```
2832
docker compose -f deploy/docker-compose/base.yaml -f deploy/docker-compose/todo-react.yaml -p "hanko-todo-react" up --build
2933
```
34+
3035
#### Angular
36+
3137
```
3238
docker compose -f deploy/docker-compose/base.yaml -f deploy/docker-compose/todo-angular.yaml -p "hanko-todo-angular" up --build
3339
```
34-
#### Next.js
40+
41+
#### Next.js (Pages Router)
42+
3543
```
3644
docker compose -f deploy/docker-compose/base.yaml -f deploy/docker-compose/todo-nextjs.yaml -p "hanko-todo-nextjs" up --build
3745
```
46+
47+
#### Next.js (App Router)
48+
49+
```
50+
docker compose -f deploy/docker-compose/base.yaml -f deploy/docker-compose/todo-nextjs-app.yaml -p "hanko-todo-nextjs-app" up --build
51+
```
52+
3853
#### Vue
54+
3955
```
4056
docker compose -f deploy/docker-compose/base.yaml -f deploy/docker-compose/todo-vue.yaml -p "hanko-todo-vue" up --build
4157
```
58+
4259
#### Svelte
60+
4361
```
4462
docker compose -f deploy/docker-compose/base.yaml -f deploy/docker-compose/todo-svelte.yaml -p "hanko-todo-svelte" up --build
4563
```

Diff for: frontend/examples/nextjs-app/.dockerignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.next

Diff for: frontend/examples/nextjs-app/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

Diff for: frontend/examples/nextjs-app/.gitignore

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

Diff for: frontend/examples/nextjs-app/Dockerfile

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM node:18-alpine AS deps
2+
WORKDIR /app
3+
COPY package.json ./
4+
RUN npm install
5+
6+
FROM node:18-alpine AS builder
7+
WORKDIR /app
8+
COPY --from=deps /app/node_modules ./node_modules
9+
COPY . .
10+
RUN npm run build
11+
12+
FROM node:18-alpine AS runner
13+
WORKDIR /app
14+
ENV NODE_ENV production
15+
COPY --from=builder /app/.next ./.next
16+
COPY --from=builder /app/node_modules ./node_modules
17+
COPY --from=builder /app/public ./public
18+
COPY --from=builder /app/package.json ./package.json
19+
CMD ["npm", "start"]

Diff for: frontend/examples/nextjs-app/README.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Hanko Next.js App Router Example
2+
3+
This is an example application that shows how to use Hanko with Next.js App Router.
4+
5+
## Overview
6+
7+
The application is a simple todo app that demonstrates:
8+
9+
- Integration of Hanko web components
10+
- JWT validation in the backend
11+
- App Router architecture with client components
12+
- Authentication flow
13+
14+
## Getting Started
15+
16+
### Prerequisites
17+
18+
- Node.js 18 or later
19+
- A running Hanko API instance
20+
- The example Express backend
21+
22+
### Environment Variables
23+
24+
Create a `.env` file in the root directory with:
25+
26+
```
27+
NEXT_PUBLIC_HANKO_API=http://localhost:8000
28+
NEXT_PUBLIC_TODO_API=http://localhost:8001
29+
```
30+
31+
### Installation
32+
33+
```bash
34+
npm install
35+
```
36+
37+
### Running the App
38+
39+
```bash
40+
npm start
41+
```
42+
43+
The application will start on http://localhost:8889.

Diff for: frontend/examples/nextjs-app/app/layout.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Metadata } from "next";
2+
import "../styles/globals.css";
3+
4+
export const metadata: Metadata = {
5+
title: "Hanko Next.js App Router Example",
6+
description: "Hanko authentication example with Next.js App Router",
7+
};
8+
9+
export default function RootLayout({
10+
children,
11+
}: {
12+
children: React.ReactNode;
13+
}) {
14+
return (
15+
<html lang="en">
16+
<head>
17+
<link rel="icon" href="/favicon.png" />
18+
</head>
19+
<body>{children}</body>
20+
</html>
21+
);
22+
}

Diff for: frontend/examples/nextjs-app/app/page.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import HankoAuth from "../components/HankoAuth";
5+
import styles from "../styles/Todo.module.css";
6+
7+
export default function Home() {
8+
const [error, setError] = useState<Error | null>(null);
9+
10+
return (
11+
<div className={styles.content}>
12+
<div className={styles.error}>{error?.message}</div>
13+
<HankoAuth setError={setError} />
14+
</div>
15+
);
16+
}

Diff for: frontend/examples/nextjs-app/app/profile/page.tsx

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
import { useCallback, useEffect, useRef, useState } from "react";
5+
import styles from "../../styles/Todo.module.css";
6+
7+
import { Hanko } from "@teamhanko/hanko-elements";
8+
import HankoProfile from "../../components/HankoProfile";
9+
import { SessionExpiredModal } from "../../components/SessionExpiredModal";
10+
11+
export default function Profile() {
12+
const router = useRouter();
13+
const [hankoClient, setHankoClient] = useState<Hanko>();
14+
const modalRef = useRef<HTMLDialogElement>(null);
15+
const [error, setError] = useState<Error | null>(null);
16+
const api = process.env.NEXT_PUBLIC_HANKO_API!;
17+
18+
const logout = () => {
19+
hankoClient?.user.logout().catch((e) => {
20+
setError(e);
21+
});
22+
};
23+
24+
const redirectToTodos = () => {
25+
router.push("/todo");
26+
};
27+
28+
const redirectToLogin = useCallback(() => {
29+
router.push("/");
30+
}, [router]);
31+
32+
useEffect(() => {
33+
if (!hankoClient) {
34+
return;
35+
}
36+
37+
if (!hankoClient.session.isValid()) {
38+
redirectToLogin();
39+
}
40+
}, [hankoClient, redirectToLogin]);
41+
42+
useEffect(() => {
43+
setHankoClient(new Hanko(api));
44+
}, [api]);
45+
46+
useEffect(() => {
47+
if (hankoClient) {
48+
hankoClient.onUserLoggedOut(() => {
49+
redirectToLogin();
50+
});
51+
}
52+
}, [hankoClient, redirectToLogin]);
53+
54+
useEffect(() => {
55+
if (hankoClient) {
56+
hankoClient.onSessionExpired(() => {
57+
modalRef.current?.showModal();
58+
});
59+
}
60+
}, [hankoClient]);
61+
62+
return (
63+
<>
64+
<SessionExpiredModal ref={modalRef} />
65+
<nav className={styles.nav}>
66+
<button onClick={logout} className={styles.button}>
67+
Logout
68+
</button>
69+
<button disabled className={styles.button}>
70+
Profile
71+
</button>
72+
<button onClick={redirectToTodos} className={styles.button}>
73+
Todos
74+
</button>
75+
</nav>
76+
<div className={styles.content}>
77+
<h1 className={styles.headline}>Profile</h1>
78+
<div className={styles.error}>{error?.message}</div>
79+
<HankoProfile setError={setError} />
80+
</div>
81+
</>
82+
);
83+
}

0 commit comments

Comments
 (0)