diff --git a/next.config.mjs b/next.config.mjs index d5456a1..692255a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,15 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, + reactStrictMode: true, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'cdn.dummyjson.com', + pathname: '/**', + }, + ], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index b41dd18..a3434ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "practice-next-page", "version": "0.1.0", "dependencies": { + "@tanstack/react-query": "^5.40.0", "next": "14.2.3", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-hook-form": "^7.51.5" }, "devDependencies": { "@types/node": "^20", @@ -442,6 +444,30 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", + "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.0.tgz", + "integrity": "sha512-iv/W0Axc4aXhFzkrByToE1JQqayxTPNotCoSCnarR/A1vDIHaoKpg7FTIfP3Ev2mbKn1yrxq0ZKYUdLEJxs6Tg==", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -3719,6 +3745,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.51.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz", + "integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index fac6906..9a4cfa0 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,20 @@ "lint": "next lint" }, "dependencies": { + "@tanstack/react-query": "^5.40.0", + "next": "14.2.3", "react": "^18", "react-dom": "^18", - "next": "14.2.3" + "react-hook-form": "^7.51.5" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.3", "postcss": "^8", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.3" + "typescript": "^5" } } diff --git a/pages/[id].tsx b/pages/[id].tsx new file mode 100644 index 0000000..01c0dbf --- /dev/null +++ b/pages/[id].tsx @@ -0,0 +1,77 @@ +// import { useRouter } from "next/router"; +import Link from "next/link"; +import Image from "next/image"; + +interface Recipe { + id: number; + name: string; + image: string; +} +export default function DynamicRount({ recipe }: { recipe: Recipe }) { + // const router = useRouter(); + // const { id } = router.query; + // console.log(router); + return ( +
+
+

+ This page is about dynamic route +

+ Back +
+
Post: {recipe.id}
+
+
+

{recipe.name}

+ random  image +
+
+
+ ); +} + +export async function getStaticProps({ + params, +}: { + params: { id: string | number }; +}) { + const id = params.id; + const res = await fetch(`https://dummyjson.com/recipes/${id}`); + const recipe = await res.json(); + return { props: { recipe } }; +} + +export async function getStaticPaths() { + const url = `https://dummyjson.com/recipes`; + const res = await fetch(url); + const dummy = await res.json(); + const recipes = dummy.recipes; + // console.log(recipes); + // console.log(Array.isArray(recipes)); // Should print true if recipes is an array + + const paths = recipes.map((recipe: Recipe) => { + return { + params: { id: recipe.id.toString() }, + }; + }); + // const paths = recipes.map((recipe: Recipe) => { + // return { + // params: { id: String(recipe.id) }, + // }; + // }); + + return { + paths, + // fallback: true, + fallback: false, + }; +} diff --git a/pages/_app.tsx b/pages/_app.tsx index a7a790f..dba9c47 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,16 @@ import "@/styles/globals.css"; import type { AppProps } from "next/app"; +import Layout from "./layout"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); export default function App({ Component, pageProps }: AppProps) { - return ; + return ( + + + + + + ); } diff --git a/pages/components/event/ListenClick.tsx b/pages/components/event/ListenClick.tsx index b97a7e6..eb215c3 100644 --- a/pages/components/event/ListenClick.tsx +++ b/pages/components/event/ListenClick.tsx @@ -1,6 +1,6 @@ export default function ListenClick() { function handleClick() { - console.log("increment like count"); + // console.log("increment like count"); } return ( + + +
+ {Array.isArray(data) ? ( + data.map((recipe: any) => ( +
+

{recipe.name}

+ random  image +
+ )) + ) : ( +
+

{data.name}

+ random  image +
+ )} +
+ + ); +} diff --git a/pages/dummy-form.tsx b/pages/dummy-form.tsx new file mode 100644 index 0000000..3222a51 --- /dev/null +++ b/pages/dummy-form.tsx @@ -0,0 +1,95 @@ +import Image from "next/image"; +import { useEffect, useState } from "react"; +interface Recipes { + id: number; + name: string; + image: string; +} + +export default function DummyForm() { + const [query, setQuery] = useState(""); + const [search, setSearch] = useState(""); + const [data, setData] = useState([]); + + const fetchData = async () => { + const queryString = query ? `/search?q=${query}` : ""; + console.log(queryString); + const res = await fetch(`https://dummyjson.com/recipes${queryString}`); + if (!res.ok) { + throw new Error("there somthing wrong"); + } + const data = await res.json(); + if (data.recipes) { + const recipes = data.recipes; + setData(recipes); + } else { + setData(data); + } + }; + + useEffect(() => { + fetchData().catch((e) => { + console.error("Somthing went wrong", e); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [search]); + + const handleSearch = (e: any) => { + if (e.key === "Enter" || e.type === "click") { + setSearch(query); + } + }; + + const handleInputChange = (e: any) => { + setQuery(e.target.value); + // console.log(query); + }; + + return ( +
+
+

Dummy, Search Form

+

+ เป็นการฝึกทำ Search แบบง่ายๆ ใช้แค่ useState useEffect ธรรมดา +

+
+ setQuery(e.target.value)} + onChange={handleInputChange} + onKeyDown={handleSearch} + placeholder="...Search" + /> + +
+
+
+ {data.length === 0 ? ( +

Data not found

+ ) : ( + data.map((recipe: Recipes) => ( +
+

{recipe.name}

+ random  image +
+ )) + )} +
+
+ ); +} diff --git a/pages/dummy-isr.tsx b/pages/dummy-isr.tsx new file mode 100644 index 0000000..d71a1ae --- /dev/null +++ b/pages/dummy-isr.tsx @@ -0,0 +1,54 @@ +import Image from "next/image"; + +function DummyISR({ recipes }: any) { + return ( +
+

+ Dummy, This is ISR(Incremental Static Regeneration) +

+

+ จริงๆก็เป็น getStaticProps เวอร์ชัน upgrade โดยเพิ่ม revalidate + เข้ามาให้ทำการ refresh ทุกๆ 300s ตามการ config นี้ +

+
+ {recipes.map((recipe: any) => ( +
+

{recipe.name}

+ random image +
+ ))} +
+
+ ); +} + +export async function getStaticProps() { + const getRandomInt = () => { + return Math.floor(Math.random() * 10 + 1); + }; + const res = await fetch( + `https://dummyjson.com/recipes?limit=${getRandomInt()}` + // `https://dummyjson.com/recipes` + ); + const response = await res.json(); + const recipes = response.recipes; + + return { + props: { + recipes: recipes, + // recipes: recipes.slice(getRandomInt()), + }, + revalidate: 300, + }; +} + +export default DummyISR; diff --git a/pages/dummy-ssg.tsx b/pages/dummy-ssg.tsx new file mode 100644 index 0000000..1864209 --- /dev/null +++ b/pages/dummy-ssg.tsx @@ -0,0 +1,64 @@ +import Image from "next/image"; + +interface PostType { + id: number; + title: string; +} + +function DummySSG({ recipes }: { recipes: PostType[] }) { + return ( +
+

+ Dummy, This is SSG(Server Site Generation) +

+

ตัวนี้ render ครั้งเดียวตอน build

+ {/* */} +
+ {recipes.map((recipe: any) => ( +
+

{recipe.name}

+ random  image +
+ ))} +
+
+ ); +} + +// export async function getStaticProps() { +// const res = await fetch("https://jsonplaceholder.typicode.com/posts"); +// const posts = await res.json(); + +// return { +// props: { +// posts, +// }, +// }; +// } + +export async function getStaticProps() { + const getRandomInt = () => { + return Math.floor(Math.random() * 10 + 1); + }; + const res = await fetch( + `https://dummyjson.com/recipes?limit=${getRandomInt()}` + ); + const response = await res.json(); + const recipes = response.recipes; + return { props: { recipes }, revalidate: 300 }; +} + +export default DummySSG; diff --git a/pages/dummy-ssr.tsx b/pages/dummy-ssr.tsx new file mode 100644 index 0000000..8f70c77 --- /dev/null +++ b/pages/dummy-ssr.tsx @@ -0,0 +1,51 @@ +import Image from "next/image"; + +interface Recipes { + id: number; + name: string; + image: string; +} + +function DummySSR({ recipes }: { recipes: Recipes[] }) { + return ( +
+

+ Hello, This is SSR(Server Side Render) +

+

+ ตัว ssr นี้จะ render ทุกครั้งที่มีการ request นะ +

+
+ {recipes.map((recipe: Recipes) => ( +
+

{recipe.name}

+ random  image +
+ ))} +
+
+ ); +} + +export async function getServerSideProps() { + const getRandomInt = () => { + return Math.floor(Math.random() * 10 + 1); + }; + const res = await fetch( + `https://dummyjson.com/recipes?limit=${getRandomInt()}` + ); + const response: { recipes: Recipes[] } = await res.json(); + const recipes = response.recipes; + return { props: { recipes } }; +} + +export default DummySSR; diff --git a/pages/dummy-tan.tsx b/pages/dummy-tan.tsx new file mode 100644 index 0000000..94a7d72 --- /dev/null +++ b/pages/dummy-tan.tsx @@ -0,0 +1,35 @@ +import Image from "next/image"; +import TanStack from "./utils/tanStack"; + +export default function DummyTan() { + const data = TanStack(); + // console.log("data from tan", TanStack()); + return ( +
+
+

Dummy, This is TanStack(useQuery)

+

+ ส่วนตัวนี้เป็นการ render จากฝั่ง client เหมือน react เลย +

+
+
+ {data && + data.map((recipe: any) => ( +
+

{recipe.name}

+ random  image +
+ ))} +
+
+ ); +} diff --git a/pages/Event/index.tsx b/pages/event.tsx similarity index 73% rename from pages/Event/index.tsx rename to pages/event.tsx index 6028189..b31b33a 100644 --- a/pages/Event/index.tsx +++ b/pages/event.tsx @@ -1,10 +1,10 @@ -import ListenClick from "../components/event/ListenClick"; +import ListenClick from "./components/event/ListenClick"; import Link from "next/link"; -import StateHooks from "../components/event/StateHooks"; +import StateHooks from "./components/event/StateHooks"; export default function EventPage() { return ( -
+

This page is about event listener page diff --git a/pages/index.tsx b/pages/index.tsx index 6229ed9..26b64c0 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -5,6 +5,9 @@ import Link from "next/link"; const inter = Inter({ subsets: ["latin"] }); export default function Home() { + const getRandomInt = (max: number) => { + return Math.floor(Math.random() * max); + }; return (
@@ -12,12 +15,15 @@ export default function Home() {

Practice this all

- + Send Props Page - + Listerner Page + + how to use dynamic router +

diff --git a/pages/layout.tsx b/pages/layout.tsx new file mode 100644 index 0000000..c874a07 --- /dev/null +++ b/pages/layout.tsx @@ -0,0 +1,9 @@ +import Navbar from "./components/navbar"; +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> + +
{children}
+ + ); +} diff --git a/pages/Prop/pagsse.tsx b/pages/prop.tsx similarity index 60% rename from pages/Prop/pagsse.tsx rename to pages/prop.tsx index a70c6d5..65eba58 100644 --- a/pages/Prop/pagsse.tsx +++ b/pages/prop.tsx @@ -1,13 +1,13 @@ -import PropSend from "../components/properties/PropSend"; -import PropSendDestructuring from "../components/properties/PropSendes"; -import PropCondition from "../components/properties/PropCond"; -import PropTernary from "../components/properties/PropTernary"; -import { title } from "../utils/const"; +import PropSend from "./components/properties/PropSend"; +import PropSendDestructuring from "./components/properties/PropSendes"; +import PropCondition from "./components/properties/PropCond"; +import PropTernary from "./components/properties/PropTernary"; +import { title } from "./utils/const"; import Link from "next/link"; export default function PropPage() { return ( -
+

This page about how to send property to other component diff --git a/pages/shallowroute.tsx b/pages/shallowroute.tsx new file mode 100644 index 0000000..1860af4 --- /dev/null +++ b/pages/shallowroute.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; + +// Current URL is '/' +function Page() { + const router = useRouter(); + + useEffect(() => { + // Always do navigations after the first render + router.push("/?counter=10", undefined, { shallow: true }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + // The counter changed! + }, [router.query.counter]); +} + +export default Page; diff --git a/pages/utils/tanStack.ts b/pages/utils/tanStack.ts new file mode 100644 index 0000000..e0149b9 --- /dev/null +++ b/pages/utils/tanStack.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; + +export default function TanStack() { + const { isPending, error, data } = useQuery({ + queryKey: ["recipes"], + queryFn: async () => { + const res = await fetch(`https://dummyjson.com/recipes/`); + const data = await res.json(); + const recipes = data.recipes; + return recipes; + }, + }); + if (isPending) return 'Loading...' + + if (error) return 'An error has occurred: ' + error.message + return data; +} \ No newline at end of file