├── .eslintrc.json ├── .gitignore ├── README.md ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── next.svg └── vercel.svg └── src ├── features └── product │ ├── index.js │ ├── useCreateProduct.js │ ├── useDeleteProduct.js │ ├── useEditProduct.js │ └── useFetchProducts.js ├── lib └── axios.js ├── pages ├── _app.js ├── _document.js ├── api │ └── hello.js ├── index.jsx └── products.js └── styles ├── Home.module.css └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | 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. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-data-fetching", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^2.6.0", 13 | "@emotion/react": "^11.10.6", 14 | "@emotion/styled": "^11.10.6", 15 | "@tanstack/react-query": "^4.29.5", 16 | "axios": "^1.3.6", 17 | "eslint": "8.39.0", 18 | "eslint-config-next": "13.3.1", 19 | "formik": "^2.2.9", 20 | "framer-motion": "^10.12.4", 21 | "next": "13.3.1", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "yup": "^1.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodevoid/react-data-fetching/96f2296779d3680ad7606654c4f4c79d525e098b/public/favicon.ico -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/product/index.js: -------------------------------------------------------------------------------- 1 | export * from "./useCreateProduct" 2 | export * from "./useDeleteProduct" 3 | export * from "./useEditProduct" 4 | export * from "./useFetchProducts" -------------------------------------------------------------------------------- /src/features/product/useCreateProduct.js: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "@/lib/axios"; 2 | import { useMutation } from "@tanstack/react-query"; 3 | 4 | export const useCreateProduct = ({ onSuccess }) => { 5 | return useMutation({ 6 | mutationFn: async (body) => { 7 | const productsResponse = await axiosInstance.post("/products", body); 8 | 9 | return productsResponse; 10 | }, 11 | onSuccess, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/features/product/useDeleteProduct.js: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "@/lib/axios"; 2 | import { useMutation } from "@tanstack/react-query"; 3 | 4 | export const useDeleteProduct = ({ onSuccess }) => { 5 | return useMutation({ 6 | mutationFn: async (id) => { 7 | const productsResponse = await axiosInstance.delete(`/products/${id}`); 8 | 9 | return productsResponse; 10 | }, 11 | onSuccess, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/features/product/useEditProduct.js: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "@/lib/axios"; 2 | import { useMutation } from "@tanstack/react-query"; 3 | 4 | export const useEditProduct = ({ onSuccess }) => { 5 | return useMutation({ 6 | mutationFn: async (body) => { 7 | const productsResponse = await axiosInstance.patch( 8 | `/products/${body.id}`, 9 | body 10 | ); 11 | 12 | return productsResponse; 13 | }, 14 | onSuccess, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/features/product/useFetchProducts.js: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "@/lib/axios"; 2 | import { useQuery } from "@tanstack/react-query"; 3 | 4 | export const useFetchProducts = ({ onError }) => { 5 | return useQuery({ 6 | queryFn: async () => { 7 | const productsResponse = await axiosInstance.get("/products"); 8 | 9 | return productsResponse; 10 | }, 11 | queryKey: ["fetch.products"], 12 | onError, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const axiosInstance = axios.create({ 4 | baseURL: "http://localhost:2000", 5 | }); 6 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from "@chakra-ui/react"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | 4 | const queryClient = new QueryClient({ 5 | defaultOptions: { 6 | queries: { 7 | refetchOnWindowFocus: false, 8 | }, 9 | }, 10 | }); 11 | 12 | export default function App({ Component, pageProps }) { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { 3 | Container, 4 | Heading, 5 | Table, 6 | Tbody, 7 | Td, 8 | Th, 9 | Thead, 10 | Tr, 11 | Spinner, 12 | FormControl, 13 | FormLabel, 14 | Input, 15 | VStack, 16 | Button, 17 | useToast, 18 | } from "@chakra-ui/react"; 19 | import { useFormik } from "formik"; 20 | import { 21 | useCreateProduct, 22 | useDeleteProduct, 23 | useEditProduct, 24 | useFetchProducts, 25 | } from "@/features/product"; 26 | 27 | // formik -> handle forms 28 | // yup -> validate 29 | // react-query -> manage API calls (caching, state, dll.) 30 | 31 | export default function Home() { 32 | const toast = useToast(); 33 | 34 | const { 35 | data, 36 | isLoading: productsIsLoading, 37 | refetch: refetchProducts, 38 | } = useFetchProducts({ 39 | onError: () => { 40 | toast({ 41 | title: "Ada kesalahan terjadi", 42 | status: "error", 43 | }); 44 | }, 45 | }); 46 | 47 | const formik = useFormik({ 48 | initialValues: { 49 | name: "", 50 | price: 0, 51 | description: "", 52 | image: "", 53 | id: 0, 54 | }, 55 | onSubmit: async () => { 56 | const { name, price, description, image, id } = formik.values; 57 | 58 | if (id) { 59 | // Melakukan PATCH /products/{id} 60 | editProduct({ 61 | name, 62 | price: parseInt(price), 63 | description, 64 | image, 65 | id, 66 | }); 67 | 68 | toast({ 69 | title: "Product edited", 70 | status: "success", 71 | }); 72 | } else { 73 | // Melakukan POST /products 74 | createProduct({ 75 | name, 76 | price: parseInt(price), 77 | description, 78 | image, 79 | }); 80 | 81 | toast({ 82 | title: "Product added", 83 | status: "success", 84 | }); 85 | } 86 | 87 | formik.setFieldValue("name", ""); 88 | formik.setFieldValue("price", 0); 89 | formik.setFieldValue("description", ""); 90 | formik.setFieldValue("image", ""); 91 | formik.setFieldValue("id", 0); 92 | }, 93 | }); 94 | 95 | const { mutate: createProduct, isLoading: createProductsIsLoading } = 96 | useCreateProduct({ 97 | onSuccess: () => { 98 | refetchProducts(); 99 | }, 100 | }); 101 | 102 | const { mutate: deleteProduct } = useDeleteProduct({ 103 | onSuccess: () => { 104 | refetchProducts(); 105 | }, 106 | }); 107 | 108 | const { mutate: editProduct, isLoading: editProductIsLoading } = 109 | useEditProduct({ 110 | onSuccess: () => { 111 | refetchProducts(); 112 | }, 113 | }); 114 | 115 | const handleFormInput = (event) => { 116 | formik.setFieldValue(event.target.name, event.target.value); 117 | }; 118 | 119 | const confirmationDelete = (productId) => { 120 | const shouldDelete = confirm("Are you sure?"); 121 | 122 | if (shouldDelete) { 123 | deleteProduct(productId); 124 | toast({ 125 | title: "Deleted product", 126 | status: "info", 127 | }); 128 | } 129 | }; 130 | 131 | const onEditClick = (product) => { 132 | formik.setFieldValue("id", product.id); 133 | formik.setFieldValue("name", product.name); 134 | formik.setFieldValue("description", product.description); 135 | formik.setFieldValue("price", product.price); 136 | formik.setFieldValue("image", product.image); 137 | }; 138 | 139 | const renderProducts = () => { 140 | return data?.data.map((product) => { 141 | return ( 142 | 143 | {product.id} 144 | {product.name} 145 | {product.price} 146 | {product.description} 147 | 148 | 151 | 152 | 153 | 159 | 160 | 161 | ); 162 | }); 163 | }; 164 | 165 | return ( 166 | <> 167 | 168 | Create Next App 169 | 170 | 171 | 172 | 173 |
174 | 175 | Home Page 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | {renderProducts()} 188 | {productsIsLoading && } 189 | 190 |
IDNamePriceDescriptionAction
191 |
192 | 193 | 194 | Product ID 195 | 200 | 201 | 202 | Product Name 203 | 208 | 209 | 210 | Price 211 | 216 | 217 | 218 | Description 219 | 224 | 225 | 226 | Image 227 | 232 | 233 | {createProductsIsLoading || editProductIsLoading ? ( 234 | 235 | ) : ( 236 | 237 | )} 238 | 239 |
240 |
241 |
242 | 243 | ); 244 | } 245 | -------------------------------------------------------------------------------- /src/pages/products.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { 3 | Container, 4 | Heading, 5 | Table, 6 | Tbody, 7 | Td, 8 | Th, 9 | Thead, 10 | Tr, 11 | Spinner 12 | } from "@chakra-ui/react"; 13 | import { useProducts } from "@/features/product/useFetchProducts"; 14 | 15 | export default function Home() { 16 | const { data: products, isLoading } = useProducts(); 17 | 18 | const renderProducts = () => { 19 | return products.map((product) => { 20 | return ( 21 | 22 | {product.id} 23 | {product.name} 24 | {product.price} 25 | {product.description} 26 | {product.image} 27 | 28 | ); 29 | }); 30 | }; 31 | 32 | return ( 33 | <> 34 | 35 | Create Next App 36 | 37 | 38 | 39 | 40 |
41 | 42 | Product Page 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {renderProducts()} 55 | {isLoading && } 56 | 57 |
IDNamePriceDescriptionImage
58 |
59 |
60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | width: var(--max-width); 46 | max-width: 100%; 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo { 108 | position: relative; 109 | } 110 | /* Enable hover only on non-touch devices */ 111 | @media (hover: hover) and (pointer: fine) { 112 | .card:hover { 113 | background: rgba(var(--card-rgb), 0.1); 114 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 115 | } 116 | 117 | .card:hover span { 118 | transform: translateX(4px); 119 | } 120 | } 121 | 122 | @media (prefers-reduced-motion) { 123 | .card:hover span { 124 | transform: none; 125 | } 126 | } 127 | 128 | /* Mobile */ 129 | @media (max-width: 700px) { 130 | .content { 131 | padding: 4rem; 132 | } 133 | 134 | .grid { 135 | grid-template-columns: 1fr; 136 | margin-bottom: 120px; 137 | max-width: 320px; 138 | text-align: center; 139 | } 140 | 141 | .card { 142 | padding: 1rem 2.5rem; 143 | } 144 | 145 | .card h2 { 146 | margin-bottom: 0.5rem; 147 | } 148 | 149 | .center { 150 | padding: 8rem 0 6rem; 151 | } 152 | 153 | .center::before { 154 | transform: none; 155 | height: 300px; 156 | } 157 | 158 | .description { 159 | font-size: 0.8rem; 160 | } 161 | 162 | .description a { 163 | padding: 1rem; 164 | } 165 | 166 | .description p, 167 | .description div { 168 | display: flex; 169 | justify-content: center; 170 | position: fixed; 171 | width: 100%; 172 | } 173 | 174 | .description p { 175 | align-items: center; 176 | inset: 0 0 auto; 177 | padding: 2rem 1rem 1.4rem; 178 | border-radius: 0; 179 | border: none; 180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 181 | background: linear-gradient( 182 | to bottom, 183 | rgba(var(--background-start-rgb), 1), 184 | rgba(var(--callout-rgb), 0.5) 185 | ); 186 | background-clip: padding-box; 187 | backdrop-filter: blur(24px); 188 | } 189 | 190 | .description div { 191 | align-items: flex-end; 192 | pointer-events: none; 193 | inset: auto 0 0; 194 | padding: 2rem; 195 | height: 200px; 196 | background: linear-gradient( 197 | to bottom, 198 | transparent 0%, 199 | rgb(var(--background-end-rgb)) 40% 200 | ); 201 | z-index: 1; 202 | } 203 | } 204 | 205 | /* Tablet and Smaller Desktop */ 206 | @media (min-width: 701px) and (max-width: 1120px) { 207 | .grid { 208 | grid-template-columns: repeat(2, 50%); 209 | } 210 | } 211 | 212 | @media (prefers-color-scheme: dark) { 213 | .vercelLogo { 214 | filter: invert(1); 215 | } 216 | 217 | .logo { 218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 219 | } 220 | } 221 | 222 | @keyframes rotate { 223 | from { 224 | transform: rotate(360deg); 225 | } 226 | to { 227 | transform: rotate(0deg); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | --------------------------------------------------------------------------------