Viết lại cách số 1 ở trên (giờ viết 1 back-end trong luôn dự án)
Database
db\index.ts
Copy import * as todos from './tables/todos' ;
export default { todos }
db\tables\connection.ts
Copy import mysql from "mysql2/promise" ;
export default mysql .createPool ({
host : 'localhost' ,
user : 'root' ,
password : '' ,
database : 'todo_app'
});
db\tables\queryUtils.ts
Copy import pool from './connection' ;
export async function SelectQuery < T >(queryString : string , params ?: any ) {
const [ results ] = await pool .execute (queryString , params);
return results as T [];
}
export async function ModifyQuery < T >(queryString : string , params ?: any ) {
const [ results ] = await pool .query (queryString , params);
return results as T [];
}
db\tables\todos.ts
Copy import {SelectQuery , ModifyQuery} from './queryUtils' ;
export interface ITodosRow {
id : number ; description : string ; isCompleted : 0 | 1 ;
}
export function getAll () {
return SelectQuery < ITodosRow >( 'SELECT * FROM todos;' );
}
export function getOne (id : number ) {
return SelectQuery < ITodosRow >( 'SELECT * FROM todos WHERE id = ?;' , [id]);
}
export function insert (todoItem : string ) {
return ModifyQuery ( 'INSERT INTO todos (description) VALUE (?);' , [todoItem])
}
export function update (id : number , description : string , isCompleted : number ) {
return ModifyQuery ( 'UPDATE todos SET description=?,isCompleted=? WHERE id=?;' , [description , isCompleted , id]);
}
export function drop (id : number ) {
return ModifyQuery ( 'DELETE FROM todos WHERE id=?;' , [id]);
}
API
app\api\todos\route.ts
Copy import { NextResponse } from 'next/server' ;
import db from '@/db' ;
export async function GET () {
const todos = await db . todos .getAll ();
return NextResponse .json (todos);
}
export async function POST (request : Request ) {
const { description } : Partial < Todo > = await request .json ();
if ( ! description) return NextResponse .json ({ "message" : "Missing required data" })
const result : any = await db . todos .insert (description);
return NextResponse .json ({ message : 'Todo created' , id : result .insertId });
}
export async function PUT (request : Request ) {
const { id , description , isCompleted } : Todo = await request .json ()
if ( ! description) return NextResponse .json ({ "message" : "Missing required data" })
try {
const result = await db . todos .update (id , description , isCompleted);
return NextResponse .json ({ message : 'Update created' , id : result })
} catch (error) {
console .log (error)
return NextResponse .json ({ message : 'Internal Server Error' , error });
}
}
export async function DELETE (request : Request ) {
const { id } : Partial < Todo > = await request .json ();
if ( ! id) return NextResponse .json ({ "message" : "Todo id required" });
try {
const [ todo ] = await db . todos .drop (id);
return NextResponse .json (todo);
} catch (error) {
console .log (error);
return NextResponse .json ({
message : 'Internal Server Error' ,
error
});
}
}
app\api\todos[id]\route.ts
Copy import { NextRequest , NextResponse } from "next/server" ;
import db from '@/db' ;
export async function GET (request : NextRequest , { params } : { params : Promise <{ id : number }> }) {
const id = ( await params).id;
if ( ! id) return NextResponse .json ({ "message" : "Missing required data" })
const todo = await db . todos .getOne (id as number );
if ( ! todo . length ) return NextResponse .json ({ "message" : "Missing required data" });
return NextResponse .json (todo[ 0 ]);
}
App
app\layout.tsx
Copy import "./globals.css" ;
import Link from 'next/link'
export default function RootLayout ({ children } : Readonly <{ children : React . ReactNode ; }>) {
return (
< html lang = "en" >
< body className = { `antialiased` } >
< div className = "py-4 flex gap-2 bg-gray-400 text-white" >
< Link href = "/" > Home </ Link >
< Link href = "/todos" > Todos </ Link >
</ div >
< main className = "container mx-auto px-4" >
{children}
</ main >
</ body >
</ html >
);
}
app\page.tsx
Copy 'use client'
import Link from 'next/link' ;
import { useRouter } from "next/navigation" ;
import React , { useState , useEffect } from 'react' ;
import { URLP } from "@/const/config" ;
type Repo = {
id : number ,
description : string ,
isCompleted : number
}
export default function Todos () {
const [ repo , setRepo ] = useState ([]);
const [ del , setDelete ] = useState ( 1 );
const router = useRouter ();
const handleDelete = async (id : number ) => {
await fetch ( URLP + `/api/todos` ,
{
method : 'DELETE' ,
body : JSON .stringify ({
id
})
}
);
setDelete (del => del + 1 );
};
useEffect (() => {
const repo = fetch ( URLP + `/api/todos` )
.then (response => response .json ())
.then (data => setRepo (data))
} , [del]);
return (
< section className = "row justify-content-center" >
< ul className = "list-decimal" >
{
repo?.map((todo: Repo) => (
< li key = { `todo-item- ${ todo .id } ` } >
< span className = 'block' > {todo.description} </ span >
< Link href = { `/todos/ ${ todo .id } ` } className = "px-4 font-semibold text-sm bg-cyan-500 text-white rounded-full shadow-sm" > Detail </ Link >
< Link href = { `/todos/ ${ todo .id } /edit` } className = "px-4 font-semibold text-sm bg-cyan-700 text-white rounded-full shadow-sm" > Edit </ Link >
< button onClick = {() => handleDelete (todo.id)} className = "px-4 font-semibold text-sm bg-red-600 text-white rounded-full shadow-sm" > Delete </ button >
</ li >
))}
</ ul >
</ section >
)
}
app\types.d.ts
Copy type Post = {
"userId" : number ,
"id" : number ,
"title" : string ,
"body" : string ,
}
type User = {
"id" : number ,
"name" : string ,
"username" : string ,
"email" : string ,
"address" : {
"street" : string ,
"suite" : string ,
"city" : string ,
"zipcode" : string ,
"geo" : {
"lat" : string ,
"lng" : string
}
} ,
"phone" : string ,
"website" : string ,
"company" : {
"name" : string ,
"catchPhrase" : string ,
"bs" : string
}
}
type Result = {
pageid : string ,
title : string ,
extract : string ,
thumbnail ?: {
source : string ,
width : number ,
height : number ,
}
}
type SearchResult = {
query ?: {
pages ?: Result [] ,
} ,
}
type BlogPost = {
id : string ,
title : string ,
date : string ,
}
type Todo = {
id : number
description : string ,
isCompleted : number
}
components\layout.tsx
Copy import "@/app/globals.css" ;
/* eslint-disable @typescript-eslint/no-explicit-any */
export default function Layout ({ children } : any ) {
return (
< div className = "container p-3 mx-auto" >
{children}
</ div >
)
}
pages_document.tsx
Copy import { Html , Head , Main , NextScript } from 'next/document' ;
export default function Document () {
return (
< Html >
< Head />
< body >
< Main />
< NextScript />
</ body >
</ Html >
);
}
pages_app.tsx
Copy import "@/app/globals.css" ;
import Layout from '@/components/layout' ;
import type { AppProps } from "next/app" ;
export default function App ({ Component , pageProps } : AppProps ) {
return < Layout >< Component { ... pageProps } /></ Layout >;
}
pages\todos\index.tsx
Copy import type { InferGetStaticPropsType , GetStaticProps } from 'next' ;
import Link from 'next/link' ;
import { URLP } from "@/const/config" ;
import { useRouter } from 'next/router' ;
type Repo = {
id : number ,
description : string ,
isCompleted : number
}
export const getStaticProps = ( async () => {
const res = await fetch ( URLP + `/api/todos` );
const repo = await res .json ();
return {
props : {
repo
}
}
}) satisfies GetStaticProps <{ repo : Repo }>
export default function Todos ({ repo } : InferGetStaticPropsType < typeof getStaticProps>) {
const router = useRouter ();
const handleDelete = async (id : number ) => {
const result = await fetch ( URLP + `/api/todos` ,
{
method : 'DELETE' ,
body : JSON .stringify ({
id
})
}
);
const res = await result .json ();
router .replace ( router .asPath);
};
return (
< section className = "row justify-content-center" >
< ul className = "list-decimal" >
{
repo.map((todo: Repo) => (
< li key = { `todo-item- ${ todo .id } ` } >
< span className = 'block' > {todo.description} </ span >
< Link href = { `/todos/ ${ todo .id } ` } className = "px-4 font-semibold text-sm bg-cyan-500 text-white rounded-full shadow-sm" > Detail </ Link >
< Link href = { `/todos/ ${ todo .id } /edit` } className = "px-4 font-semibold text-sm bg-cyan-700 text-white rounded-full shadow-sm" > Edit </ Link >
< button onClick = {() => handleDelete (todo.id)} className = "px-4 font-semibold text-sm bg-red-600 text-white rounded-full shadow-sm" > Delete </ button >
</ li >
))}
</ ul >
</ section >
)
}
pages\todos\create.tsx
Copy import { useState } from "react" ;
import { URLP } from "@/const/config" ;
export default function Create () {
const [ value , setValue ] = useState < string >( '' );
const handleSubmit = async (e : React . MouseEvent < HTMLButtonElement >) => {
e .preventDefault ();
const isCompleted = 0 ;
const description = value;
await fetch ( URLP + `/api/todos` ,
{
method : 'POST' ,
body : JSON .stringify ({
description ,
isCompleted
})
}
);
};
return (
< section className = "row justify-content-center" >
< form className = "p-4 shadow border" >
< label className = "text-gray-700" > Description </ label >
< input value = {value} onChange = {e => setValue ( e . target .value)} type = "text" className = "mt-1 block w-full rounded-md bg-gray-100 border-transparent focus:border-gray-500 focus:bg-white focus:ring-0" placeholder = "Add Todo" />
< button onClick = {handleSubmit} className = "text-gray-100 mt-2 text-xs leading-5 font-semibold bg-lime-950 rounded-full py-1 px-3 flex items-center space-x-2 hover:bg-lime-700 dark:highlight-white/5" > Add Todo </ button >
</ form >
</ section >
)
}
pages\todos[id]\index.tsx
Copy import type { InferGetStaticPropsType , GetStaticProps } from 'next' ;
import { URLP } from "@/const/config" ;
type Repo = {
id : number ,
description : string ,
isCompleted : number
}
export default function Detail ({ repo } : InferGetStaticPropsType < typeof getStaticProps>) {
return (
< div className = 'm-3 p-4 w-60 h-auto rounded-lg bg-white shadow-md' >
< h2 >Id : { repo .id}</ h2 >
< h3 > Description : </ h3 >
< p >{repo.description} </ p >
< h4 > isCompleted : </ h4 >
< p >{repo.isCompleted} </ p >
</ div >
)
}
export const getStaticProps = ( async ({ params }) => {
const { id } : any = await params;
const res = await fetch ( URLP + `/api/todos/ ${ id } ` );
const repo = await res .json ()
return { props : { repo } }
}) satisfies GetStaticProps <{ repo : Repo }>
export async function getStaticPaths () {
const response = await fetch ( URLP + `/api/todos` );
const postList : Repo [] = await response .json ();
return {
paths : postList .map ((post) => {
return {
params : {
id : ` ${ post .id } ` ,
} ,
}
}) ,
fallback : false
}
}
pages\todos\[id]\edit.tsx
Copy import { useParams } from "next/navigation" ;
import { useEffect , useState } from "react" ;
import { URLP } from "@/const/config" ;
export default function Edit () {
const params : any = useParams ();
var id = params ?.id as number ?? 1 ;
const [ obj , setData ] = useState ({ description : "" , completed : 0 });
useEffect (() => {
const fetchData = async () => {
const result = await fetch ( URLP + `/api/todos?id= ${ id } ` );
const repo = await result .json ();
const description = repo .description as string ;
const completed = repo .isCompleted as number ;
setData ({ ... obj , description , completed });
};
fetchData ();
} , []);
const handleNameChange = (description : string ) => {
console .log ({ ... obj , description });
setData ({ ... obj , description });
};
const handleNumberChange = (completed : number ) => {
console .log ({ ... obj , completed });
setData ({ ... obj , completed });
};
const handleSubmit = async (e : React . MouseEvent < HTMLButtonElement >) => {
e .preventDefault ();
await fetch ( URLP + `/api/todos` ,
{
method : 'PUT' ,
body : JSON .stringify ({
... obj ,
id
})
}
);
};
return (
< section className = "row justify-content-center" >
< form className = "p-4 shadow border" >
< label className = "text-gray-700" > Description </ label >
< input value = {obj.description} onChange = {e => handleNameChange ( e . target .value)} type = "text" className = "mt-1 block w-full rounded-md bg-gray-100 border-transparent focus:border-gray-500 focus:bg-white focus:ring-0" placeholder = "Edit Todo" />
< label className = "text-gray-700" > isCompleted </ label >
< input value = {obj.completed} onChange = {e => handleNumberChange ( parseInt ( e . target .value))} type = "text" className = "mt-1 block w-full rounded-md bg-gray-100 border-transparent focus:border-gray-500 focus:bg-white focus:ring-0" />
< button onClick = {handleSubmit} className = "text-gray-100 mt-2 text-xs leading-5 font-semibold bg-lime-950 rounded-full py-1 px-3 flex items-center space-x-2 hover:bg-lime-700 dark:highlight-white/5" > Edit Todo </ button >
</ form >
</ section >
)
}
const\config.ts
Copy export const URLP = process . env . URL || "http://localhost:3000" ;
.env
Copy PORT = 3000
URLP = http : //localhost:3000