Skip to content

Commit 1f86af3

Browse files
committed
feat: add frontend/nextjs
1 parent 5f15625 commit 1f86af3

17 files changed

+467
-0
lines changed

frontend/nextjs/.eslintrc.json

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

frontend/nextjs/.gitignore

Lines changed: 36 additions & 0 deletions
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+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

frontend/nextjs/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
# or
14+
bun dev
15+
```
16+
17+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18+
19+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20+
21+
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22+
23+
## Learn More
24+
25+
To learn more about Next.js, take a look at the following resources:
26+
27+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29+
30+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31+
32+
## Deploy on Vercel
33+
34+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35+
36+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client";
2+
3+
import type React from "react";
4+
import { useState } from "react";
5+
import { Button, Form, Input, Space } from "antd";
6+
7+
interface BookFormProps {
8+
onSave: (bookData: { name: string; url: string }) => void;
9+
onCancel: () => void;
10+
initialData?: { name: string; url: string };
11+
}
12+
13+
const BookForm: React.FC<BookFormProps> = ({
14+
onSave,
15+
onCancel,
16+
initialData,
17+
}) => {
18+
const [name, setName] = useState(initialData?.name || "");
19+
const [url, setUrl] = useState(initialData?.url || "");
20+
21+
const handleSubmit = async (e: React.FormEvent) => {
22+
e.preventDefault();
23+
onSave({ name, url });
24+
};
25+
26+
return (
27+
<Form onFinish={handleSubmit} className="book-form">
28+
<h2>{initialData ? "Edit" : "Add"} Book</h2>
29+
30+
<Form.Item
31+
label="Name"
32+
name="name"
33+
rules={[{ required: true, message: "Please enter a name" }]}
34+
>
35+
<Input />
36+
</Form.Item>
37+
38+
<Form.Item
39+
label="Url"
40+
name="url"
41+
rules={[{ required: true, message: "Please enter a url" }]}
42+
>
43+
<Input />
44+
</Form.Item>
45+
46+
</Form>
47+
);
48+
};
49+
50+
export default BookForm;

frontend/nextjs/app/books/page.tsx

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"use client";
2+
import React, { useState, useEffect } from "react";
3+
import { Input, List, Pagination, Modal, Spin } from "antd";
4+
import { PlusOutlined } from "@ant-design/icons";
5+
import BookForm from "./bookform";
6+
7+
interface Book {
8+
id: number;
9+
name: string;
10+
url: string;
11+
}
12+
13+
async function getData() {
14+
const res = await fetch("http://localhost:3000/books");
15+
if (!res.ok) {
16+
throw new Error("Failed to fetch data");
17+
}
18+
return res.json();
19+
}
20+
21+
const BookList = () => {
22+
const [books, setBooks] = useState<Book[]>([]);
23+
const [loading, setLoading] = useState(true);
24+
const [currentPage, setCurrentPage] = useState(1);
25+
const [pageSize, setPageSize] = useState(20);
26+
const [editingBook, setEditingBook] = useState<Book | null>(null);
27+
const [editing, setEditing] = useState(false);
28+
const [showAddBookForm, setShowAddBookForm] = useState(false);
29+
const handleAddBook = () => {
30+
setShowAddBookForm(true);
31+
};
32+
const saveBookData = async (bookData) => {
33+
try {
34+
const res = await fetch("http://localhost:3000/books", {
35+
method: "POST",
36+
headers: {
37+
"Content-Type": "application/json",
38+
},
39+
body: JSON.stringify(bookData),
40+
});
41+
if (!res.ok) {
42+
throw new Error("Failed to add book");
43+
}
44+
const newBook = await res.json();
45+
setBooks((prevBooks) => [...prevBooks, newBook]);
46+
setShowAddBookForm(false); // Close the form/modal on success
47+
} catch (error) {
48+
console.error(error);
49+
}
50+
};
51+
52+
useEffect(() => {
53+
const fetchBooks = async () => {
54+
try {
55+
const data = await getData();
56+
setBooks(data.data);
57+
setLoading(false);
58+
} catch (error) {
59+
console.error(error);
60+
setLoading(false);
61+
}
62+
};
63+
fetchBooks();
64+
}, []);
65+
66+
const currentBooks = books.slice(
67+
(currentPage - 1) * pageSize,
68+
currentPage * pageSize,
69+
);
70+
const handlePageChange = (page, pageSize) => {
71+
setCurrentPage(page);
72+
setPageSize(pageSize);
73+
};
74+
const handleEdit = (book: Book) => {
75+
setEditingBook(book);
76+
setEditing(true);
77+
};
78+
79+
const handleSave = async () => {
80+
if (!editingBook) return;
81+
try {
82+
const res = await fetch(`http://localhost:3000/books/${editingBook.id}`, {
83+
method: "PUT",
84+
headers: {
85+
"Content-Type": "application/json",
86+
},
87+
body: JSON.stringify(editingBook),
88+
});
89+
if (!res.ok) {
90+
throw new Error("Failed to update book");
91+
}
92+
setBooks((prevBooks) =>
93+
prevBooks.map((b) => (b.id === editingBook.id ? editingBook : b)),
94+
);
95+
setEditingBook(null);
96+
setEditing(false);
97+
} catch (error) {
98+
console.error(error);
99+
}
100+
};
101+
102+
const handleCancel = () => {
103+
setEditingBook(null);
104+
setEditing(false);
105+
};
106+
107+
return (
108+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
109+
<h1>Books</h1>
110+
<button onClick={handleAddBook} className="add-book-button">
111+
<PlusOutlined />
112+
</button>
113+
<Modal open={showAddBookForm} onCancel={() => setShowAddBookForm(false)} okText="Save">
114+
<BookForm
115+
onSave={saveBookData}
116+
onCancel={() => setShowAddBookForm(false)}
117+
/>
118+
</Modal>
119+
120+
{loading ? (
121+
<Spin size="large" fullscreen />
122+
) : (
123+
<List
124+
bordered
125+
pagination={{
126+
current: currentPage,
127+
pageSize,
128+
total: books.length,
129+
responsive: true,
130+
showSizeChanger: true,
131+
size: "small",
132+
onChange: handlePageChange,
133+
}}
134+
dataSource={books}
135+
renderItem={(book: Book) => (
136+
<List.Item key={book.id} onClick={() => handleEdit(book)}>
137+
{editing && editingBook?.id === book.id ? (
138+
<Modal
139+
title="Edit Book"
140+
open={editing && editingBook?.id === book.id}
141+
onOk={handleSave}
142+
onCancel={(e) => {
143+
e.stopPropagation(); // Prevent event bubbling
144+
handleCancel();
145+
}}
146+
>
147+
<Input
148+
value={editingBook.name}
149+
onChange={(e) =>
150+
setEditingBook({ ...editingBook, name: e.target.value })
151+
}
152+
/>
153+
</Modal>
154+
) : (
155+
book.name
156+
)}
157+
</List.Item>
158+
)}
159+
/>
160+
)}
161+
</main>
162+
);
163+
};
164+
165+
export default BookList;

frontend/nextjs/app/favicon.ico

25.3 KB
Binary file not shown.

frontend/nextjs/app/globals.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
:root {
2+
--foreground-rgb: 0, 0, 0;
3+
--background-start-rgb: 214, 219, 220;
4+
--background-end-rgb: 255, 255, 255;
5+
}
6+
7+
@media (prefers-color-scheme: dark) {
8+
:root {
9+
--foreground-rgb: 255, 255, 255;
10+
--background-start-rgb: 0, 0, 0;
11+
--background-end-rgb: 0, 0, 0;
12+
}
13+
}
14+
15+
body {
16+
color: rgb(var(--foreground-rgb));
17+
background: linear-gradient(
18+
to bottom,
19+
transparent,
20+
rgb(var(--background-end-rgb))
21+
)
22+
rgb(var(--background-start-rgb));
23+
}
24+
25+
@layer utilities {
26+
.text-balance {
27+
text-wrap: balance;
28+
}
29+
}

frontend/nextjs/app/layout.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Metadata } from "next";
2+
import { Inter } from "next/font/google";
3+
// import "./globals.css";
4+
import { AntdRegistry } from "@ant-design/nextjs-registry";
5+
6+
const inter = Inter({ subsets: ["latin"] });
7+
8+
export const metadata: Metadata = {
9+
title: "lifebook",
10+
description: "Record things in my life",
11+
};
12+
13+
export default function RootLayout({
14+
children,
15+
}: Readonly<{
16+
children: React.ReactNode;
17+
}>) {
18+
return (
19+
<html lang="zh">
20+
<body className={inter.className}>
21+
<AntdRegistry>{children}</AntdRegistry>
22+
</body>
23+
</html>
24+
);
25+
}

frontend/nextjs/app/page.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { List } from "antd";
2+
import Link from "next/link";
3+
import BookList from "./books/page";
4+
5+
interface Book {
6+
id: number;
7+
name: string;
8+
url: string;
9+
}
10+
11+
async function getData() {
12+
const res = await fetch("http://localhost:3000/books");
13+
if (!res.ok) {
14+
throw new Error("Failed to fetch data");
15+
}
16+
return res.json();
17+
}
18+
19+
export default async function Home() {
20+
const books = await getData();
21+
22+
return (
23+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
24+
<h1>Home</h1>
25+
26+
<List>
27+
<Link href="/books">Books</Link>
28+
<BookList books={books} />
29+
</List>
30+
</main>
31+
);
32+
}

frontend/nextjs/bun.lockb

176 KB
Binary file not shown.

0 commit comments

Comments
 (0)