├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── components.json ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public └── assets │ ├── audio-posters │ ├── aphi-bum-dhi.jpeg │ ├── bay-matshu.jpeg │ ├── bum-choe.jpeg │ ├── choe-tsheyi-ra.jpeg │ ├── dhue-hema.jpeg │ ├── dochula.jpeg │ ├── drakcin-gyalmo.jpeg │ ├── druk-mi-yong-gi-moenlam.jpeg │ ├── funk-that.jpeg │ ├── gasey.jpg │ ├── hamigoway.jpeg │ ├── hangtoma.jpeg │ ├── jarim-choe-dha-nga.jpeg │ ├── jawey-mo.jpeg │ ├── lam-joyee-joyee.jpeg │ ├── namkhai-kam.jpeg │ ├── norzib.jpeg │ ├── nyingthuenma.jpeg │ ├── say-you-love-me.jpeg │ ├── sem-bara-macha.jpeg │ ├── thong-ra-mathong.jpeg │ ├── ya-taru-ma-taru.jpeg │ ├── yar-la-aee.jpeg │ └── zay-di-bay-ru.jpeg │ └── audio │ ├── aphi-bum-dhi.mp3 │ ├── bay-matshu.mp3 │ ├── bum-choe.mp3 │ ├── choe-tsheyi-ra.mp3 │ ├── dhue-hema.mp3 │ ├── dochula.mp3 │ ├── draksin-gyalmo.mp3 │ ├── druk-mi-yong-gi-moenlam.mp3 │ ├── funk-that.mp3 │ ├── gasey.mp3 │ ├── hamigoway.mp3 │ ├── hangtoma.mp3 │ ├── jarim-choe-dha-nga.mp3 │ ├── jawey-mo.mp3 │ ├── lam-joyee-joyee.mp3 │ ├── namkhai-kam.mp3 │ ├── norzib.mp3 │ ├── nyingthuenma.mp3 │ ├── say-you-love-me.mp3 │ ├── sem-bara-macha.mp3 │ ├── thong-ra-mathong.mp3 │ ├── ya-taru-ma-taru.mp3 │ ├── yar-la-aee.mp3 │ └── zay-di-bayru.mp3 ├── src ├── actions │ └── parallel-route.ts ├── app │ ├── api │ │ └── og │ │ │ ├── Geist-Bold.ttf │ │ │ ├── Geist-Regular.ttf │ │ │ ├── Geist-SemiBold.ttf │ │ │ └── route.tsx │ ├── features │ │ ├── (features-listing) │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── (implementations) │ │ │ ├── infinite-scrolling │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ ├── with-server-action │ │ │ │ ├── (page) │ │ │ │ │ ├── error.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── @explanation │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ └── with-use-swr │ │ │ │ ├── (page) │ │ │ │ ├── error.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── @explanation │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── intercepting-routes │ │ │ ├── (page) │ │ │ │ ├── error.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── @explanation │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── @interceptionModal │ │ │ │ ├── (...)product │ │ │ │ │ └── [productId] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── default.tsx │ │ │ └── layout.tsx │ │ │ ├── loading.tsx │ │ │ ├── music-streaming │ │ │ ├── (page) │ │ │ │ ├── error.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── @explanation │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ │ ├── pagination │ │ │ ├── client-side │ │ │ │ ├── (page) │ │ │ │ │ ├── error.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── @explanation │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── server-side │ │ │ │ ├── (page) │ │ │ │ ├── error.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── @explanation │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ └── parallel-routing │ │ │ ├── @explanation │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── @firstPage │ │ │ ├── error.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── @fourthPage │ │ │ ├── error.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── @secondPage │ │ │ ├── error.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── @thirdPage │ │ │ ├── error.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── global-error.tsx │ ├── icon.svg │ ├── layout.tsx │ ├── loading.tsx │ ├── page.tsx │ └── product │ │ └── [productId] │ │ ├── loading.tsx │ │ └── page.tsx ├── components │ ├── app-breadcrumb │ │ └── index.tsx │ ├── features-listing │ │ └── index.tsx │ ├── features │ │ ├── infinite-scrolling │ │ │ ├── with-server-action │ │ │ │ ├── explanation │ │ │ │ │ └── index.tsx │ │ │ │ └── implementation │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── wsa-load-more.tsx │ │ │ └── with-use-swr │ │ │ │ ├── explanation │ │ │ │ └── index.tsx │ │ │ │ └── implementation │ │ │ │ ├── index.tsx │ │ │ │ └── wswr-load-more.tsx │ │ ├── intercepting-routes │ │ │ ├── explanation │ │ │ │ ├── index.tsx │ │ │ │ └── route-interception-folder-structure.tsx │ │ │ ├── implementation │ │ │ │ └── indext.tsx │ │ │ └── product-detail-modal.tsx │ │ ├── music-streaming │ │ │ ├── explanation │ │ │ │ └── index.tsx │ │ │ └── implementation │ │ │ │ ├── audio-card-wrapper │ │ │ │ └── index.tsx │ │ │ │ ├── audio-card │ │ │ │ ├── index.tsx │ │ │ │ └── play-animation.tsx │ │ │ │ ├── audio-player-wrapper │ │ │ │ └── index.tsx │ │ │ │ ├── audio-player │ │ │ │ ├── audio-loop │ │ │ │ │ └── index.tsx │ │ │ │ ├── audio-shuffle │ │ │ │ │ └── index.tsx │ │ │ │ ├── audio-slider │ │ │ │ │ └── index.tsx │ │ │ │ ├── audio-sound │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ ├── pagination │ │ │ ├── client-side │ │ │ │ ├── explanation │ │ │ │ │ └── index.tsx │ │ │ │ └── implementation │ │ │ │ │ └── index.tsx │ │ │ ├── pagination-data-table.tsx │ │ │ ├── pagination-nav.tsx │ │ │ ├── pagination-table-header.tsx │ │ │ ├── product-table-columns.tsx │ │ │ └── server-side │ │ │ │ ├── explanation │ │ │ │ └── index.tsx │ │ │ │ └── implementation │ │ │ │ └── index.tsx │ │ └── parallel-routing │ │ │ ├── explanation │ │ │ ├── index.tsx │ │ │ └── parallel-routing-folder-structure.tsx │ │ │ ├── implementation │ │ │ └── index.tsx │ │ │ ├── parallel-route-content.tsx │ │ │ ├── parallel-route-error.tsx │ │ │ ├── parallel-route-loader.tsx │ │ │ └── throw-error.tsx │ ├── hero-feature-separator │ │ └── index.tsx │ ├── layouts │ │ └── feature-implementation-explanation │ │ │ └── index.tsx │ ├── libraries-used │ │ └── index.tsx │ ├── loaders │ │ ├── action-loader.tsx │ │ ├── content-loader.tsx │ │ ├── page-loader.tsx │ │ └── progress-loader.tsx │ ├── logo │ │ └── index.tsx │ ├── mobile-feature-layout-header │ │ └── index.tsx │ ├── mode-toggle │ │ └── index.tsx │ ├── parallel-route-children-skeleton │ │ └── index.tsx │ ├── product │ │ ├── product-card.tsx │ │ ├── product-details │ │ │ ├── index.tsx │ │ │ └── product-image-carousel.tsx │ │ └── product-list.tsx │ ├── providers │ │ ├── audio-player-init-provider.tsx │ │ └── theme-provider.tsx │ ├── search-features │ │ └── index.tsx │ ├── shared │ │ ├── back-navigation.tsx │ │ ├── data-table │ │ │ └── index.tsx │ │ ├── page-error.tsx │ │ └── param-update-input.tsx │ ├── site-footer │ │ └── index.tsx │ ├── site-header │ │ ├── desktop-nav.tsx │ │ ├── index.tsx │ │ └── mobile-nav.tsx │ ├── templates │ │ ├── feature-explanation.tsx │ │ └── feature-implementation.tsx │ ├── text-highlight.tsx │ │ └── index.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── file-tree.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── scroll-area.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ └── tooltip.tsx ├── config │ ├── hero-features.ts │ └── navigation.ts ├── helpers │ └── fetch-helper.ts ├── hooks │ ├── use-breadcrumb-objects.ts │ ├── use-remove-search-params.ts │ └── use-tailwind-media-query.ts ├── lib │ ├── constants │ │ ├── metadata.ts │ │ ├── misc.ts │ │ ├── song-list.ts │ │ └── tailwind-device-width.ts │ ├── types │ │ ├── hero-feature.ts │ │ ├── misc.ts │ │ ├── navigation.ts │ │ └── product.ts │ └── utils │ │ ├── index.ts │ │ ├── misc.ts │ │ ├── pagination.ts │ │ ├── rgb-data-url.ts │ │ └── shadcn.utils.ts ├── middleware.ts ├── services │ └── product-service.ts ├── stores │ └── audio-player-init-store.ts └── styles │ ├── action-loader.module.css │ ├── content-loader.module.css │ ├── globals.css │ ├── play-animation.module.css │ └── progress-loader.module.css ├── tailwind.config.ts ├── tsconfig.json ├── vercel.json └── yarn.lock /.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 | .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 | .env.development 31 | .env.production 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "AQABAPAA", 4 | "autoplay", 5 | "BAAAAAAALAAAAAABAAEAAAICRAEA", 6 | "clsx", 7 | "cmdk", 8 | "doesn", 9 | "dorji", 10 | "draksin", 11 | "dummyjson", 12 | "embla", 13 | "firstname", 14 | "gyalmo", 15 | "imgur", 16 | "ISWOSA", 17 | "ISWSA", 18 | "lastname", 19 | "Linkin", 20 | "Noto", 21 | "nprogress", 22 | "nuqs", 23 | "pokeapi", 24 | "Pokemons", 25 | "shadcn", 26 | "tanstack", 27 | "uidotdev", 28 | "usehooks", 29 | "vaul", 30 | "WOSA", 31 | "wswr", 32 | "XSMALL", 33 | "XXSMALL", 34 | "xxxs", 35 | "zipcode", 36 | "Zustand" 37 | ], 38 | "editor.formatOnSave": true 39 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dorji Tshering 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js App Router Demo 2 | 3 | Welcome to the Next.js App Router Demo! This project showcases various features and concepts implemented using the Next.js App Router. The application demonstrates real-world examples of infinite scrolling, pagination, parallel routing, and more. 4 | 5 | ## Features 6 | 7 | - **Infinite Scrolling**: Implemented using server actions and `useSWR`. 8 | - **Pagination**: Server-side and client-side data fetching with `tanstack/react-table`. 9 | - **Parallel Routing**: Render multiple pages simultaneously. 10 | - **Intercepting Routes**: Modal routing without losing context. 11 | - **Music & Video Streaming**: Real-time media streaming examples. 12 | 13 | ## Tech Stack 14 | 15 | - **Next.js** 16 | - **TailwindCSS** 17 | - **React Icons** 18 | - **Radix UI** 19 | 20 | ## Getting Started 21 | 22 | 1. Clone the repository 23 | 2. Install dependencies: `npm install` 24 | 3. Run the development server: `npm run dev` 25 | 26 | ## License 27 | 28 | This project is open-source and available under the [MIT License](LICENSE). 29 | 30 | --- 31 | 32 | Built with ❤️ by [dorji-dev](https://github.com/dorji-dev) 33 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "cdn.dummyjson.com", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app-router-features", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-accordion": "^1.1.2", 13 | "@radix-ui/react-aspect-ratio": "^1.0.3", 14 | "@radix-ui/react-avatar": "^1.0.4", 15 | "@radix-ui/react-checkbox": "^1.0.4", 16 | "@radix-ui/react-dialog": "^1.0.5", 17 | "@radix-ui/react-dropdown-menu": "^2.0.6", 18 | "@radix-ui/react-hover-card": "^1.0.7", 19 | "@radix-ui/react-navigation-menu": "^1.1.4", 20 | "@radix-ui/react-popover": "^1.0.7", 21 | "@radix-ui/react-progress": "^1.0.3", 22 | "@radix-ui/react-scroll-area": "^1.0.5", 23 | "@radix-ui/react-separator": "^1.0.3", 24 | "@radix-ui/react-slider": "^1.1.2", 25 | "@radix-ui/react-slot": "^1.0.2", 26 | "@radix-ui/react-switch": "^1.0.3", 27 | "@radix-ui/react-tooltip": "^1.0.7", 28 | "@tanstack/react-table": "^8.13.2", 29 | "@uidotdev/usehooks": "^2.4.1", 30 | "@vercel/speed-insights": "^1.0.10", 31 | "class-variance-authority": "^0.7.0", 32 | "clsx": "^2.1.0", 33 | "cmdk": "^1.0.0", 34 | "embla-carousel-autoplay": "^8.0.0", 35 | "embla-carousel-react": "^8.0.0", 36 | "geist": "^1.2.2", 37 | "next": "15.2.2", 38 | "next-themes": "^0.2.1", 39 | "nuqs": "^1.17.1", 40 | "react": "19.0.0", 41 | "react-dom": "19.0.0", 42 | "react-error-boundary": "^4.0.13", 43 | "react-icons": "^5.0.1", 44 | "react-intersection-observer": "^9.8.1", 45 | "react-use-audio-player": "^2.2.0", 46 | "swr": "^2.2.5", 47 | "tailwind-merge": "^2.2.1", 48 | "tailwindcss-animate": "^1.0.7", 49 | "vaul": "^0.9.0", 50 | "zustand": "^4.5.2" 51 | }, 52 | "devDependencies": { 53 | "@tailwindcss/postcss": "^4.0.14", 54 | "@types/node": "^20", 55 | "@types/react": "19.0.10", 56 | "@types/react-dom": "19.0.4", 57 | "eslint": "^8", 58 | "eslint-config-next": "15.2.2", 59 | "postcss": "^8.5.3", 60 | "tailwindcss": "^4.0.14", 61 | "typescript": "^5", 62 | "use-resize-observer": "^9.1.0" 63 | }, 64 | "resolutions": { 65 | "@types/react": "19.0.10", 66 | "@types/react-dom": "19.0.4" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-anonymous-default-export 2 | export default { 3 | plugins: { 4 | "@tailwindcss/postcss": {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/assets/audio-posters/aphi-bum-dhi.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/aphi-bum-dhi.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/bay-matshu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/bay-matshu.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/bum-choe.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/bum-choe.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/choe-tsheyi-ra.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/choe-tsheyi-ra.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/dhue-hema.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/dhue-hema.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/dochula.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/dochula.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/drakcin-gyalmo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/drakcin-gyalmo.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/druk-mi-yong-gi-moenlam.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/druk-mi-yong-gi-moenlam.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/funk-that.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/funk-that.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/gasey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/gasey.jpg -------------------------------------------------------------------------------- /public/assets/audio-posters/hamigoway.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/hamigoway.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/hangtoma.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/hangtoma.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/jarim-choe-dha-nga.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/jarim-choe-dha-nga.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/jawey-mo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/jawey-mo.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/lam-joyee-joyee.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/lam-joyee-joyee.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/namkhai-kam.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/namkhai-kam.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/norzib.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/norzib.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/nyingthuenma.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/nyingthuenma.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/say-you-love-me.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/say-you-love-me.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/sem-bara-macha.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/sem-bara-macha.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/thong-ra-mathong.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/thong-ra-mathong.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/ya-taru-ma-taru.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/ya-taru-ma-taru.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/yar-la-aee.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/yar-la-aee.jpeg -------------------------------------------------------------------------------- /public/assets/audio-posters/zay-di-bay-ru.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio-posters/zay-di-bay-ru.jpeg -------------------------------------------------------------------------------- /public/assets/audio/aphi-bum-dhi.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/aphi-bum-dhi.mp3 -------------------------------------------------------------------------------- /public/assets/audio/bay-matshu.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/bay-matshu.mp3 -------------------------------------------------------------------------------- /public/assets/audio/bum-choe.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/bum-choe.mp3 -------------------------------------------------------------------------------- /public/assets/audio/choe-tsheyi-ra.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/choe-tsheyi-ra.mp3 -------------------------------------------------------------------------------- /public/assets/audio/dhue-hema.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/dhue-hema.mp3 -------------------------------------------------------------------------------- /public/assets/audio/dochula.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/dochula.mp3 -------------------------------------------------------------------------------- /public/assets/audio/draksin-gyalmo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/draksin-gyalmo.mp3 -------------------------------------------------------------------------------- /public/assets/audio/druk-mi-yong-gi-moenlam.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/druk-mi-yong-gi-moenlam.mp3 -------------------------------------------------------------------------------- /public/assets/audio/funk-that.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/funk-that.mp3 -------------------------------------------------------------------------------- /public/assets/audio/gasey.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/gasey.mp3 -------------------------------------------------------------------------------- /public/assets/audio/hamigoway.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/hamigoway.mp3 -------------------------------------------------------------------------------- /public/assets/audio/hangtoma.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/hangtoma.mp3 -------------------------------------------------------------------------------- /public/assets/audio/jarim-choe-dha-nga.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/jarim-choe-dha-nga.mp3 -------------------------------------------------------------------------------- /public/assets/audio/jawey-mo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/jawey-mo.mp3 -------------------------------------------------------------------------------- /public/assets/audio/lam-joyee-joyee.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/lam-joyee-joyee.mp3 -------------------------------------------------------------------------------- /public/assets/audio/namkhai-kam.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/namkhai-kam.mp3 -------------------------------------------------------------------------------- /public/assets/audio/norzib.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/norzib.mp3 -------------------------------------------------------------------------------- /public/assets/audio/nyingthuenma.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/nyingthuenma.mp3 -------------------------------------------------------------------------------- /public/assets/audio/say-you-love-me.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/say-you-love-me.mp3 -------------------------------------------------------------------------------- /public/assets/audio/sem-bara-macha.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/sem-bara-macha.mp3 -------------------------------------------------------------------------------- /public/assets/audio/thong-ra-mathong.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/thong-ra-mathong.mp3 -------------------------------------------------------------------------------- /public/assets/audio/ya-taru-ma-taru.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/ya-taru-ma-taru.mp3 -------------------------------------------------------------------------------- /public/assets/audio/yar-la-aee.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/yar-la-aee.mp3 -------------------------------------------------------------------------------- /public/assets/audio/zay-di-bayru.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/public/assets/audio/zay-di-bayru.mp3 -------------------------------------------------------------------------------- /src/actions/parallel-route.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | /** 4 | * A server action that simulates fake api call for parallel routing demo 5 | * @param timeOutValue 6 | * @param searchParam 7 | */ 8 | export const fakeAPI = async (timeOutValue: number, searchParam: string) => { 9 | const promise = new Promise((resolve) => { 10 | setTimeout(() => { 11 | resolve(`${searchParam}`); 12 | }, timeOutValue); 13 | }); 14 | await promise; 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/api/og/Geist-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/src/app/api/og/Geist-Bold.ttf -------------------------------------------------------------------------------- /src/app/api/og/Geist-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/src/app/api/og/Geist-Regular.ttf -------------------------------------------------------------------------------- /src/app/api/og/Geist-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/src/app/api/og/Geist-SemiBold.ttf -------------------------------------------------------------------------------- /src/app/api/og/route.tsx: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { ImageResponse } from "next/og"; 3 | 4 | export const runtime = "edge"; 5 | 6 | const geistRegular = fetch( 7 | new URL("./Geist-Regular.ttf", import.meta.url) 8 | ).then((res) => res.arrayBuffer()); 9 | 10 | const geistSemiBold = fetch( 11 | new URL("./Geist-SemiBold.ttf", import.meta.url) 12 | ).then((res) => res.arrayBuffer()); 13 | 14 | const geistBold = fetch(new URL("./Geist-Bold.ttf", import.meta.url)).then( 15 | (res) => res.arrayBuffer() 16 | ); 17 | 18 | export async function GET(req: NextRequest): Promise { 19 | try { 20 | const { searchParams } = new URL(req.url); 21 | 22 | const title = searchParams.has("title") 23 | ? searchParams.get("title") 24 | : "Every day features with Next.js App Router."; 25 | 26 | return new ImageResponse( 27 | ( 28 |
35 | 36 | 40 | NEXT 41 | 45 | .js 46 | 47 | 48 | 49 | 53 | APP 54 | 55 | 56 |

60 | {title} 61 |

62 |

63 | @dorji-dev 64 |

65 |
66 | ), 67 | { 68 | width: 843, 69 | height: 441, 70 | fonts: [ 71 | { 72 | name: "GeistRegular", 73 | data: await geistRegular, 74 | style: "normal", 75 | weight: 400, 76 | }, 77 | { 78 | name: "GeistSemiBold", 79 | data: await geistSemiBold, 80 | style: "normal", 81 | weight: 400, 82 | }, 83 | { 84 | name: "GeistBold", 85 | data: await geistBold, 86 | style: "normal", 87 | weight: 400, 88 | }, 89 | ], 90 | } 91 | ); 92 | } catch (e) { 93 | if (!(e instanceof Error)) throw e; 94 | 95 | // eslint-disable-next-line no-console 96 | console.log(e.message); 97 | return new Response(`Failed to generate the image`, { 98 | status: 500, 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/features/(features-listing)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(features-listing)/page.tsx: -------------------------------------------------------------------------------- 1 | import AppBreadCrumb from "@/components/app-breadcrumb"; 2 | import FeaturesListing from "@/components/features-listing"; 3 | import SearchFeatures from "@/components/search-features"; 4 | import { heroFeatures } from "@/config/hero-features"; 5 | 6 | const FeaturesPage = () => { 7 | return ( 8 |
9 |
10 | 11 |
12 |

Features and Demos

13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default FeaturesPage; 24 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/page.tsx: -------------------------------------------------------------------------------- 1 | import AppBreadCrumb from "@/components/app-breadcrumb"; 2 | import TextHighlight from "@/components/text-highlight.tsx"; 3 | import { Metadata, Route } from "next"; 4 | import Link from "next/link"; 5 | 6 | export const metadata: Metadata = { 7 | title: "Infinite scrolling with server action and useSWR", 8 | description: 9 | "Implementation of infinite scrolling in Next.js app router with server actions and useSWR hook", 10 | openGraph: { 11 | images: ["/api/og?title=Infinite Scrolling"], 12 | }, 13 | }; 14 | 15 | const InfiniteScrollingPage = () => { 16 | return ( 17 |
18 |
19 | 20 |
21 |
Infinite scrolling
22 |

23 | Implementation of infinite scrolling in NextJS app router with{" "} 24 | {" "} 28 | and {" "} 29 | hook. Data fetching on the server side vs client side. 30 |

31 |
32 | 36 | With server action 37 | 38 | 42 | With useSWR hook 43 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default InfiniteScrollingPage; 50 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-server-action/(page)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageError from "@/components/shared/page-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-server-action/(page)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-server-action/(page)/page.tsx: -------------------------------------------------------------------------------- 1 | import ISWithServerActionImplementation from "@/components/features/infinite-scrolling/with-server-action/implementation"; 2 | import ContentLoader from "@/components/loaders/content-loader"; 3 | import FeatureImplementationTemplate from "@/components/templates/feature-implementation"; 4 | import type { Metadata } from "next"; 5 | import { Suspense } from "react"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Infinite scrolling with server actions", 9 | description: 10 | "Implementation of infinite scrolling with server component and server actions", 11 | openGraph: { 12 | images: ["/api/og?title=Infinite Scrolling with Server Actions"], 13 | }, 14 | }; 15 | 16 | const InfiniteScrollWithServerActionPage = async (props: { 17 | searchParams: Promise<{ search: string }>; 18 | }) => { 19 | const searchParams = await props.searchParams; 20 | return ( 21 | 27 | 30 | 31 | 32 | } 33 | > 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default InfiniteScrollWithServerActionPage; 41 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-server-action/@explanation/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-server-action/@explanation/page.tsx: -------------------------------------------------------------------------------- 1 | import ISWithServerActionExplanation from "@/components/features/infinite-scrolling/with-server-action/explanation"; 2 | import FeatureExplanationTemplate from "@/components/templates/feature-explanation"; 3 | 4 | const ISWithServerActionExplanationPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ISWithServerActionExplanationPage; 13 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-server-action/layout.tsx: -------------------------------------------------------------------------------- 1 | import FeatureImplementationExplanationLayout from "@/components/layouts/feature-implementation-explanation"; 2 | 3 | export default FeatureImplementationExplanationLayout; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-use-swr/(page)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageError from "@/components/shared/page-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-use-swr/(page)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-use-swr/(page)/page.tsx: -------------------------------------------------------------------------------- 1 | import ISWithUseSWRImplementation from "@/components/features/infinite-scrolling/with-use-swr/implementation"; 2 | import FeatureImplementationTemplate from "@/components/templates/feature-implementation"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Infinite scrolling with useSWR", 7 | description: 8 | "Implementation of infinite scrolling with useSWR and client side rendering", 9 | openGraph: { 10 | images: ["/api/og?title=Infinite Scrolling with useSWR"], 11 | }, 12 | }; 13 | 14 | const InfiniteScrollWithUseSWRPage = () => { 15 | return ( 16 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default InfiniteScrollWithUseSWRPage; 27 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-use-swr/@explanation/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-use-swr/@explanation/page.tsx: -------------------------------------------------------------------------------- 1 | import ISWithUseSWRExplanation from "@/components/features/infinite-scrolling/with-use-swr/explanation"; 2 | import FeatureExplanationTemplate from "@/components/templates/feature-explanation"; 3 | 4 | const ISWithUseSWRExplanationPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ISWithUseSWRExplanationPage; 13 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/infinite-scrolling/with-use-swr/layout.tsx: -------------------------------------------------------------------------------- 1 | import FeatureImplementationExplanationLayout from "@/components/layouts/feature-implementation-explanation"; 2 | 3 | export default FeatureImplementationExplanationLayout; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/(page)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageError from "@/components/shared/page-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/(page)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/(page)/page.tsx: -------------------------------------------------------------------------------- 1 | import InterceptingRoutesImplementation from "@/components/features/intercepting-routes/implementation/indext"; 2 | import ContentLoader from "@/components/loaders/content-loader"; 3 | import FeatureImplementationTemplate from "@/components/templates/feature-implementation"; 4 | import type { Metadata } from "next"; 5 | import { Suspense } from "react"; 6 | 7 | export const metadata: Metadata = { 8 | title: "NextJs intercepting routes", 9 | description: 10 | "Implementation of route interception in NextJs app router, also known as modal routing", 11 | openGraph: { 12 | images: ["/api/og?title=Route Interception"], 13 | }, 14 | }; 15 | 16 | const InterceptingRoutesPage = () => { 17 | return ( 18 | 23 | 26 | 27 | 28 | } 29 | > 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default InterceptingRoutesPage; 37 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/@explanation/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/@explanation/page.tsx: -------------------------------------------------------------------------------- 1 | import InterceptingRoutesExplanation from "@/components/features/intercepting-routes/explanation"; 2 | import FeatureExplanationTemplate from "@/components/templates/feature-explanation"; 3 | 4 | const InterceptingRoutesExplanationPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default InterceptingRoutesExplanationPage; 13 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/@interceptionModal/(...)product/[productId]/page.tsx: -------------------------------------------------------------------------------- 1 | import ProductDetailModal from "@/components/features/intercepting-routes/product-detail-modal"; 2 | import { Suspense } from "react"; 3 | import ContentLoader from "@/components/loaders/content-loader"; 4 | import ProductDetails from "@/components/product/product-details"; 5 | 6 | const InterceptedProductDetailPage = () => { 7 | return ( 8 | 9 | 12 | 13 | 14 | } 15 | > 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default InterceptedProductDetailPage; 23 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/@interceptionModal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/intercepting-routes/layout.tsx: -------------------------------------------------------------------------------- 1 | import FeatureImplementationExplanationLayout from "@/components/layouts/feature-implementation-explanation"; 2 | import { PropsWithChildren, ReactNode } from "react"; 3 | 4 | interface InterceptingRouteLayoutProps extends PropsWithChildren { 5 | interceptionModal: ReactNode; 6 | explanation: ReactNode; 7 | } 8 | 9 | // Since the route interception is inside "intercepting-routes" segment, 10 | // it must have a layout.{ts, tsx, js, jsx} file that receives the slot 11 | // "interceptionModal" and renders it. 12 | const InterceptingRouteLayout = ({ 13 | children, 14 | interceptionModal, 15 | explanation, 16 | }: InterceptingRouteLayoutProps) => { 17 | return ( 18 | <> 19 | {interceptionModal} 20 | 21 | {children} 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default InterceptingRouteLayout; 28 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/music-streaming/(page)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageError from "@/components/shared/page-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; -------------------------------------------------------------------------------- /src/app/features/(implementations)/music-streaming/(page)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/music-streaming/(page)/page.tsx: -------------------------------------------------------------------------------- 1 | import MusicStreamingImplementation from "@/components/features/music-streaming/implementation"; 2 | import ContentLoader from "@/components/loaders/content-loader"; 3 | import FeatureImplementationTemplate from "@/components/templates/feature-implementation"; 4 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 5 | import type { Metadata } from "next"; 6 | import { Suspense } from "react"; 7 | 8 | export const metadata: Metadata = { 9 | title: "Music streaming", 10 | description: "Music streaming with NextJs App Router", 11 | openGraph: { 12 | images: ["/api/og?title=Music Streaming"], 13 | }, 14 | }; 15 | 16 | const MusicStreamingImplementationPage = () => { 17 | return ( 18 | 23 |
24 | 25 | 26 | Disclaimer 27 | 28 | 29 | All songs and images featured on this page are the artistic property 30 | of their respective creators and is for the{" "} 31 | educational purposes only. And a 32 | shout-out to all the talented Bhutanese artists behind the songs and 33 | images featured here. 34 | 35 | 36 | 39 | 40 |
41 | } 42 | > 43 | 44 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | export default MusicStreamingImplementationPage; 51 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/music-streaming/@explanation/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/music-streaming/@explanation/page.tsx: -------------------------------------------------------------------------------- 1 | import MusicStreamingExplanation from "@/components/features/music-streaming/explanation"; 2 | import FeatureExplanationTemplate from "@/components/templates/feature-explanation"; 3 | 4 | const MusicStreamingExplanationPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default MusicStreamingExplanationPage; 13 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/music-streaming/layout.tsx: -------------------------------------------------------------------------------- 1 | import FeatureImplementationExplanationLayout from "@/components/layouts/feature-implementation-explanation"; 2 | 3 | export default FeatureImplementationExplanationLayout; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/client-side/(page)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageError from "@/components/shared/page-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/client-side/(page)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/client-side/(page)/page.tsx: -------------------------------------------------------------------------------- 1 | import ClientSidePaginationImplementation from "@/components/features/pagination/client-side/implementation"; 2 | import FeatureImplementationTemplate from "@/components/templates/feature-implementation"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Pagination with client side data fetching", 7 | description: 8 | "Implementation pagination in NextJs app router with client side data fetching", 9 | openGraph: { 10 | images: ["/api/og?title=Pagination Client Side"], 11 | }, 12 | }; 13 | 14 | const ClientSidePaginationPage = () => { 15 | return ( 16 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default ClientSidePaginationPage; 27 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/client-side/@explanation/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/client-side/@explanation/page.tsx: -------------------------------------------------------------------------------- 1 | import ClientSidePaginationExplanation from "@/components/features/pagination/client-side/explanation"; 2 | import FeatureExplanationTemplate from "@/components/templates/feature-explanation"; 3 | 4 | const ClientSidePaginationExplanationPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ClientSidePaginationExplanationPage; 13 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/client-side/layout.tsx: -------------------------------------------------------------------------------- 1 | import FeatureImplementationExplanationLayout from "@/components/layouts/feature-implementation-explanation"; 2 | 3 | export default FeatureImplementationExplanationLayout; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/page.tsx: -------------------------------------------------------------------------------- 1 | import AppBreadCrumb from "@/components/app-breadcrumb"; 2 | import { Metadata, Route } from "next"; 3 | import Link from "next/link"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Pagination", 7 | description: "Pagination with NextJs App Router", 8 | openGraph: { 9 | images: ["/api/og?title=Pagination"], 10 | }, 11 | }; 12 | 13 | const PaginationPage = () => { 14 | return ( 15 |
16 |
17 | 18 |
19 |
Pagination
20 |

21 | Implementation of pagination in NextJS app router with client side and 22 | server side data fetching. 23 |

24 |
25 | 29 | Server side pagination 30 | 31 | 35 | Client side pagination 36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default PaginationPage; 43 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/server-side/(page)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageError from "@/components/shared/page-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; 11 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/server-side/(page)/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/server-side/(page)/page.tsx: -------------------------------------------------------------------------------- 1 | import ServerSidePaginationImplementation from "@/components/features/pagination/server-side/implementation"; 2 | import ContentLoader from "@/components/loaders/content-loader"; 3 | import FeatureImplementationTemplate from "@/components/templates/feature-implementation"; 4 | import type { Metadata } from "next"; 5 | import { Suspense } from "react"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Pagination with server side data fetching", 9 | description: 10 | "Implementation pagination in NextJs with server side data fetching", 11 | openGraph: { 12 | images: ["/api/og?title=Pagination Server Side"], 13 | }, 14 | }; 15 | 16 | const ServerSidePaginationPage = async (props: { 17 | searchParams: Promise<{ page: string; search: string }>; 18 | }) => { 19 | const searchParams = await props.searchParams; 20 | return ( 21 | 26 | 29 | 30 | 31 | } 32 | > 33 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default ServerSidePaginationPage; 43 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/server-side/@explanation/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/server-side/@explanation/page.tsx: -------------------------------------------------------------------------------- 1 | import ServerSidePaginationExplanation from "@/components/features/pagination/server-side/explanation"; 2 | import FeatureExplanationTemplate from "@/components/templates/feature-explanation"; 3 | 4 | const ServerSidePaginationExplanationPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ServerSidePaginationExplanationPage; 13 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/pagination/server-side/layout.tsx: -------------------------------------------------------------------------------- 1 | import FeatureImplementationExplanationLayout from "@/components/layouts/feature-implementation-explanation"; 2 | 3 | export default FeatureImplementationExplanationLayout; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@explanation/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; 4 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@explanation/page.tsx: -------------------------------------------------------------------------------- 1 | import ParallelRoutingExplanation from "@/components/features/parallel-routing/explanation"; 2 | import FeatureExplanationTemplate from "@/components/templates/feature-explanation"; 3 | 4 | const ParallelRoutingExplanationPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ParallelRoutingExplanationPage; 13 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@firstPage/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ParallelRouteError from "@/components/features/parallel-routing/parallel-route-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset, error }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@firstPage/loading.tsx: -------------------------------------------------------------------------------- 1 | import ParallelRouteLoader from "@/components/features/parallel-routing/parallel-route-loader"; 2 | 3 | const Loading = () => { 4 | return ; 5 | }; 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@firstPage/page.tsx: -------------------------------------------------------------------------------- 1 | import { fakeAPI } from "@/actions/parallel-route"; 2 | import ParallelRoutePageContent from "@/components/features/parallel-routing/parallel-route-content"; 3 | 4 | // Force the page to dynamic rendering via searchParams access 5 | // Using pages searchParams prop will opt the whole route into dynamic rendering 6 | // https://nextjs.org/docs/app/building-your-application/rendering/server-components#switching-to-dynamic-rendering 7 | // This is just for the demo, so you can refresh the page and see how 8 | // page streaming works. 9 | const FirstPage = async ( 10 | props: { 11 | searchParams: Promise<{ value: string }>; 12 | } 13 | ) => { 14 | const searchParams = await props.searchParams; 15 | await fakeAPI(2000, searchParams.value); 16 | return ; 17 | }; 18 | 19 | export default FirstPage; 20 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@fourthPage/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ParallelRouteError from "@/components/features/parallel-routing/parallel-route-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset, error }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; 11 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@fourthPage/loading.tsx: -------------------------------------------------------------------------------- 1 | import ParallelRouteLoader from "@/components/features/parallel-routing/parallel-route-loader"; 2 | 3 | const Loading = () => { 4 | return ; 5 | }; 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@fourthPage/page.tsx: -------------------------------------------------------------------------------- 1 | import { fakeAPI } from "@/actions/parallel-route"; 2 | import ParallelRoutePageContent from "@/components/features/parallel-routing/parallel-route-content"; 3 | 4 | // Force the page to dynamic rendering via searchParams access 5 | // Using pages searchParams prop will opt the whole route into dynamic rendering 6 | // https://nextjs.org/docs/app/building-your-application/rendering/server-components#switching-to-dynamic-rendering 7 | // This is just for the demo, so you can refresh the page and see how 8 | // page streaming works. 9 | const FourthPage = async ( 10 | props: { 11 | searchParams: Promise<{ value: string }>; 12 | } 13 | ) => { 14 | const searchParams = await props.searchParams; 15 | await fakeAPI(8000, searchParams.value); 16 | return ; 17 | }; 18 | 19 | export default FourthPage; 20 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@secondPage/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ParallelRouteError from "@/components/features/parallel-routing/parallel-route-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset, error }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; 11 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@secondPage/loading.tsx: -------------------------------------------------------------------------------- 1 | import ParallelRouteLoader from "@/components/features/parallel-routing/parallel-route-loader"; 2 | 3 | const Loading = () => { 4 | return ; 5 | }; 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@secondPage/page.tsx: -------------------------------------------------------------------------------- 1 | import { fakeAPI } from "@/actions/parallel-route"; 2 | import ParallelRoutePageContent from "@/components/features/parallel-routing/parallel-route-content"; 3 | 4 | // Force the page to dynamic rendering via searchParams access 5 | // Using pages searchParams prop will opt the whole route into dynamic rendering 6 | // https://nextjs.org/docs/app/building-your-application/rendering/server-components#switching-to-dynamic-rendering 7 | // This is just for the demo, so you can refresh the page and see how 8 | // page streaming works. 9 | const SecondPage = async ( 10 | props: { 11 | searchParams: Promise<{ value: string }>; 12 | } 13 | ) => { 14 | const searchParams = await props.searchParams; 15 | await fakeAPI(4000, searchParams.value); 16 | return ; 17 | }; 18 | 19 | export default SecondPage; 20 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@thirdPage/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ParallelRouteError from "@/components/features/parallel-routing/parallel-route-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset, error }: ErrorPageProps) => { 7 | return ; 8 | }; 9 | 10 | export default ErrorHandler; 11 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@thirdPage/loading.tsx: -------------------------------------------------------------------------------- 1 | import ParallelRouteLoader from "@/components/features/parallel-routing/parallel-route-loader"; 2 | 3 | const Loading = () => { 4 | return ; 5 | }; 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/@thirdPage/page.tsx: -------------------------------------------------------------------------------- 1 | import { fakeAPI } from "@/actions/parallel-route"; 2 | import ParallelRoutePageContent from "@/components/features/parallel-routing/parallel-route-content"; 3 | 4 | // Force the page to dynamic rendering via searchParams access 5 | // Using pages searchParams prop will opt the whole route into dynamic rendering 6 | // https://nextjs.org/docs/app/building-your-application/rendering/server-components#switching-to-dynamic-rendering 7 | // This is just for the demo, so you can refresh the page and see how 8 | // page streaming works. 9 | const ThirdPage = async ( 10 | props: { 11 | searchParams: Promise<{ value: string }>; 12 | } 13 | ) => { 14 | const searchParams = await props.searchParams; 15 | await fakeAPI(6000, searchParams.value); 16 | return ; 17 | }; 18 | 19 | export default ThirdPage; 20 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/layout.tsx: -------------------------------------------------------------------------------- 1 | import FeatureImplementationExplanationLayout from "@/components/layouts/feature-implementation-explanation"; 2 | import { PropsWithChildren, ReactNode } from "react"; 3 | 4 | interface ParallelRoutingLayoutProps extends PropsWithChildren { 5 | firstPage: ReactNode; 6 | secondPage: ReactNode; 7 | thirdPage: ReactNode; 8 | fourthPage: ReactNode; 9 | explanation: ReactNode; 10 | } 11 | 12 | const ParallelRoutingLayout = (props: ParallelRoutingLayoutProps) => { 13 | const { 14 | firstPage, 15 | secondPage, 16 | thirdPage, 17 | fourthPage, 18 | children, 19 | explanation, 20 | } = props; 21 | return ( 22 | 23 |
24 |
{children}
25 |
26 |
27 | {firstPage} 28 |
29 |
30 | {secondPage} 31 |
32 |
33 | {thirdPage} 34 |
35 |
36 | {fourthPage} 37 |
38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export default ParallelRoutingLayout; 45 | -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/loading.tsx: -------------------------------------------------------------------------------- 1 | import ParallelRouteChildrenSkeleton from "@/components/parallel-route-children-skeleton"; 2 | 3 | // This loading will suspense the children slot 4 | export default ParallelRouteChildrenSkeleton; -------------------------------------------------------------------------------- /src/app/features/(implementations)/parallel-routing/page.tsx: -------------------------------------------------------------------------------- 1 | import ParallelRoutingImplementation from "@/components/features/parallel-routing/implementation"; 2 | import FeatureImplementationTemplate from "@/components/templates/feature-implementation"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "NextJs parallel routing", 7 | description: "Demonstration of parallel routing concept in NextJs app router", 8 | openGraph: { 9 | images: ["/api/og?title=Parallel Routing"], 10 | }, 11 | }; 12 | 13 | const ParallelRoutingImplementationPage = () => { 14 | return ( 15 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default ParallelRoutingImplementationPage; 25 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PageError from "@/components/shared/page-error"; 4 | import { ErrorPageProps } from "@/lib/types/misc"; 5 | 6 | const ErrorHandler = ({ reset }: ErrorPageProps) => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default ErrorHandler; 17 | -------------------------------------------------------------------------------- /src/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GeistSans } from "geist/font/sans"; 2 | import "../styles/globals.css"; 3 | import SiteHeader from "@/components/site-header"; 4 | import { ThemeProvider } from "@/components/providers/theme-provider"; 5 | import SiteFooter from "@/components/site-footer"; 6 | import { ReactNode } from "react"; 7 | import { SpeedInsights } from "@vercel/speed-insights/next"; 8 | import { AudioPlayerInitProvider } from "@/components/providers/audio-player-init-provider"; 9 | import type { Metadata } from "next"; 10 | import { AudioPlayerWrapper } from "@/components/features/music-streaming/implementation/audio-player-wrapper"; 11 | 12 | export const metadata: Metadata = { 13 | title: { 14 | default: "Next.js App Router", 15 | template: "%s | Next.js App Router", 16 | }, 17 | metadataBase: new URL("https://next-app-gamma-lovat.vercel.app"), 18 | openGraph: { 19 | title: "Everyday features with Next.js App Router", 20 | description: 21 | "A demo of every day features and concepts using NextJs app router", 22 | images: ["/api/og"], 23 | }, 24 | }; 25 | 26 | export default function RootLayout({ 27 | children, 28 | }: Readonly<{ 29 | children: ReactNode; 30 | }>) { 31 | return ( 32 | 33 | 34 | 35 | 41 | 42 |
43 | 44 |
{children}
45 | 46 | 47 |
48 |
49 |
50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; 4 | -------------------------------------------------------------------------------- /src/app/product/[productId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import PageLoader from "@/components/loaders/page-loader"; 2 | 3 | export default PageLoader; -------------------------------------------------------------------------------- /src/app/product/[productId]/page.tsx: -------------------------------------------------------------------------------- 1 | import AppBreadCrumb from "@/components/app-breadcrumb"; 2 | import ProductDetails from "@/components/product/product-details"; 3 | import BackNavigation from "@/components/shared/back-navigation"; 4 | 5 | const ProductDetailPage = () => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default ProductDetailPage; 22 | -------------------------------------------------------------------------------- /src/components/features/infinite-scrolling/with-server-action/implementation/index.tsx: -------------------------------------------------------------------------------- 1 | import WSALoadMore from "./wsa-load-more"; 2 | import ProductList from "@/components/product/product-list"; 3 | import { getProducts } from "@/services/product-service"; 4 | import ParamUpdateInput from "../../../../shared/param-update-input"; 5 | 6 | const PAGE_SIZE = 12; 7 | 8 | interface ISWithServerActionImplementationProps { 9 | searchKey: string; 10 | } 11 | 12 | const ISWithServerActionImplementation = async ({ 13 | searchKey, 14 | }: ISWithServerActionImplementationProps) => { 15 | const productsResponse = await getProducts(undefined, PAGE_SIZE, searchKey); 16 | 17 | if (!productsResponse) { 18 | return
No product to display
; 19 | } 20 | 21 | const initialOffset = 22 | 2 * PAGE_SIZE < productsResponse.total ? 2 * PAGE_SIZE : null; 23 | 24 | // fetch products and return Product list and next offset 25 | const getProductListNodes = async (offset: number) => { 26 | "use server"; 27 | try { 28 | const response = await getProducts(offset, PAGE_SIZE, searchKey); 29 | if (!response) { 30 | return null; 31 | } 32 | // here you could make use of next and previous value from your api to calculate nextOffset 33 | const nextOffset = 34 | offset + PAGE_SIZE < response.total ? offset + PAGE_SIZE : null; 35 | return [ 36 | , 37 | nextOffset, 38 | ] as const; 39 | } catch (_) { 40 | return null; 41 | } 42 | }; 43 | 44 | return ( 45 | <> 46 |
47 | 48 |
49 | {productsResponse.products.length ? ( 50 | 55 | 56 | 57 | ) : ( 58 |

59 | No products for{" "} 60 | {searchKey} 61 |

62 | )} 63 | 64 | ); 65 | }; 66 | 67 | export default ISWithServerActionImplementation; 68 | -------------------------------------------------------------------------------- /src/components/features/infinite-scrolling/with-server-action/implementation/wsa-load-more.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { InView } from "react-intersection-observer"; 3 | import React, { useRef, useState, useTransition } from "react"; 4 | import ContentLoader from "@/components/loaders/content-loader"; 5 | 6 | interface WSALoadMoreProps extends React.PropsWithChildren { 7 | getProductListNodes: ( 8 | offset: number 9 | ) => Promise; 10 | initialOffset: number | null; 11 | } 12 | 13 | const WSALoadMore = ({ 14 | children, 15 | getProductListNodes, 16 | initialOffset, 17 | }: WSALoadMoreProps) => { 18 | const [isPending, startTransition] = useTransition(); 19 | const offsetRef = useRef(initialOffset); 20 | const [productListNodes, setProductListNodes] = useState( 21 | [] 22 | ); 23 | 24 | // invoke server action when our target node is in view 25 | const updateProductListNodes = () => { 26 | if (!offsetRef.current) { 27 | return; 28 | } 29 | startTransition(async () => { 30 | const response = await getProductListNodes(offsetRef.current as number); 31 | if (response) { 32 | const [listNode, nextOffset] = response; 33 | offsetRef.current = nextOffset; 34 | setProductListNodes((previousNodeList) => [ 35 | ...previousNodeList, 36 | listNode, 37 | ]); 38 | } 39 | }); 40 | }; 41 | 42 | return ( 43 | <> 44 |
45 | {children} 46 | {productListNodes} 47 |
48 | inView && updateProductListNodes()} 51 | > 52 |
53 |
54 | {isPending && ( 55 |
56 | 57 |
58 | )} 59 | 60 | ); 61 | }; 62 | 63 | export default WSALoadMore; 64 | -------------------------------------------------------------------------------- /src/components/features/infinite-scrolling/with-use-swr/explanation/index.tsx: -------------------------------------------------------------------------------- 1 | import TextHighlight from "@/components/text-highlight.tsx"; 2 | 3 | const ISWithUseSWRExplanation = () => { 4 | return ( 5 |
6 |

7 | In this implementation we are making use of the{" "} 8 | {" "} 12 | hook from{" "} 13 | , a light 14 | weight client side data fetching library. 15 |

16 |

17 | All the data is fetched on the client in the{" "} 18 | component. 19 |

20 |

21 | {" "} 25 | accepts two params. The first param is a function that receives the page 26 | index starting from and the data of the 27 | previous fetch call. The function needs to return a url based on those 28 | two info which will then be passed to the the second param which is also 29 | a function that actually fetches your data. If the function{" "} 30 | returns{" "} 31 | then, no data will be fetched. 32 |

33 |

34 | The response of all those data call can be accessed as an array of those 35 | responses in the value returned by the{" "} 36 | hook. You also get the{" "} 37 | and{" "} 38 | status with which you can control 39 | the UI by showing loaders whenever necessary. 40 |

41 |

42 | We make use of{" "} 43 | {" "} 47 | to observe the end of product list upon which we increment the page size 48 | for next fetch. 49 |

50 |

51 | handles the caching of our client side data 52 | fetch. If you{" "} 53 | {" "} 57 | and return to this page, you will see all the previously loaded products 58 | instantly. 59 |

60 |

61 | You can learn more about infinite loading with{" "} 62 | via given links. 63 |

64 |
65 | ); 66 | }; 67 | 68 | export default ISWithUseSWRExplanation; 69 | -------------------------------------------------------------------------------- /src/components/features/infinite-scrolling/with-use-swr/implementation/index.tsx: -------------------------------------------------------------------------------- 1 | import ParamUpdateInput from "@/components/shared/param-update-input"; 2 | import WithUseSWRLoadMore from "./wswr-load-more"; 3 | import { Suspense } from "react"; 4 | 5 | // need to wrap any component that uses useSearchParams hook in Suspense if not 6 | // it will throw build error more here https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout 7 | const ISWithUseSWRImplementation = async () => { 8 | return ( 9 |
10 |
11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default ISWithUseSWRImplementation; 23 | -------------------------------------------------------------------------------- /src/components/features/intercepting-routes/implementation/indext.tsx: -------------------------------------------------------------------------------- 1 | import ProductCard from "@/components/product/product-card"; 2 | import { getProducts } from "@/services/product-service"; 3 | import Link from "next/link"; 4 | const PAGE_SIZE = 12; 5 | 6 | const InterceptingRoutesImplementation = async () => { 7 | const productsResponse = await getProducts(undefined, PAGE_SIZE); 8 | 9 | if (productsResponse === null) { 10 | return
Error fetching products
; 11 | } 12 | 13 | return ( 14 |
15 | {productsResponse.products.length ? ( 16 |
17 | {productsResponse.products.map((product) => ( 18 | 23 | 24 | 25 | ))} 26 |
27 | ) : ( 28 |

No products

29 | )} 30 |
31 | ); 32 | }; 33 | 34 | export default InterceptingRoutesImplementation; 35 | -------------------------------------------------------------------------------- /src/components/features/intercepting-routes/product-detail-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Dialog, DialogContent } from "@/components/ui/dialog"; 4 | import { useRouter } from "next/navigation"; 5 | import { ReactNode } from "react"; 6 | 7 | interface ProductDetailModalProps { 8 | children: ReactNode; 9 | } 10 | 11 | const ProductDetailModal = ({ children }: ProductDetailModalProps) => { 12 | const router = useRouter(); 13 | 14 | return ( 15 | !open && router.back()}> 16 | 17 |
18 | 25 | {children} 26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default ProductDetailModal; 33 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/explanation/index.tsx: -------------------------------------------------------------------------------- 1 | import TextHighlight from "@/components/text-highlight.tsx"; 2 | 3 | const MusicStreamingExplanation = () => { 4 | return ( 5 |
6 |

7 | This feature makes use of{" "} 8 | {" "} 12 | library built on top of{" "} 13 | , a 14 | library that makes working with audio in the browser easier. 15 |

16 |

17 | The library provides two hooks to work with audios in react.{" "} 18 | and{" "} 19 | . The implementation makes 20 | use of the latter hook since we need access to the same audio status, 21 | controls, and functions in different components i.e. the audio player 22 | component at the bottom and the audio cards. 23 |

24 |

25 | All the audio controls and functions are provided by the audio hook and 26 | you just have to play with it to get the hang of it. 27 |

28 |

29 | As for the audio files, for now they are static contents and are placed 30 | directly inside the folder of the repo. 31 | This is not an ideal way to store and read audio files in real 32 | applications. So in real apps, you would store it in some CDN server and 33 | fetch from there and render it for the users to listen to the songs. 34 |

35 |
36 | ); 37 | }; 38 | 39 | export default MusicStreamingExplanation; 40 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/audio-card-wrapper/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | 5 | export const AudioCardWrapper = dynamic(() => import("../audio-card"), { 6 | ssr: false, 7 | }); 8 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/audio-card/play-animation.tsx: -------------------------------------------------------------------------------- 1 | import styles from "@/styles/play-animation.module.css"; 2 | import clsx from "clsx"; 3 | 4 | interface PropsPlayAnimationProps { 5 | className?: string; 6 | } 7 | 8 | const PlayAnimation = ({ className }: PropsPlayAnimationProps) => { 9 | return ( 10 |
11 | A 12 | B 13 | c 14 | D 15 | E 16 | F 17 | G 18 | H 19 | A 20 | B 21 | c 22 | D 23 | E 24 | F 25 | G 26 | H 27 | B 28 | c 29 | H 30 | A 31 | B 32 | c 33 |
34 | ); 35 | }; 36 | 37 | export default PlayAnimation; 38 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/audio-player-wrapper/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | 5 | export const AudioPlayerWrapper = dynamic( 6 | () => 7 | import("@/components/features/music-streaming/implementation/audio-player"), 8 | { 9 | ssr: false, 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/audio-player/audio-loop/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Tooltip, 4 | TooltipContent, 5 | TooltipProvider, 6 | TooltipTrigger, 7 | } from "@/components/ui/tooltip"; 8 | import clsx from "clsx"; 9 | import React from "react"; 10 | import { TfiLoop } from "react-icons/tfi"; 11 | 12 | interface AudioLoopProps { 13 | isLooping: boolean; 14 | onToggle: () => void; 15 | } 16 | 17 | const AudioLoop = ({ isLooping, onToggle }: AudioLoopProps) => { 18 | return ( 19 | 20 | 21 | 22 | 32 | 33 | 34 | {isLooping ? "Looping" : "Loop current song"} 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default AudioLoop; 42 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/audio-player/audio-shuffle/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Tooltip, 4 | TooltipContent, 5 | TooltipProvider, 6 | TooltipTrigger, 7 | } from "@/components/ui/tooltip"; 8 | import clsx from "clsx"; 9 | import React from "react"; 10 | import { TfiControlShuffle } from "react-icons/tfi"; 11 | 12 | interface AudioShuffleProps { 13 | onShuffle: () => void; 14 | shuffle: boolean; 15 | } 16 | 17 | const AudioShuffle = ({ onShuffle, shuffle }: AudioShuffleProps) => { 18 | return ( 19 | 20 | 21 | 22 | 32 | 33 | Shuffle 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default AudioShuffle; 40 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/audio-player/audio-slider/index.tsx: -------------------------------------------------------------------------------- 1 | import { Slider } from "@/components/ui/slider"; 2 | import { formatDuration } from "@/lib/utils/misc"; 3 | import React, { useEffect, useRef, useState } from "react"; 4 | import { useGlobalAudioPlayer } from "react-use-audio-player"; 5 | 6 | const AudioSlider = () => { 7 | const [position, setPosition] = useState(0); 8 | const { getPosition, duration, seek, playing } = useGlobalAudioPlayer(); 9 | const frameRef = useRef(undefined); 10 | 11 | useEffect(() => { 12 | getPosition && setPosition(getPosition()); 13 | }, [getPosition]); 14 | 15 | useEffect(() => { 16 | const animate = () => { 17 | setPosition(getPosition()); 18 | frameRef.current = requestAnimationFrame(animate); 19 | }; 20 | 21 | frameRef.current = window.requestAnimationFrame(animate); 22 | 23 | return () => { 24 | if (frameRef.current) { 25 | cancelAnimationFrame(frameRef.current); 26 | } 27 | }; 28 | }, [getPosition, duration]); 29 | 30 | return ( 31 | <> 32 | 33 | {formatDuration(position, position > 3600 ? "hh:mm:ss" : "mm:ss")} 34 | 35 | { 41 | setPosition(values); 42 | seek(values); 43 | }} 44 | className="w-[80%] h-[4px]" 45 | /> 46 | 47 | {formatDuration(duration, duration > 3600 ? "hh:mm:ss" : "mm:ss")} 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default AudioSlider; 54 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/audio-player/audio-sound/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Slider } from "@/components/ui/slider"; 3 | import { 4 | Tooltip, 5 | TooltipContent, 6 | TooltipProvider, 7 | TooltipTrigger, 8 | } from "@/components/ui/tooltip"; 9 | import { cn } from "@/lib/utils"; 10 | import React from "react"; 11 | import { MdVolumeOff, MdVolumeUp } from "react-icons/md"; 12 | 13 | interface AudioSoundProps { 14 | toggleMute: () => void; 15 | muted: boolean; 16 | audioVolume: number; 17 | onSliderValueChange: (values: number[]) => void; 18 | sliderClass?: string; 19 | } 20 | 21 | const AudioSound = ({ 22 | toggleMute, 23 | muted, 24 | audioVolume, 25 | onSliderValueChange, 26 | sliderClass, 27 | }: AudioSoundProps) => { 28 | return ( 29 | <> 30 | 31 | 32 | 33 | 40 | 41 | {muted ? "Unmute" : "Mute"} 42 | 43 | 44 | 51 | 52 | ); 53 | }; 54 | 55 | export default AudioSound; 56 | -------------------------------------------------------------------------------- /src/components/features/music-streaming/implementation/index.tsx: -------------------------------------------------------------------------------- 1 | import { SONG_LIST } from "@/lib/constants/song-list"; 2 | import { AudioCardWrapper } from "./audio-card-wrapper"; 3 | 4 | const MusicStreamingImplementation = () => { 5 | return ( 6 |
7 | {SONG_LIST.map((song) => ( 8 | 9 | ))} 10 |
11 | ); 12 | }; 13 | 14 | export default MusicStreamingImplementation; 15 | -------------------------------------------------------------------------------- /src/components/features/pagination/client-side/explanation/index.tsx: -------------------------------------------------------------------------------- 1 | import TextHighlight from "@/components/text-highlight.tsx"; 2 | 3 | const ClientSidePaginationExplanation = () => { 4 | return ( 5 |
6 |

7 | For this implementation, we have used{" "} 8 | 9 | hook to fetch data on the client side. 10 |

11 |

12 | For the table, we have used a reusable component{" "} 13 | which contains our main 14 | table component created with{" "} 15 | library. 16 |

17 |

18 | For the and{" "} 19 | value, we have used query params instead 20 | of react states. As you change the page or search input value, the query 21 | params are updated accordingly and is used to fetch the data as 22 | necessary. 23 |

24 |

25 | Remember that if we update the params the{" "} 26 | {" "} 30 | way, it will cause a server request of the page. But we don't want 31 | that since the query params are used only inside the client components. 32 | So we need a way that the updating query params only cause the re-render 33 | of the client components that makes use of those values. It is quite 34 | hacky to get that functionality currently so, the implementation makes 35 | use of , 36 | a query param update library for . It 37 | supports shallow updates and can be used for both{" "} 38 | and router. 39 |

40 |

41 | That's all it takes to get one of the most common features for 42 | content display in the web world. 43 |

44 |
45 | ); 46 | }; 47 | 48 | export default ClientSidePaginationExplanation; 49 | -------------------------------------------------------------------------------- /src/components/features/pagination/client-side/implementation/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useSWR from "swr"; 4 | import ParamUpdateInput from "@/components/shared/param-update-input"; 5 | import { fetchHelper } from "@/helpers/fetch-helper"; 6 | import { ProductsResponse } from "@/lib/types/product"; 7 | import { useQueryState } from "nuqs"; 8 | import ContentLoader from "@/components/loaders/content-loader"; 9 | import PaginationDataTable from "../../pagination-data-table"; 10 | import PaginationNav from "../../pagination-nav"; 11 | 12 | const resetKeysOnSearch = ["page"]; 13 | 14 | const PAGE_SIZE = 6; 15 | 16 | // you can abstract away the infinite loading logic in an effect called useInfiniteLoading() 17 | // where both the fetFetchURL and fetcher functions will be defined there 18 | // I have kept it here so that you don't have to switch context to get the concept 19 | const getFetchURL = (offset: number, searchKey: string | null) => { 20 | const url = `https://dummyjson.com/products${ 21 | searchKey?.trim() ? `/search?q=${searchKey}&` : "?" 22 | }limit=${PAGE_SIZE}&skip=${offset}`; 23 | return url; // SWR key 24 | }; 25 | 26 | const fetcher = async (url: string): Promise => { 27 | return fetchHelper({ 28 | url, 29 | method: "GET", 30 | }); 31 | }; 32 | 33 | const ClientSidePaginationImplementation = () => { 34 | const [searchQuery, _] = useQueryState("search"); 35 | const [page, __] = useQueryState("page"); 36 | // this logic will depend on your backend api 37 | const getOffsetValue = () => { 38 | if (page && Number(page)) { 39 | const toFixedValue = Number(Number(page).toFixed(0)); 40 | return toFixedValue <= 1 ? 0 : (toFixedValue - 1) * PAGE_SIZE; 41 | } else { 42 | return 0; 43 | } 44 | }; 45 | 46 | const { 47 | data: productsResponse, 48 | error, 49 | isLoading, 50 | } = useSWR(getFetchURL(getOffsetValue(), searchQuery), fetcher); 51 | 52 | if (error) { 53 | return ( 54 |

55 | There was an error fetching data 56 |

57 | ); 58 | } 59 | 60 | return ( 61 |
62 |
63 | 64 |
65 | {/* Hiding input field on every search with loader give bad UX so the 66 | loader is shown here */} 67 | {isLoading && ( 68 |
69 | 70 |
71 | )} 72 | 73 | {!isLoading && 74 | (productsResponse?.products.length ? ( 75 | <> 76 | 77 |
78 | 83 |
84 | 85 | ) : ( 86 |

87 | There are no products to display 88 |

89 | ))} 90 |
91 | ); 92 | }; 93 | 94 | export default ClientSidePaginationImplementation; 95 | -------------------------------------------------------------------------------- /src/components/features/pagination/pagination-table-header.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tooltip, 3 | TooltipContent, 4 | TooltipProvider, 5 | TooltipTrigger, 6 | } from "@/components/ui/tooltip"; 7 | import { Column } from "@tanstack/react-table"; 8 | import { RxCaretSort } from "react-icons/rx"; 9 | 10 | interface PaginationTableHeaderProps { 11 | headerValue: string; 12 | tooltipValue: string; 13 | tableRef: React.RefObject; 14 | column?: Column; 15 | } 16 | 17 | const PaginationTableHeader = ({ 18 | headerValue, 19 | tooltipValue, 20 | tableRef, 21 | column, 22 | }: PaginationTableHeaderProps) => { 23 | return ( 24 | 25 | 26 | 27 | {column ? ( 28 | 37 | ) : ( 38 | 39 | {headerValue} 40 | 41 | )} 42 | 43 | 44 |

{tooltipValue}

45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default PaginationTableHeader; 52 | -------------------------------------------------------------------------------- /src/components/features/pagination/server-side/explanation/index.tsx: -------------------------------------------------------------------------------- 1 | import TextHighlight from "@/components/text-highlight.tsx"; 2 | 3 | const ServerSidePaginationExplanation = () => { 4 | return ( 5 |
6 |

7 | One of the most common everyday features is pagination. In this 8 | implementation, the data fetching is always done on the server inside{" "} 9 | and the data 10 | is passed to the client component{" "} 11 | . 12 |

13 |

14 | For the table, we have used{" "} 15 | {" "} 19 | library to handle client side sorting and column visibility toggle. I 20 | highly recommend the library for tabular data since it is a breeze to 21 | set up and the features are very intuitive. 22 |

23 |

24 | While searching the product, we update the query param with key{" "} 25 | which causes server request and our page 26 | gets the the value of the query which is used to fetch data on the 27 | server again. So even our search results are rendered on the server as 28 | well. 29 |

30 |

31 | Similarly, while clicking on the page number or the{" "} 32 | and {" "} 33 | navigation buttons, we make use of query param with the key{" "} 34 | causing server request where the data for 35 | that page is fetched on the server. 36 |

37 |

38 | Note that the query value is reset 39 | whenever the user makes new search which is the only reasonable thing 40 | since we want every search results pagination to start from page 1. 41 |

42 |

43 | Currently doesn't seem to support 44 | shallow query param updates meaning every query param update via{" "} 45 | {" "} 49 | will cause server request which means all your server components for the 50 | page will re-render. You would want this for some cases like for the 51 | current feature, but you may not want the behavior for other cases. It 52 | is quite hacky to get that behavior currently so I am using a library 53 | called {" "} 54 | which supports shallow updates. It supports both page and app router. 55 |

56 |
57 | ); 58 | }; 59 | 60 | export default ServerSidePaginationExplanation; 61 | -------------------------------------------------------------------------------- /src/components/features/pagination/server-side/implementation/index.tsx: -------------------------------------------------------------------------------- 1 | import { getProducts } from "@/services/product-service"; 2 | import ParamUpdateInput from "@/components/shared/param-update-input"; 3 | import PaginationDataTable from "../../pagination-data-table"; 4 | import PaginationNav from "../../pagination-nav"; 5 | 6 | const PAGE_SIZE = 6; 7 | 8 | interface ServerSidePaginationImplementationProps { 9 | page: string; 10 | search: string; 11 | } 12 | 13 | const ServerSidePaginationImplementation = async ({ 14 | page, 15 | search, 16 | }: ServerSidePaginationImplementationProps) => { 17 | // this logic will depend on your backend api 18 | const getOffsetValue = () => { 19 | if (page && Number(page)) { 20 | const toFixedValue = Number(Number(page).toFixed(0)); 21 | return toFixedValue <= 1 ? 0 : (toFixedValue - 1) * PAGE_SIZE; 22 | } else { 23 | return 0; 24 | } 25 | }; 26 | 27 | const productsResponse = await getProducts( 28 | getOffsetValue(), 29 | PAGE_SIZE, 30 | search 31 | ); 32 | 33 | if (!productsResponse) { 34 | return ( 35 |

36 | There was an error fetching data 37 |

38 | ); 39 | } 40 | 41 | const resetKeysOnSearch = ["page"]; 42 | 43 | return ( 44 |
45 |
46 | 47 |
48 | {productsResponse.products.length ? ( 49 | <> 50 | 51 |
52 | 57 |
58 | 59 | ) : ( 60 |

61 | There are no products to display 62 |

63 | )} 64 |
65 | ); 66 | }; 67 | 68 | export default ServerSidePaginationImplementation; 69 | -------------------------------------------------------------------------------- /src/components/features/parallel-routing/explanation/index.tsx: -------------------------------------------------------------------------------- 1 | import TextHighlight from "@/components/text-highlight.tsx"; 2 | import ParallelRoutingFolderStructure from "./parallel-routing-folder-structure"; 3 | 4 | const ParallelRoutingExplanation = () => { 5 | return ( 6 |
7 |

8 | {" "} 12 | is a opt in feature in NextJs where you can render one or more pages 13 | within the same layout simultaneously or conditionally. It is 14 | recommended for highly dynamic sections of an application like 15 | dashboards and feeds on social sites. 16 |

17 |

18 | Go through the folder structure for the current implementation below for 19 | a quick drive on how to define parallel routes. 20 |

21 | 22 |

23 | As you can see, parallel routes are denoted by the named{" "} 24 | 28 | . Slots are defined with the convention 29 |

30 |

31 | Currently we are at the route segment{" "} 32 | , and we have four parallel 33 | routes under that namely ,{" "} 34 | ,{" "} 35 | , and{" "} 36 | which corresponds to{" "} 37 | , ,{" "} 38 | , and page 39 | titles respectively on the implementation section. 40 |

41 |

42 | Now all those named slots are passed as props to the{" "} 43 | parent{" "} 44 | file. Inside that layout file, you 45 | can choose to render those slots any way you want and can also position 46 | them any way you want. The file 47 | directly under folder will be 48 | passed as prop to the layout as well. 49 | That's why is an implicit slot 50 | that NextJs automatically passes to your layout. 51 |

52 |

53 | Each parallel routes can have both {" "} 54 | and files which enables page 55 | streaming and granular error handling as you can see in the demo. You 56 | can play around it get the idea. 57 |

58 |
59 | ); 60 | }; 61 | 62 | export default ParallelRoutingExplanation; 63 | -------------------------------------------------------------------------------- /src/components/features/parallel-routing/implementation/index.tsx: -------------------------------------------------------------------------------- 1 | import TextHighlight from "@/components/text-highlight.tsx"; 2 | 3 | const ParallelRoutingImplementation = () => { 4 | return ( 5 |

6 | Refresh the page to see how the each parallel pages/routes can have it's own 7 | loading states that enables{" "} 8 | {" "} 12 | with{" "} 13 | {" "} 17 | files. Click on the red outline button to throw runtime error and see how 18 | granular you can go with{" "} 19 | {" "} 23 | on the parallel page/route level as well with{" "} 24 | {" "} 28 | files. 29 |

30 | ); 31 | }; 32 | 33 | export default ParallelRoutingImplementation; 34 | -------------------------------------------------------------------------------- /src/components/features/parallel-routing/parallel-route-content.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; 3 | import ThrowError from "./throw-error"; 4 | import TextHighlight from "@/components/text-highlight.tsx"; 5 | 6 | interface ParallelRoutePageContentProps { 7 | pageName: string; 8 | timeToLoad: number; 9 | } 10 | 11 | const ParallelRoutePageContent = ({ 12 | pageName, 13 | timeToLoad, 14 | }: ParallelRoutePageContentProps) => { 15 | return ( 16 |
17 |

18 | page.tsx 19 |

20 |

{pageName}

21 |

22 | Takes{" "} 23 | 24 | {timeToLoad} 25 | 26 | s to load. 27 |

28 |
29 | 30 | 31 | 34 | 35 | 36 |
Interactive
37 |

38 | This is just to demo that the already rendered page can be 39 | interactive while other pages are still loading. Basically, we are 40 | able to stream pages just like components with{" "} 41 | . 45 |

46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default ParallelRoutePageContent; 57 | -------------------------------------------------------------------------------- /src/components/features/parallel-routing/parallel-route-error.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { ErrorPageProps } from "@/lib/types/misc"; 3 | 4 | type ParallelRouteErrorProps = { 5 | reset: () => void; 6 | } & Pick; 7 | 8 | const ParallelRouteError = ({ reset, error }: ParallelRouteErrorProps) => { 9 | return ( 10 |
11 |

12 | error.tsx 13 |

14 |

Oops! Sth went wrong

15 |

Error: {error?.message}

16 | 19 |
20 | ); 21 | }; 22 | 23 | export default ParallelRouteError; 24 | -------------------------------------------------------------------------------- /src/components/features/parallel-routing/parallel-route-loader.tsx: -------------------------------------------------------------------------------- 1 | interface ParallelRouteLoaderProps { 2 | pageName: string; 3 | } 4 | 5 | const ParallelRouteLoader = ({ pageName }: ParallelRouteLoaderProps) => { 6 | return ( 7 |
8 |

9 | Loading page{" "} 10 | {pageName} ... 11 |

12 |
13 | ); 14 | }; 15 | 16 | export default ParallelRouteLoader; 17 | -------------------------------------------------------------------------------- /src/components/features/parallel-routing/throw-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { IoBugSharp } from "react-icons/io5"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Tooltip, 7 | TooltipContent, 8 | TooltipProvider, 9 | TooltipTrigger, 10 | } from "@/components/ui/tooltip"; 11 | import { useState } from "react"; 12 | 13 | const ThrowError = () => { 14 | const [clicked, setClicked] = useState(false); 15 | 16 | if (clicked) { 17 | throw new Error("Simulated error"); 18 | } 19 | 20 | return ( 21 | 22 | 23 | 24 | 32 | 33 | Click to trigger error 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default ThrowError; 40 | -------------------------------------------------------------------------------- /src/components/hero-feature-separator/index.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from "../ui/separator"; 2 | 3 | const HeroFeatureSeparator = () => { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default HeroFeatureSeparator; 21 | -------------------------------------------------------------------------------- /src/components/layouts/feature-implementation-explanation/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import MobileFeatureLayoutHeader from "@/components/mobile-feature-layout-header"; 3 | import { Separator } from "@/components/ui/separator"; 4 | import React, { ReactNode } from "react"; 5 | 6 | interface FeatureImplementationExplanationLayoutProps 7 | extends React.PropsWithChildren { 8 | explanation: ReactNode; 9 | } 10 | 11 | const FeatureImplementationExplanationLayout = ({ 12 | children, 13 | explanation, 14 | }: FeatureImplementationExplanationLayoutProps) => { 15 | return ( 16 |
17 | 18 | {/* desktop left bar */} 19 |
20 |
21 | {explanation} 22 |
23 |
24 | 28 | {/* Feature implementation */} 29 |
{children}
30 |
31 | ); 32 | }; 33 | 34 | export default FeatureImplementationExplanationLayout; 35 | -------------------------------------------------------------------------------- /src/components/libraries-used/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LIBRARIES_USED } from "@/lib/constants/misc"; 4 | import { 5 | HoverCard, 6 | HoverCardContent, 7 | HoverCardTrigger, 8 | } from "../ui/hover-card"; 9 | import { Button } from "../ui/button"; 10 | import { PiArrowLineUpRightThin } from "react-icons/pi"; 11 | 12 | const LibrariesUsed = () => { 13 | return ( 14 |
15 | {LIBRARIES_USED.map((library) => ( 16 | 17 | 18 | 24 | 25 | 26 |
27 |
28 |
{library.name}
29 |

{library.description}

30 | 35 | Visit{" "} 36 | 37 | 38 |
39 |
40 |
41 |
42 | ))} 43 |
44 | ); 45 | }; 46 | 47 | export default LibrariesUsed; 48 | -------------------------------------------------------------------------------- /src/components/loaders/action-loader.tsx: -------------------------------------------------------------------------------- 1 | import styles from "../../styles/action-loader.module.css"; 2 | 3 | const ActionLoader = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default ActionLoader; 23 | -------------------------------------------------------------------------------- /src/components/loaders/content-loader.tsx: -------------------------------------------------------------------------------- 1 | import { ClassNameProp } from "@/lib/types/misc"; 2 | import styles from "../../styles/content-loader.module.css"; 3 | import clsx from "clsx"; 4 | 5 | const ContentLoader = ({ className }: ClassNameProp) => { 6 | return ( 7 |
8 | Loading.. 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default ContentLoader; 15 | -------------------------------------------------------------------------------- /src/components/loaders/page-loader.tsx: -------------------------------------------------------------------------------- 1 | import ContentLoader from "./content-loader"; 2 | 3 | const PageLoader = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default PageLoader; 12 | -------------------------------------------------------------------------------- /src/components/loaders/progress-loader.tsx: -------------------------------------------------------------------------------- 1 | import styles from "../../styles/progress-loader.module.css"; 2 | 3 | const ProgressLoader = () => { 4 | return
; 5 | }; 6 | 7 | export default ProgressLoader; 8 | -------------------------------------------------------------------------------- /src/components/logo/index.tsx: -------------------------------------------------------------------------------- 1 | import { GiAbstract062 } from "react-icons/gi"; 2 | import { Separator } from "../ui/separator"; 3 | 4 | const SiteLogo = () => { 5 | return ( 6 | <> 7 | 8 | 9 | NEXT 10 | 11 | .js 12 | 13 | 14 | 15 | APP 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default SiteLogo; 22 | -------------------------------------------------------------------------------- /src/components/mobile-feature-layout-header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Sheet, SheetContent, SheetTrigger } from "../ui/sheet"; 2 | import { ReactNode } from "react"; 3 | import AppBreadCrumb from "../app-breadcrumb"; 4 | 5 | interface MobileFeatureLayoutHeaderProps { 6 | explanation: ReactNode; 7 | } 8 | 9 | const MobileFeatureLayoutHeader = ({ 10 | explanation, 11 | }: MobileFeatureLayoutHeaderProps) => { 12 | return ( 13 |
14 | 15 | 16 | 17 | View explanation 18 | 19 | 20 | {explanation} 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default MobileFeatureLayoutHeader; 28 | -------------------------------------------------------------------------------- /src/components/mode-toggle/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | 4 | import React from "react"; 5 | import { Button } from "../ui/button"; 6 | import { useTheme } from "next-themes"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from "../ui/dropdown-menu"; 13 | import { IoSunnyOutline } from "react-icons/io5"; 14 | import { GiMoonBats } from "react-icons/gi"; 15 | 16 | const ModeToggle = () => { 17 | const { setTheme } = useTheme(); 18 | 19 | return ( 20 | 21 | 22 | 27 | 28 | 29 | setTheme("light")}> 30 | Light 31 | 32 | setTheme("dark")}> 33 | Dark 34 | 35 | setTheme("system")}> 36 | System 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default ModeToggle; 44 | -------------------------------------------------------------------------------- /src/components/parallel-route-children-skeleton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "../ui/skeleton"; 2 | 3 | const ParallelRouteChildrenSkeleton = () => { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default ParallelRouteChildrenSkeleton; 26 | -------------------------------------------------------------------------------- /src/components/product/product-card.tsx: -------------------------------------------------------------------------------- 1 | import { Product } from "@/lib/types/product"; 2 | import { toTitleCase } from "@/lib/utils/misc"; 3 | import { lightGrayBlurData } from "@/lib/utils/rgb-data-url"; 4 | import Image from "next/image"; 5 | 6 | interface ProductCardProps { 7 | product: Product; 8 | } 9 | 10 | const ProductCard = ({ product }: ProductCardProps) => { 11 | return ( 12 |
16 |
17 | {product.title} 30 |
31 |
32 |

{toTitleCase(product.title)}

33 |

34 | {toTitleCase(product.category)} 35 |

36 |

37 | Nu.{product.price}{" "} 38 | {product.discountPercentage}%Off 39 |

40 |
41 |
42 | ); 43 | }; 44 | 45 | export default ProductCard; 46 | -------------------------------------------------------------------------------- /src/components/product/product-details/index.tsx: -------------------------------------------------------------------------------- 1 | import { toTitleCase } from "@/lib/utils/misc"; 2 | import { getProductDetailById } from "@/services/product-service"; 3 | import ProductImageCarousel from "./product-image-carousel"; 4 | import { headers } from "next/headers"; 5 | 6 | interface ProductDetailsProps { 7 | usedIn?: "page" | "modal"; 8 | } 9 | 10 | const ProductDetails = async ({ usedIn }: ProductDetailsProps) => { 11 | // Since it is not possible to read query params in the server components 12 | // directly, we are reading it from the headers which we have modified inside the middleware 13 | const productId = (await headers()).get("product_id"); 14 | const product = await getProductDetailById(productId as string); 15 | 16 | if (product === null) { 17 | return ( 18 |

19 | There was an error fetching data. 20 |

21 | ); 22 | } 23 | 24 | return ( 25 |
26 |
{toTitleCase(product.title)}
27 |

{product.description}

28 | 32 |
33 |

34 | ${product.price}{" "} 35 | 36 | {product.discountPercentage}% Off 37 | 38 |

39 |

40 | {product.rating} 41 |

42 |

43 | 44 | Stock: 45 | {" "} 46 | {product.stock} 47 |

48 |

49 | 50 | Brand: 51 | {" "} 52 | {product.brand} 53 |

54 |

55 | 56 | Category: 57 | {" "} 58 | {product.category} 59 |

60 |
61 |
62 | ); 63 | }; 64 | 65 | export default ProductDetails; 66 | -------------------------------------------------------------------------------- /src/components/product/product-list.tsx: -------------------------------------------------------------------------------- 1 | import { ProductsResponse } from "@/lib/types/product"; 2 | import ProductCard from "./product-card"; 3 | 4 | interface ProductListProps { 5 | products: ProductsResponse["products"]; 6 | } 7 | 8 | const ProductList = ({ products }: ProductListProps) => { 9 | return products.map((product) => ( 10 | 11 | )); 12 | }; 13 | 14 | export default ProductList; 15 | -------------------------------------------------------------------------------- /src/components/providers/audio-player-init-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | AudioPlayerInitStore, 5 | createAudioPlayerStore, 6 | } from "@/stores/audio-player-init-store"; 7 | import { createContext, useRef, useContext, PropsWithChildren } from "react"; 8 | import { type StoreApi, useStore } from "zustand"; 9 | 10 | export const AudioPlayerInitContext = 11 | createContext | null>(null); 12 | 13 | export const AudioPlayerInitProvider = ({ children }: PropsWithChildren) => { 14 | const storeRef = useRef>(undefined); 15 | if (!storeRef.current) { 16 | storeRef.current = createAudioPlayerStore(); 17 | } 18 | 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export const useAudioPlayerInit = ( 27 | selector: (store: AudioPlayerInitStore) => T 28 | ): T => { 29 | const audioPlayerInitContext = useContext(AudioPlayerInitContext); 30 | 31 | if (!audioPlayerInitContext) { 32 | throw new Error( 33 | `useAudioPlayerInit must be use within AudioPlayerInitProvider` 34 | ); 35 | } 36 | 37 | return useStore(audioPlayerInitContext, selector); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/search-features/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, Fragment, useCallback } from "react"; 4 | import { 5 | CommandDialog, 6 | CommandInput, 7 | CommandList, 8 | CommandEmpty, 9 | CommandGroup, 10 | CommandItem, 11 | CommandSeparator, 12 | } from "../ui/command"; 13 | import { heroFeatures } from "@/config/hero-features"; 14 | import { useRouter } from "next/navigation"; 15 | import { buttonVariants } from "../ui/button"; 16 | import { cn } from "@/lib/utils"; 17 | 18 | const SearchFeatures = () => { 19 | const [open, setOpen] = useState(false); 20 | const router = useRouter(); 21 | 22 | useEffect(() => { 23 | const down = (e: KeyboardEvent) => { 24 | if (e.key === "k" && (e.metaKey || e.ctrlKey)) { 25 | e.preventDefault(); 26 | setOpen((open) => !open); 27 | } 28 | }; 29 | 30 | document.addEventListener("keydown", down); 31 | return () => document.removeEventListener("keydown", down); 32 | }, []); 33 | 34 | const runCommand = useCallback((command: () => unknown) => { 35 | setOpen(false); 36 | command(); 37 | }, []); 38 | 39 | return ( 40 | <> 41 |
setOpen(true)} 44 | onKeyDown={(e) => 45 | e.target === e.currentTarget && e.key === "Enter" && setOpen(true) 46 | } 47 | className={cn( 48 | buttonVariants({ variant: "outline" }), 49 | "flex text-muted-foreground/80 cursor-pointer rounded-[8px] justify-between mx-auto max-w-[250px] px-[8px] font-normal" 50 | )} 51 | > 52 | Search features & demos 53 | 54 | K 55 | 56 |
57 | 58 | 59 | 60 | No results found. 61 | {heroFeatures.map((feature) => ( 62 | 63 | 64 | {feature.subFeatures.map((subFeature) => ( 65 | 67 | runCommand(() => router.push(subFeature.href)) 68 | } 69 | key={subFeature.description} 70 | value={subFeature.description} 71 | className="cursor-pointer" 72 | > 73 |
74 |

{subFeature.hrefText}

75 | 76 | {subFeature.description} 77 | 78 |
79 |
80 | ))} 81 |
82 | 83 |
84 | ))} 85 |
86 |
87 | 88 | ); 89 | }; 90 | 91 | export default SearchFeatures; 92 | -------------------------------------------------------------------------------- /src/components/shared/back-navigation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { Button } from "../ui/button"; 5 | import { IoMdArrowBack } from "react-icons/io"; 6 | 7 | const BackNavigation = () => { 8 | const router = useRouter(); 9 | return ( 10 | 13 | ); 14 | }; 15 | 16 | export default BackNavigation; 17 | -------------------------------------------------------------------------------- /src/components/shared/data-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { flexRender, Table as ReactTable } from "@tanstack/react-table"; 2 | 3 | import { 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TableHead, 8 | TableHeader, 9 | TableRow, 10 | } from "@/components/ui/table"; 11 | import { RefObject } from "react"; 12 | import { cn } from "@/lib/utils"; 13 | 14 | interface DataTableProps { 15 | table: ReactTable; 16 | columnLength: number; 17 | tableRef: RefObject; 18 | containerClass?: string; 19 | } 20 | 21 | /** 22 | * Reusable tanstack table component. All the table config should be passed from the parent component. 23 | */ 24 | const DataTable = ({ 25 | table, 26 | columnLength, 27 | tableRef, 28 | containerClass, 29 | }: DataTableProps) => { 30 | return ( 31 |
32 | 33 | 34 | {table.getHeaderGroups().map((headerGroup) => ( 35 | 36 | {headerGroup.headers.map((header) => { 37 | return ( 38 | 39 | {header.isPlaceholder 40 | ? null 41 | : flexRender( 42 | header.column.columnDef.header, 43 | header.getContext() 44 | )} 45 | 46 | ); 47 | })} 48 | 49 | ))} 50 | 51 | 52 | {table.getRowModel().rows?.length ? ( 53 | table.getRowModel().rows.map((row) => ( 54 | 58 | {row.parentId} 59 | {row.getVisibleCells().map((cell) => ( 60 | 61 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 62 | 63 | ))} 64 | 65 | )) 66 | ) : ( 67 | 68 | 72 | No results. 73 | 74 | 75 | )} 76 | 77 |
78 |
79 | ); 80 | }; 81 | 82 | export default DataTable; 83 | -------------------------------------------------------------------------------- /src/components/shared/page-error.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Button, buttonVariants } from "../ui/button"; 3 | import { ErrorPageProps } from "@/lib/types/misc"; 4 | 5 | const PageError = ({ reset }: ErrorPageProps) => { 6 | return ( 7 |
8 |

Something went wrong!

9 |
10 | 11 | Home 12 | 13 | 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default PageError; 20 | -------------------------------------------------------------------------------- /src/components/shared/param-update-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Input } from "@/components/ui/input"; 4 | import { parseAsString, useQueryState } from "nuqs"; 5 | import { useDebounce } from "@uidotdev/usehooks"; 6 | import { useEffect, useState, useTransition } from "react"; 7 | import { useRemoveSearchParams } from "@/hooks/use-remove-search-params"; 8 | import ActionLoader from "../loaders/action-loader"; 9 | 10 | interface ParamUpdateInputProps { 11 | shallow: boolean; 12 | paramKey?: string; 13 | resetParamKeys?: string[]; 14 | } 15 | 16 | const ParamUpdateInput = ({ 17 | shallow, 18 | paramKey = "search", 19 | resetParamKeys = [], 20 | }: ParamUpdateInputProps) => { 21 | const [isLoading, startTransition] = useTransition(); // lets us know if the server request is completed 22 | const [initialSearchQuery, setSearchQuery] = useQueryState( 23 | paramKey, 24 | parseAsString.withOptions({ 25 | startTransition: shallow ? undefined : startTransition, 26 | }) 27 | ); 28 | const removeParams = useRemoveSearchParams(resetParamKeys, true); 29 | const [searchQueryValue, setSearchQueryValue] = useState(initialSearchQuery); 30 | const debouncedQueryValue = useDebounce(searchQueryValue, 300); 31 | 32 | useEffect( 33 | function updateSearchQuery() { 34 | const updateQuery = async () => { 35 | debouncedQueryValue !== null && 36 | setSearchQuery(debouncedQueryValue || null, { shallow }); // remove query key if the value === '' 37 | debouncedQueryValue !== null && (await removeParams()); 38 | }; 39 | updateQuery(); 40 | }, 41 | // eslint-disable-next-line react-hooks/exhaustive-deps 42 | [debouncedQueryValue] 43 | ); 44 | 45 | return ( 46 |
47 | e.key === "Enter" && e.currentTarget.blur()} 49 | value={searchQueryValue ?? ""} 50 | onChange={(e) => setSearchQueryValue(e.target.value)} 51 | placeholder="Search products" 52 | className="pr-[40px]" 53 | /> 54 | {isLoading && ( 55 |
56 | 57 |
58 | )} 59 |
60 | ); 61 | }; 62 | 63 | export default ParamUpdateInput; 64 | -------------------------------------------------------------------------------- /src/components/site-footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { FaXTwitter, FaGithub } from "react-icons/fa6"; 2 | import { buttonVariants } from "../ui/button"; 3 | import { cn } from "@/lib/utils"; 4 | const SiteFooter = () => { 5 | return ( 6 | 62 | ); 63 | }; 64 | 65 | export default SiteFooter; 66 | -------------------------------------------------------------------------------- /src/components/site-header/desktop-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { 5 | NavigationMenu, 6 | NavigationMenuContent, 7 | NavigationMenuContentItem, 8 | NavigationMenuItem, 9 | NavigationMenuList, 10 | NavigationMenuTrigger, 11 | } from "../ui/navigation-menu"; 12 | import { navConfig } from "@/config/navigation"; 13 | import SiteLogo from "../logo"; 14 | import { FaCircle } from "react-icons/fa"; 15 | 16 | const DesktopNav = () => { 17 | return ( 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | {navConfig.desktopNav.withMenu.map((menu) => ( 26 | 27 | 28 | {" "} 29 | {menu.title} 30 | 31 | 32 |
    33 | {menu.subMenu.map((subMenu) => ( 34 | 39 | {subMenu.description} 40 | 41 | ))} 42 |
43 |
44 |
45 | ))} 46 |
47 |
48 | 60 |
61 | ); 62 | }; 63 | 64 | export default DesktopNav; 65 | -------------------------------------------------------------------------------- /src/components/site-header/index.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { buttonVariants } from "../ui/button"; 3 | import ModeToggle from "../mode-toggle"; 4 | import MobileNav from "./mobile-nav"; 5 | import { FaGithub } from "react-icons/fa"; 6 | import DesktopNav from "./desktop-nav"; 7 | import { TbSlash } from "react-icons/tb"; 8 | import Link from "next/link"; 9 | 10 | const SiteHeader = () => { 11 | return ( 12 |
13 |
14 |
15 | 16 | 17 | 50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default SiteHeader; 57 | -------------------------------------------------------------------------------- /src/components/templates/feature-explanation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ScrollArea } from "../ui/scroll-area"; 3 | import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; 4 | import { BsInfoSquare } from "react-icons/bs"; 5 | 6 | interface FeatureExplanationTemplateProps extends React.PropsWithChildren { 7 | disclaimer?: string; 8 | } 9 | 10 | const FeatureExplanationTemplate = ({ 11 | children, 12 | disclaimer, 13 | }: FeatureExplanationTemplateProps) => { 14 | return ( 15 | <> 16 |
17 |

18 | 19 | @ 20 | 21 | explanation 22 | {disclaimer && ( 23 | 24 | 28 | 31 | 32 | 37 | {disclaimer} 38 | 39 | 40 | )} 41 |

42 |
43 | 44 | {children} 45 |
46 |

47 | If you have any queries, suggestions, or feedback, you can reach me 48 | via{" "} 49 | 54 | twitter 55 | 56 | . 57 |

58 |
59 |
60 | 61 | ); 62 | }; 63 | 64 | export default FeatureExplanationTemplate; 65 | -------------------------------------------------------------------------------- /src/components/templates/feature-implementation.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from "next"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import { ScrollArea, ScrollBar } from "../ui/scroll-area"; 5 | import AppBreadCrumb from "../app-breadcrumb"; 6 | 7 | interface FeatureImplementationTemplateProps extends React.PropsWithChildren { 8 | longFeatureTitle: string; 9 | resourceLink: string; 10 | inspirationLink?: string; 11 | apiLink?: string; 12 | } 13 | 14 | const FeatureImplementationTemplate = ({ 15 | longFeatureTitle, 16 | children, 17 | resourceLink, 18 | inspirationLink, 19 | apiLink, 20 | }: FeatureImplementationTemplateProps) => { 21 | return ( 22 |
23 | {/* Desktop page header */} 24 |
25 | 26 |
27 | {apiLink && ( 28 | 33 | Data API 34 | 35 | )} 36 | {inspirationLink && ( 37 | 42 | Inspiration 43 | 44 | )} 45 | 50 | Github link 51 | 52 |
53 |
54 |

{longFeatureTitle}

55 | {/* Mobile page header */} 56 | 57 |
58 | 63 | Github link 64 | 65 | {inspirationLink && ( 66 | 71 | Inspiration 72 | 73 | )} 74 | {apiLink && ( 75 | 80 | Data API 81 | 82 | )} 83 |
84 | 85 |
86 |
{children}
87 |
88 | ); 89 | }; 90 | 91 | export default FeatureImplementationTemplate; 92 | -------------------------------------------------------------------------------- /src/components/text-highlight.tsx/index.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from "next"; 2 | import Link from "next/link"; 3 | 4 | interface TextHighlightProps { 5 | text: string; 6 | textLink?: string; 7 | } 8 | 9 | const TextHighlight = ({ text, textLink }: TextHighlightProps) => { 10 | return ( 11 | 12 | {textLink ? ( 13 | 18 | {text} 19 | 20 | ) : ( 21 | {text} 22 | )} 23 | 24 | ); 25 | }; 26 | 27 | export default TextHighlight; 28 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { GoChevronRight } from "react-icons/go"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = ({ 12 | ref, 13 | className, 14 | ...props 15 | }: React.ComponentPropsWithoutRef & { 16 | ref?: React.RefObject>; 17 | }) => ( 18 | 19 | ); 20 | AccordionItem.displayName = "AccordionItem"; 21 | 22 | const AccordionTrigger = ({ 23 | ref, 24 | className, 25 | children, 26 | ...props 27 | }: React.ComponentPropsWithoutRef & { 28 | ref?: React.RefObject>; 29 | }) => ( 30 | 31 | svg]:rotate-90", 35 | className 36 | )} 37 | {...props} 38 | > 39 | {children} 40 | 41 | 42 | 43 | ); 44 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 45 | 46 | const AccordionContent = ({ 47 | ref, 48 | className, 49 | children, 50 | ...props 51 | }: React.ComponentPropsWithoutRef & { 52 | ref?: React.RefObject>; 53 | }) => ( 54 | 59 |
{children}
60 |
61 | ); 62 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 63 | 64 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 65 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = ( 23 | { 24 | ref, 25 | className, 26 | variant = "default", 27 | ...props 28 | }: React.ComponentPropsWithoutRef<"div"> & { 29 | ref?: React.RefObject; 30 | variant?: "default" | "destructive"; 31 | } 32 | ) => (
) 38 | Alert.displayName = "Alert" 39 | 40 | const AlertTitle = ( 41 | { 42 | ref, 43 | className, 44 | ...props 45 | }: React.HTMLAttributes & { 46 | ref?: React.RefObject; 47 | } 48 | ) => (
) 53 | AlertTitle.displayName = "AlertTitle" 54 | 55 | const AlertDescription = ( 56 | { 57 | ref, 58 | className, 59 | ...props 60 | }: React.HTMLAttributes & { 61 | ref?: React.RefObject; 62 | } 63 | ) => (
) 68 | AlertDescription.displayName = "AlertDescription" 69 | 70 | export { Alert, AlertTitle, AlertDescription } 71 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Avatar = ({ 9 | ref, 10 | className, 11 | ...props 12 | }: React.ComponentPropsWithoutRef & { 13 | ref?: React.RefObject>; 14 | }) => ( 15 | 23 | ); 24 | Avatar.displayName = AvatarPrimitive.Root.displayName; 25 | 26 | const AvatarImage = ({ 27 | ref, 28 | className, 29 | ...props 30 | }: React.ComponentPropsWithoutRef & { 31 | ref?: React.RefObject>; 32 | }) => ( 33 | 38 | ); 39 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 40 | 41 | const AvatarFallback = ({ 42 | ref, 43 | className, 44 | ...props 45 | }: React.ComponentPropsWithoutRef & { 46 | ref?: React.RefObject>; 47 | }) => ( 48 | 56 | ); 57 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 58 | 59 | export { Avatar, AvatarImage, AvatarFallback }; 60 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-full font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-white shadow-sm hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-[36px] px-[16px] py-[8px]", 24 | sm: "h-[32px] rounded-full px-[12px] text-[12px]", 25 | lg: "h-[40px] rounded-full px-[32px]", 26 | icon: "h-[36px] w-[36px]", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = ({ 43 | ref, 44 | className, 45 | variant, 46 | size, 47 | asChild = false, 48 | ...props 49 | }: ButtonProps & { 50 | ref?: React.RefObject; 51 | }) => { 52 | const Comp = asChild ? Slot : "button"; 53 | return ( 54 | 59 | ); 60 | }; 61 | Button.displayName = "Button"; 62 | 63 | export { Button, buttonVariants }; 64 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = ({ 6 | ref, 7 | className, 8 | ...props 9 | }: React.HTMLAttributes & { 10 | ref?: React.RefObject; 11 | }) => ( 12 |
20 | ); 21 | Card.displayName = "Card"; 22 | 23 | const CardHeader = ({ 24 | ref, 25 | className, 26 | ...props 27 | }: React.HTMLAttributes & { 28 | ref?: React.RefObject; 29 | }) => ( 30 |
35 | ); 36 | CardHeader.displayName = "CardHeader"; 37 | 38 | const CardTitle = ({ 39 | ref, 40 | className, 41 | ...props 42 | }: React.HTMLAttributes & { 43 | ref?: React.RefObject; 44 | }) => ( 45 |
53 | ); 54 | CardTitle.displayName = "CardTitle"; 55 | 56 | const CardDescription = ({ 57 | ref, 58 | className, 59 | ...props 60 | }: React.HTMLAttributes & { 61 | ref?: React.RefObject; 62 | }) => ( 63 |

64 | ); 65 | CardDescription.displayName = "CardDescription"; 66 | 67 | const CardContent = ({ 68 | ref, 69 | className, 70 | ...props 71 | }: React.HTMLAttributes & { 72 | ref?: React.RefObject; 73 | }) =>

; 74 | CardContent.displayName = "CardContent"; 75 | 76 | const CardFooter = ({ 77 | ref, 78 | className, 79 | ...props 80 | }: React.HTMLAttributes & { 81 | ref?: React.RefObject; 82 | }) => ( 83 |
88 | ); 89 | CardFooter.displayName = "CardFooter"; 90 | 91 | export { 92 | Card, 93 | CardHeader, 94 | CardFooter, 95 | CardTitle, 96 | CardDescription, 97 | CardContent, 98 | }; 99 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { IoCheckmark } from "react-icons/io5"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = ({ 10 | ref, 11 | className, 12 | ...props 13 | }: React.ComponentPropsWithoutRef & { 14 | ref?: React.RefObject>; 15 | }) => ( 16 | 24 | 29 | 30 | 31 | 32 | ); 33 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 34 | 35 | export { Checkbox }; 36 | -------------------------------------------------------------------------------- /src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const HoverCard = HoverCardPrimitive.Root; 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger; 11 | 12 | const HoverCardContent = ({ 13 | ref, 14 | className, 15 | align = "center", 16 | sideOffset = 4, 17 | ...props 18 | }: React.ComponentPropsWithoutRef & { 19 | ref?: React.RefObject>; 20 | }) => ( 21 | 31 | ); 32 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; 33 | 34 | export { HoverCard, HoverCardTrigger, HoverCardContent }; 35 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = ({ 9 | ref, 10 | className, 11 | type, 12 | ...props 13 | }: InputProps & { 14 | ref?: React.RefObject; 15 | }) => { 16 | return ( 17 | 26 | ); 27 | }; 28 | Input.displayName = "Input"; 29 | 30 | export { Input }; 31 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor; 13 | 14 | const PopoverContent = ({ 15 | ref, 16 | className, 17 | align = "center", 18 | sideOffset = 4, 19 | ...props 20 | }: React.ComponentPropsWithoutRef & { 21 | ref?: React.RefObject>; 22 | }) => ( 23 | 24 | 34 | 35 | ); 36 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 37 | 38 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 39 | -------------------------------------------------------------------------------- /src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Progress = ({ 9 | ref, 10 | className, 11 | value, 12 | ...props 13 | }: React.ComponentPropsWithoutRef & { 14 | ref?: React.RefObject>; 15 | }) => ( 16 | 24 | 28 | 29 | ); 30 | Progress.displayName = ProgressPrimitive.Root.displayName; 31 | 32 | export { Progress }; 33 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = ({ 9 | ref, 10 | className, 11 | children, 12 | ...props 13 | }: React.ComponentPropsWithoutRef & { 14 | ref?: React.RefObject>; 15 | }) => ( 16 | 21 | 22 | {children} 23 | 24 | 25 | 26 | 27 | ); 28 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 29 | 30 | const ScrollBar = ({ 31 | ref, 32 | className, 33 | orientation = "vertical", 34 | ...props 35 | }: React.ComponentPropsWithoutRef< 36 | typeof ScrollAreaPrimitive.ScrollAreaScrollbar 37 | > & { 38 | ref?: React.RefObject< 39 | React.ComponentRef 40 | >; 41 | }) => ( 42 | 55 | 56 | 57 | ); 58 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 59 | 60 | export { ScrollArea, ScrollBar }; 61 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = ({ 9 | ref, 10 | className, 11 | orientation = "horizontal", 12 | decorative = true, 13 | ...props 14 | }: React.ComponentPropsWithoutRef & { 15 | ref?: React.RefObject>; 16 | }) => ( 17 | 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SliderPrimitive from "@radix-ui/react-slider"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Slider = ({ 9 | ref, 10 | className, 11 | ...props 12 | }: React.ComponentPropsWithoutRef & { 13 | ref?: React.RefObject>; 14 | }) => ( 15 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | Slider.displayName = SliderPrimitive.Root.displayName; 30 | 31 | export { Slider }; 32 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Switch = ({ 9 | ref, 10 | className, 11 | ...props 12 | }: React.ComponentPropsWithoutRef & { 13 | ref?: React.RefObject>; 14 | }) => ( 15 | 23 | 28 | 29 | ); 30 | Switch.displayName = SwitchPrimitives.Root.displayName; 31 | 32 | export { Switch }; 33 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = ({ 15 | ref, 16 | className, 17 | sideOffset = 4, 18 | ...props 19 | }: React.ComponentPropsWithoutRef & { 20 | ref?: React.RefObject>; 21 | }) => ( 22 | 31 | ); 32 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 33 | 34 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 35 | -------------------------------------------------------------------------------- /src/config/navigation.ts: -------------------------------------------------------------------------------- 1 | import { NavigationConfig } from "@/lib/types/navigation"; 2 | 3 | export const navConfig: NavigationConfig = { 4 | desktopNav: { 5 | withMenu: [ 6 | { 7 | title: "File conventions", 8 | subMenu: [ 9 | { 10 | title: "default.js", 11 | href: "/", 12 | description: 13 | "Render fallback within parallel routes on hard reload.", 14 | }, 15 | { 16 | title: "error.js", 17 | href: "/", 18 | description: 19 | "Handle runtime errors gracefully resulting from the route segment and its nested children.", 20 | }, 21 | { 22 | title: "loading.js", 23 | href: "/", 24 | description: 25 | "Show an instant loading UI while your route segment is loading.", 26 | }, 27 | { 28 | title: "template.js", 29 | href: "/", 30 | description: 31 | "Just like layout.js but unlike layout, it creates a new instance for each children on navigation.", 32 | }, 33 | { 34 | title: "not-found.js", 35 | href: "/", 36 | description: 37 | "Render UI when notFound() is called withing a route segment.", 38 | }, 39 | { 40 | title: "Metadata", 41 | href: "/", 42 | description: 43 | "Statically or dynamically generate your metadata files for each route segment.", 44 | }, 45 | ], 46 | }, 47 | ], 48 | withoutMenu: [ 49 | { 50 | title: "Internationalization", 51 | href: "/", 52 | description: 53 | "Isolate errors to affected segments while keeping the rest of the application functional.", 54 | }, 55 | ], 56 | }, 57 | mobileNav: { 58 | withSubMenu: [ 59 | { 60 | title: "File conventions", 61 | subMenu: [ 62 | { 63 | title: "default.js", 64 | href: "/", 65 | }, 66 | { 67 | title: "error.js", 68 | href: "/", 69 | }, 70 | { 71 | title: "loading.js", 72 | href: "/", 73 | }, 74 | { 75 | title: "template.js", 76 | href: "/", 77 | }, 78 | { 79 | title: "not-found.js", 80 | href: "/", 81 | }, 82 | { 83 | title: "Metadata", 84 | href: "/", 85 | }, 86 | ], 87 | }, 88 | ], 89 | withoutSubMenu: [ 90 | { 91 | title: "Internationalization", 92 | href: "/", 93 | }, 94 | ], 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /src/helpers/fetch-helper.ts: -------------------------------------------------------------------------------- 1 | import { FetchArguments } from "@/lib/types/misc"; 2 | 3 | /** 4 | * Wrapper for `fetch` that handles promise rejection based on response status. 5 | * @param fetchArgs 6 | * @returns 7 | */ 8 | export const fetchHelper = async ( 9 | fetchArgs: FetchArguments 10 | ): Promise => { 11 | try { 12 | const response = await fetch(fetchArgs.url, { 13 | method: fetchArgs.method, 14 | body: JSON.stringify(fetchArgs.body), 15 | headers: fetchArgs.headers, 16 | next: fetchArgs.nextOptions, 17 | cache: fetchArgs.cache, 18 | }); 19 | 20 | if (!response.ok) { 21 | let errorDetails; 22 | try { 23 | errorDetails = await response.json(); 24 | } catch (_) { 25 | errorDetails = { 26 | status: response.status, 27 | statusText: response.statusText, 28 | message: "An unexpected error occurred", 29 | }; 30 | } 31 | 32 | const error = new Error("Fetch Error"); 33 | error.name = "FetchError"; 34 | (error as any).details = errorDetails; 35 | throw error; 36 | } 37 | 38 | return response.json(); 39 | } catch (error) { 40 | // Handle network errors, CORS issues, etc. 41 | console.error("Fetch error:", error); 42 | throw error; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/hooks/use-breadcrumb-objects.ts: -------------------------------------------------------------------------------- 1 | import { BreadCrumbObj } from "@/lib/types/misc"; 2 | import { getProductDetailById } from "@/services/product-service"; 3 | import { usePathname } from "next/navigation"; 4 | import { useEffect, useState } from "react"; 5 | 6 | /** 7 | * A hook to get an array of breadcrumb objects, both static and dynamic. 8 | * @returns 9 | */ 10 | export const useBreadCrumbObjects = () => { 11 | const [breadCrumbObjects, setBreadcrumbObjects] = useState( 12 | [] 13 | ); 14 | const paths = usePathname(); 15 | const pathNames = paths.split("/").filter((path) => path); 16 | 17 | useEffect(() => { 18 | (async () => { 19 | let breadCrumbObjects: BreadCrumbObj[] = []; 20 | // at product page 21 | if (pathNames.includes("product") && pathNames.length === 2) { 22 | breadCrumbObjects = await getProductPageBreadcrumbObjects( 23 | pathNames, 24 | pathNames.at(-1) as string 25 | ); 26 | } else { 27 | // static routes 28 | breadCrumbObjects = getStaticBreadcrumbObjects(pathNames); 29 | } 30 | setBreadcrumbObjects(breadCrumbObjects); 31 | })(); 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, [paths]); 34 | 35 | return { 36 | breadCrumbObjects, 37 | }; 38 | }; 39 | 40 | // Utility to get breadcrumb objects for all static routes 41 | const getStaticBreadcrumbObjects = (pathNames: string[]): BreadCrumbObj[] => { 42 | return pathNames.reduce( 43 | (accumulator, path, index) => { 44 | let href: string | undefined; 45 | let label = path.split("-").join(" "); 46 | 47 | if (index === pathNames.length - 1) { 48 | href = undefined; 49 | } else { 50 | href = `/${pathNames.slice(0, index + 1).join("/")}`; 51 | } 52 | 53 | return [ 54 | ...accumulator, 55 | { 56 | label, 57 | href, 58 | }, 59 | ]; 60 | }, 61 | [ 62 | { 63 | label: "Home", 64 | href: "/", 65 | }, 66 | ] 67 | ); 68 | }; 69 | 70 | const getProductPageBreadcrumbObjects = async ( 71 | pathNames: string[], 72 | productId: string 73 | ): Promise => { 74 | const product = await getProductDetailById(productId); 75 | return pathNames.reduce( 76 | (accumulator, path, index) => { 77 | let href: string | undefined; 78 | let label: string; 79 | 80 | if (index === pathNames.length - 1) { 81 | label = product?.title ?? "No title"; 82 | href = undefined; 83 | } else { 84 | label = path.split("-").join(" "); 85 | href = `/${pathNames.slice(0, index + 1).join("/")}`; 86 | } 87 | if (path === "product") { 88 | return accumulator; 89 | } else { 90 | return [ 91 | ...accumulator, 92 | { 93 | label, 94 | href, 95 | }, 96 | ]; 97 | } 98 | }, 99 | [ 100 | { 101 | label: "Home", 102 | href: "/", 103 | }, 104 | ] 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/hooks/use-remove-search-params.ts: -------------------------------------------------------------------------------- 1 | import { useQueryStates } from "nuqs"; 2 | 3 | /** 4 | * Hook to remove multiple URL search params. Useful if you want to reset or remove 5 | * other query key-value based on another query. 6 | * @param keysToDelete Just an array of param keys that needs to be removed 7 | * @param shallow Whether the deletion should cause server request. 8 | * `true` won't cause server request while `false` will. 9 | * @returns 10 | */ 11 | export const useRemoveSearchParams = ( 12 | keysToDelete: string[], 13 | shallow: boolean 14 | ) => { 15 | const nullableParamKeys = keysToDelete.reduce<{ 16 | [index: string]: { parse: (value: string) => string | null }; 17 | }>((accumulator, queryKey) => { 18 | accumulator[queryKey] = { parse: () => null }; 19 | return accumulator; 20 | }, {}); 21 | const [_, resetQueryKeys] = useQueryStates( 22 | { ...nullableParamKeys }, 23 | { 24 | shallow, 25 | } 26 | ); 27 | 28 | /** 29 | * Calling this function will remove all the query passed in the hook initialization. 30 | */ 31 | const removeSearchParams = async () => { 32 | await resetQueryKeys( 33 | keysToDelete.reduce<{ [index: string]: null }>((accumulator, key) => { 34 | accumulator[key] = null; 35 | return accumulator; 36 | }, {}) 37 | ); 38 | }; 39 | return removeSearchParams; 40 | }; 41 | -------------------------------------------------------------------------------- /src/hooks/use-tailwind-media-query.ts: -------------------------------------------------------------------------------- 1 | import { TailwindBreakPoints } from "@/lib/types/misc"; 2 | import { useEffect, useState } from "react"; 3 | 4 | /** 5 | * A hook to dynamically query device size based on tailwind default breakpoints. 6 | * Checks for the minimum width query: `(min-width: ${tailwindMediaWidth}px)` to make it compliant with 7 | * tailwind default media breakpoint classes like `sm | md | lg | xl | 2xl` 8 | * @param tailwindMediaWidth: "640" | "768" | "1024" | "1280" | "1536" 9 | * @returns 10 | */ 11 | export function useTailwindMediaQuery( 12 | tailwindMediaWidth: TailwindBreakPoints 13 | ) { 14 | const [mediaMatches, setMediaMatches] = useState(false); 15 | const [isChecking, setIsChecking] = useState(true); // 16 | 17 | useEffect(() => { 18 | const mediaWatcher = window.matchMedia( 19 | `(min-width: ${tailwindMediaWidth}px)` 20 | ); 21 | setMediaMatches(mediaWatcher.matches); 22 | setIsChecking(false); 23 | 24 | function updateMediaMatch(e: MediaQueryListEvent) { 25 | setMediaMatches(e.matches); 26 | } 27 | 28 | mediaWatcher.addEventListener("change", updateMediaMatch); 29 | 30 | return function cleanup() { 31 | mediaWatcher.removeEventListener("change", updateMediaMatch); 32 | }; 33 | }, [tailwindMediaWidth]); 34 | 35 | return { 36 | mediaMatches, 37 | isChecking, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/constants/metadata.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorji-dev/next_app/cfa49826afbe811a18fbce2115487426e503f146/src/lib/constants/metadata.ts -------------------------------------------------------------------------------- /src/lib/constants/misc.ts: -------------------------------------------------------------------------------- 1 | import { LibrariesUsed } from "../types/misc"; 2 | 3 | export const LIBRARIES_USED: LibrariesUsed = [ 4 | { 5 | name: "swr", 6 | link: "https://swr.vercel.app/", 7 | description: "React Hooks for Data Fetching.", 8 | }, 9 | { 10 | name: "nuqs", 11 | link: "https://nuqs.47ng.com/", 12 | description: "Type-safe search params state manager for Next.js.", 13 | }, 14 | { 15 | name: "@tanstack/react-table", 16 | link: "https://tanstack.com/table/latest", 17 | description: "Headless UI for building powerful tables & datagrids.", 18 | }, 19 | { 20 | name: "react-intersection-observer", 21 | link: "https://react-intersection-observer.vercel.app/?path=/docs/intro--docs", 22 | description: 23 | "React implementation of the Intersection Observer API to tell you when an element enters or leaves the viewport.", 24 | }, 25 | { 26 | name: "next-themes", 27 | link: "https://github.com/pacocoursey/next-themes", 28 | description: 29 | "Perfect Next.js dark mode in 2 lines of code. Support System preference and any other theme with no flashing.", 30 | }, 31 | { 32 | name: "react-icons", 33 | link: "https://react-icons.github.io/react-icons/", 34 | description: 35 | "Include popular icons in your React projects easily with react-icons, which utilizes ES6 imports that allows you to include only the icons that your project is using.", 36 | }, 37 | { 38 | name: "radix-ui", 39 | link: "https://www.radix-ui.com/primitives/docs/overview/introduction", 40 | description: 41 | "Unstyled, accessible, open source React primitives for high-quality web apps and design systems.", 42 | }, 43 | { 44 | name: "tailwindcss", 45 | link: "https://tailwindcss.com/", 46 | description: 47 | "A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.", 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /src/lib/constants/tailwind-device-width.ts: -------------------------------------------------------------------------------- 1 | export const TAILWIND_XXSMALL = "360"; 2 | export const TAILWIND_XSMALL = "480"; 3 | export const TAILWIND_SMALL = "640"; 4 | export const TAILWIND_MEDIUM = "768"; 5 | export const TAILWIND_LARGE = "1024"; 6 | export const TAILWIND_XL = "1280"; 7 | export const TAILWIND_2XL = "1536"; 8 | -------------------------------------------------------------------------------- /src/lib/types/hero-feature.ts: -------------------------------------------------------------------------------- 1 | import { Route } from "next"; 2 | 3 | export type HeroFeatures = { 4 | title: string; 5 | content: string; 6 | subFeatures: { 7 | hrefText: string; 8 | href: Route; 9 | completed: boolean; 10 | description: string; 11 | }[]; 12 | href?: Route 13 | }[]; 14 | -------------------------------------------------------------------------------- /src/lib/types/misc.ts: -------------------------------------------------------------------------------- 1 | import { IconType } from "react-icons/lib"; 2 | import { 3 | TAILWIND_2XL, 4 | TAILWIND_LARGE, 5 | TAILWIND_MEDIUM, 6 | TAILWIND_SMALL, 7 | TAILWIND_XL, 8 | TAILWIND_XSMALL, 9 | TAILWIND_XXSMALL, 10 | } from "../constants/tailwind-device-width"; 11 | 12 | export type FETCH_METHODS = "POST" | "GET" | "DELETE" | "PATCH" | "PUT"; 13 | 14 | export type ClassNameProp = { 15 | className?: string; 16 | }; 17 | 18 | export type TailwindBreakPoints = 19 | | typeof TAILWIND_XXSMALL 20 | | typeof TAILWIND_XSMALL 21 | | typeof TAILWIND_SMALL 22 | | typeof TAILWIND_MEDIUM 23 | | typeof TAILWIND_LARGE 24 | | typeof TAILWIND_XL 25 | | typeof TAILWIND_2XL; 26 | 27 | export type NextFetchTags = ""; 28 | 29 | export type FetchArguments = { 30 | url: string; 31 | method: FETCH_METHODS; 32 | body?: BodyType; 33 | headers?: { [index: string]: string }; 34 | nextOptions?: 35 | | (Omit & { 36 | tags?: NextFetchTags[]; 37 | }) 38 | | undefined; 39 | cache?: RequestCache; 40 | }; 41 | 42 | export type ErrorPageProps = { 43 | reset: () => void; 44 | error?: Error & { digest?: string }; 45 | }; 46 | 47 | export type LibrariesUsed = { 48 | name: string; 49 | link: string; 50 | description: string; 51 | }[]; 52 | 53 | export interface FileTreeDataItem { 54 | id: string; 55 | name: string; 56 | icon?: IconType; 57 | children?: FileTreeDataItem[]; 58 | } 59 | 60 | export type BreadCrumbObj = { label: string; href?: string }; 61 | 62 | export type Song = { 63 | name: string; 64 | id: string; 65 | artists: string[]; 66 | poster: string 67 | }; 68 | -------------------------------------------------------------------------------- /src/lib/types/navigation.ts: -------------------------------------------------------------------------------- 1 | import { Route } from "next"; 2 | 3 | export type NavigationConfig = { 4 | desktopNav: { 5 | withMenu: { 6 | title: string; 7 | subMenu: { 8 | title: string; 9 | href: Route; 10 | description: string; 11 | }[]; 12 | }[]; 13 | withoutMenu: { 14 | title: string; 15 | href: Route; 16 | description: string; 17 | }[]; 18 | }; 19 | mobileNav: { 20 | withSubMenu: { 21 | title: string; 22 | subMenu: { 23 | title: string; 24 | href: Route; 25 | }[]; 26 | }[]; 27 | withoutSubMenu: { 28 | title: string; 29 | href: Route; 30 | }[]; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/types/product.ts: -------------------------------------------------------------------------------- 1 | export interface ProductsResponse { 2 | products: Product[]; 3 | total: number; 4 | skip: number; 5 | limit: number; 6 | } 7 | 8 | export type Product = { 9 | id: number; 10 | title: string; 11 | description: string; 12 | price: number; 13 | discountPercentage: number; 14 | rating: number; 15 | stock: number; 16 | brand: string; 17 | category: string; 18 | thumbnail: string; 19 | images: string[]; 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shadcn.utils'; -------------------------------------------------------------------------------- /src/lib/utils/misc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert string to title case 3 | * @param str 4 | * @returns 5 | */ 6 | export const toTitleCase = (str: string): string => { 7 | return str.replace( 8 | /\b\w+/g, 9 | (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase() 10 | ); 11 | }; 12 | 13 | /** 14 | * Formats the given duration in seconds to the given format 15 | * @param seconds The duration in seconds 16 | * @param format The format to format the duration in `hh:mm:ss` or `mm:ss` 17 | * @returns The formatted duration 18 | */ 19 | export function formatDuration(seconds: number, format: "hh:mm:ss" | "mm:ss") { 20 | const date = new Date(seconds * 1000); 21 | 22 | return format === "hh:mm:ss" 23 | ? date.toISOString().slice(11, 19) 24 | : date.toISOString().slice(14, 19); 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/utils/pagination.ts: -------------------------------------------------------------------------------- 1 | export const getActivePage = ( 2 | page: string | null, 3 | pageSize: number, 4 | totalItems: number 5 | ) => { 6 | const toFixedValue = Number(Number(page).toFixed(0)); 7 | const maximumSizePossible = getMaximumPagePossible(pageSize, totalItems); 8 | if (page && Number(page) && toFixedValue <= maximumSizePossible) { 9 | return toFixedValue <= 0 ? 1 : toFixedValue; 10 | } else { 11 | return 1; 12 | } 13 | }; 14 | 15 | export const getMaximumPagePossible = ( 16 | pageSize: number, 17 | totalItems: number 18 | ) => { 19 | let maximumPagePossible = 1; 20 | while (maximumPagePossible * pageSize < totalItems) { 21 | maximumPagePossible++; 22 | } 23 | return maximumPagePossible; 24 | }; 25 | 26 | export const getPagesToShow = ( 27 | maximumSizePossible: number, 28 | activePage: number, 29 | totalNumberToShow: number 30 | ) => { 31 | let topFivePageNumber: number[] = []; 32 | const partitionValue = Math.floor(totalNumberToShow / 2); 33 | // left half values based on partition value, always starts at 1 34 | const leastLeftPartitionValues = Array.from({ length: partitionValue }).map( 35 | (_, index) => index + 1 36 | ); 37 | 38 | // right half values based on partition value, maximum size possible, and total number to show 39 | const greatestRightPartitionValues = Array.from({ 40 | length: partitionValue, 41 | }) 42 | .map( 43 | (_, index) => 44 | (maximumSizePossible < totalNumberToShow 45 | ? totalNumberToShow 46 | : maximumSizePossible) - index 47 | ) 48 | .reverse(); 49 | if (leastLeftPartitionValues.includes(activePage)) { 50 | topFivePageNumber = [...leastLeftPartitionValues]; 51 | Array.from({ 52 | length: totalNumberToShow - leastLeftPartitionValues.length, 53 | }).forEach((_, index) => 54 | topFivePageNumber.push( 55 | leastLeftPartitionValues[leastLeftPartitionValues.length - 1] + 56 | (index + 1) 57 | ) 58 | ); 59 | } else if (greatestRightPartitionValues.includes(activePage)) { 60 | const leftPageNumbers: number[] = []; 61 | Array.from({ 62 | length: totalNumberToShow - greatestRightPartitionValues.length, 63 | }).forEach((_, index) => 64 | leftPageNumbers.push(greatestRightPartitionValues[0] - (index + 1)) 65 | ); 66 | topFivePageNumber = [ 67 | ...leftPageNumbers.reverse(), 68 | ...greatestRightPartitionValues, 69 | ]; 70 | } else { 71 | const leftPageNumbers: number[] = []; 72 | const rightPageNumbers: number[] = []; 73 | Array.from({ length: partitionValue }).forEach((_, index) => 74 | leftPageNumbers.push(activePage - (index + 1)) 75 | ); 76 | Array.from({ length: partitionValue }).forEach((_, index) => 77 | rightPageNumbers.push(activePage + (index + 1)) 78 | ); 79 | topFivePageNumber = [ 80 | ...leftPageNumbers.reverse(), 81 | activePage, 82 | ...rightPageNumbers, 83 | ]; 84 | } 85 | return topFivePageNumber; 86 | }; 87 | -------------------------------------------------------------------------------- /src/lib/utils/rgb-data-url.ts: -------------------------------------------------------------------------------- 1 | const keyStr = 2 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 3 | 4 | const triplet = (e1: number, e2: number, e3: number) => 5 | keyStr.charAt(e1 >> 2) + 6 | keyStr.charAt(((e1 & 3) << 4) | (e2 >> 4)) + 7 | keyStr.charAt(((e2 & 15) << 2) | (e3 >> 6)) + 8 | keyStr.charAt(e3 & 63); 9 | 10 | export const rgbDataURL = (r: number, g: number, b: number) => 11 | `data:image/gif;base64,R0lGODlhAQABAPAA${ 12 | triplet(0, r, g) + triplet(b, 255, 255) 13 | }/yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==`; 14 | 15 | export const lightGrayBlurData = rgbDataURL(229, 228, 226); -------------------------------------------------------------------------------- /src/lib/utils/shadcn.utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export function middleware(request: NextRequest) { 4 | const requestHeaders = new Headers(request.headers); 5 | const productId = request.nextUrl.pathname.split("/").at(-1); 6 | // set the product id as part of request header to access in the server components 7 | // without having to prop drill from page or layout 8 | // This is just a workaround for now until Next supports such access 9 | requestHeaders.set("product_id", productId as string); 10 | 11 | return NextResponse.next({ 12 | request: { 13 | headers: requestHeaders, 14 | }, 15 | }); 16 | } 17 | 18 | export const config = { 19 | matcher: ["/product/:path*"], 20 | }; 21 | -------------------------------------------------------------------------------- /src/services/product-service.ts: -------------------------------------------------------------------------------- 1 | import { fetchHelper } from "@/helpers/fetch-helper"; 2 | import { Product, ProductsResponse } from "@/lib/types/product"; 3 | 4 | /** 5 | * Get product listings 6 | * @param offset 7 | * @param pageSize 8 | * @param searchKey 9 | * @returns `null` if there is an error while fetching, so don't forget to handle it to show appropriate UI 10 | */ 11 | export const getProducts = async ( 12 | offset: number = 0, 13 | pageSize: number, 14 | searchKey: string = "" 15 | ) => { 16 | // modify url based on existence of search key 17 | const url = `https://dummyjson.com/products${ 18 | searchKey?.trim() ? `/search?q=${searchKey}&` : "?" 19 | }limit=${pageSize}&skip=${offset}`; 20 | return fetchHelper({ 21 | url, 22 | method: "GET", 23 | }).catch(() => null); 24 | }; 25 | 26 | /** 27 | * Get product details by product ID 28 | * @param id `productId` 29 | * @returns `null` if there is an error while fetching, so don't forget to handle it to show appropriate UI 30 | */ 31 | export const getProductDetailById = async (id: string) => { 32 | const url = `https://dummyjson.com/products/${id}`; 33 | return fetchHelper({ 34 | url, 35 | method: "GET", 36 | }).catch(() => null); 37 | }; 38 | -------------------------------------------------------------------------------- /src/stores/audio-player-init-store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "zustand/vanilla"; 2 | 3 | export type AudioPlayerInitState = { 4 | isPlayerInitialized: boolean; 5 | }; 6 | 7 | export type AudioPlayerInitActions = { 8 | setIsPlayerInitialized: (init: boolean) => void; 9 | }; 10 | 11 | export type AudioPlayerInitStore = AudioPlayerInitState & 12 | AudioPlayerInitActions; 13 | 14 | const initialAudioPlayerState: AudioPlayerInitState = { 15 | isPlayerInitialized: false, 16 | }; 17 | 18 | export const createAudioPlayerStore = ( 19 | initialState: AudioPlayerInitState = initialAudioPlayerState 20 | ) => { 21 | return createStore()((set) => ({ 22 | ...initialState, 23 | setIsPlayerInitialized: (init) => 24 | set(() => ({ isPlayerInitialized: init })), 25 | })); 26 | }; 27 | -------------------------------------------------------------------------------- /src/styles/action-loader.module.css: -------------------------------------------------------------------------------- 1 | .action_loader { 2 | position: relative; 3 | width: 20px; 4 | height: 20px; 5 | border-radius: 10px; 6 | } 7 | 8 | .action_loader div { 9 | width: 8%; 10 | height: 24%; 11 | background: rgb(128, 128, 128); 12 | position: absolute; 13 | left: 47%; 14 | top: 37%; 15 | opacity: 0; 16 | border-radius: 50px; 17 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); 18 | animation: fade458 1s linear infinite; 19 | } 20 | 21 | @keyframes fade458 { 22 | from { 23 | opacity: 1; 24 | } 25 | 26 | to { 27 | opacity: 0.25; 28 | } 29 | } 30 | 31 | .action_loader .bar1 { 32 | transform: rotate(0deg) translate(0, -130%); 33 | animation-delay: 0s; 34 | } 35 | 36 | .action_loader .bar2 { 37 | transform: rotate(30deg) translate(0, -130%); 38 | animation-delay: -1.1s; 39 | } 40 | 41 | .action_loader .bar3 { 42 | transform: rotate(60deg) translate(0, -130%); 43 | animation-delay: -1s; 44 | } 45 | 46 | .action_loader .bar4 { 47 | transform: rotate(90deg) translate(0, -130%); 48 | animation-delay: -0.9s; 49 | } 50 | 51 | .action_loader .bar5 { 52 | transform: rotate(120deg) translate(0, -130%); 53 | animation-delay: -0.8s; 54 | } 55 | 56 | .action_loader .bar6 { 57 | transform: rotate(150deg) translate(0, -130%); 58 | animation-delay: -0.7s; 59 | } 60 | 61 | .action_loader .bar7 { 62 | transform: rotate(180deg) translate(0, -130%); 63 | animation-delay: -0.6s; 64 | } 65 | 66 | .action_loader .bar8 { 67 | transform: rotate(210deg) translate(0, -130%); 68 | animation-delay: -0.5s; 69 | } 70 | 71 | .action_loader .bar9 { 72 | transform: rotate(240deg) translate(0, -130%); 73 | animation-delay: -0.4s; 74 | } 75 | 76 | .action_loader .bar10 { 77 | transform: rotate(270deg) translate(0, -130%); 78 | animation-delay: -0.3s; 79 | } 80 | 81 | .action_loader .bar11 { 82 | transform: rotate(300deg) translate(0, -130%); 83 | animation-delay: -0.2s; 84 | } 85 | 86 | .action_loader .bar12 { 87 | transform: rotate(330deg) translate(0, -130%); 88 | animation-delay: -0.1s; 89 | } 90 | -------------------------------------------------------------------------------- /src/styles/content-loader.module.css: -------------------------------------------------------------------------------- 1 | .content_loader { 2 | position: relative; 3 | margin: auto; 4 | width: 100px; 5 | height: 100px; 6 | background: transparent; 7 | border: 3px solid rgba(22, 163, 74, 0.1); 8 | border-radius: 50%; 9 | text-align: center; 10 | line-height: 100px; 11 | font-family: sans-serif; 12 | font-size: 10px; 13 | font-weight: 600; 14 | color: #16a34a; 15 | letter-spacing: 2px; 16 | text-transform: uppercase; 17 | text-shadow: 0 0 10px #16a34a; 18 | box-shadow: 0 0 20px rgba(0, 0, 0, .15); 19 | } 20 | 21 | .content_loader::before { 22 | content: ''; 23 | position: absolute; 24 | top: 0px; 25 | left: 0px; 26 | width: 100%; 27 | height: 100%; 28 | border: 3px solid transparent; 29 | border-top: 3px solid #16a34a; 30 | border-right: 3px solid #16a34a; 31 | border-radius: 50%; 32 | animation: animateC 2s linear infinite; 33 | } 34 | 35 | .content_loader span { 36 | display: block; 37 | position: absolute; 38 | top: calc(50% - 2px); 39 | left: 50%; 40 | width: 50%; 41 | height: 4px; 42 | background: transparent; 43 | transform-origin: left; 44 | animation: animate 2s linear infinite; 45 | } 46 | 47 | .content_loader span::before { 48 | content: ''; 49 | position: absolute; 50 | width: 12px; 51 | height: 12px; 52 | border-radius: 50%; 53 | background: #16a34a; 54 | top: -10px; 55 | right: -4px; 56 | box-shadow: 0 0 20px 5px #16a34a; 57 | } 58 | 59 | @keyframes animateC { 60 | 0% { 61 | transform: rotate(0deg); 62 | } 63 | 64 | 100% { 65 | transform: rotate(360deg); 66 | } 67 | } 68 | 69 | @keyframes animate { 70 | 0% { 71 | transform: rotate(45deg); 72 | } 73 | 74 | 100% { 75 | transform: rotate(405deg); 76 | } 77 | } -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @config '../../tailwind.config.ts'; 4 | 5 | /* 6 | The default border color has changed to `currentColor` in Tailwind CSS v4, 7 | so we've added these compatibility styles to make sure everything still 8 | looks the same as it did with Tailwind CSS v3. 9 | 10 | If we ever want to remove these styles, we need to add an explicit border 11 | color utility to any element that depends on these defaults. 12 | */ 13 | @layer base { 14 | *, 15 | ::after, 16 | ::before, 17 | ::backdrop, 18 | ::file-selector-button { 19 | border-color: var(--color-gray-200, currentColor); 20 | } 21 | } 22 | 23 | @layer base { 24 | :root { 25 | --background: 0 0% 100%; 26 | --foreground: 240 10% 3.9%; 27 | --card: 0 0% 100%; 28 | --card-foreground: 240 10% 3.9%; 29 | --popover: 0 0% 100%; 30 | --popover-foreground: 240 10% 3.9%; 31 | --primary: 142.1 76.2% 36.3%; 32 | --primary-foreground: 355.7 100% 97.3%; 33 | --secondary: 240 4.8% 95.9%; 34 | --secondary-foreground: 240 5.9% 10%; 35 | --muted: 240 4.8% 95.9%; 36 | --muted-foreground: 240 3.8% 46.1%; 37 | --accent: 240 4.8% 95.9%; 38 | --accent-foreground: 240 5.9% 10%; 39 | --destructive: 0 84.2% 60.2%; 40 | --destructive-foreground: 0 0% 98%; 41 | --border: 240 5.9% 90%; 42 | --input: 240 5.9% 90%; 43 | --ring: 142.1 76.2% 36.3%; 44 | --radius: 0.5rem; 45 | } 46 | 47 | .dark { 48 | --background: 20 14.3% 4.1%; 49 | --foreground: 0 0% 95%; 50 | --card: 24 9.8% 10%; 51 | --card-foreground: 0 0% 95%; 52 | --popover: 0 0% 9%; 53 | --popover-foreground: 0 0% 95%; 54 | --primary: 142.1 70.6% 45.3%; 55 | --primary-foreground: 144.9 80.4% 10%; 56 | --secondary: 240 3.7% 15.9%; 57 | --secondary-foreground: 0 0% 98%; 58 | --muted: 0 0% 15%; 59 | --muted-foreground: 240 5% 64.9%; 60 | --accent: 12 6.5% 15.1%; 61 | --accent-foreground: 0 0% 98%; 62 | --destructive: 0 62.8% 30.6%; 63 | --destructive-foreground: 0 85.7% 97.3%; 64 | --border: 240 3.7% 15.9%; 65 | --input: 240 3.7% 15.9%; 66 | --ring: 142.4 71.8% 29.2%; 67 | } 68 | 69 | * { 70 | @apply border-border; 71 | } 72 | 73 | html { 74 | @apply text-[15px]; 75 | } 76 | 77 | h1 { 78 | @apply text-[max(48px,min(5vw,76px))] leading-[1.12]; 79 | } 80 | h2 { 81 | @apply text-[max(42px,min(5vw,56px))] leading-[1.12]; 82 | } 83 | h3 { 84 | @apply text-[max(36px,min(5vw,50px))] leading-[1.12]; 85 | } 86 | h4 { 87 | @apply text-[max(30px,min(5vw,44px))] leading-[1.12]; 88 | } 89 | h5 { 90 | @apply text-[max(24px,min(5vw,36px))] leading-[1.3]; 91 | } 92 | h6 { 93 | @apply text-[max(18px,min(5vw,28px))] leading-[1.12]; 94 | } 95 | .theme-link { 96 | @apply text-[14px] text-primary underline decoration-border underline-offset-[4px] hover:opacity-[.7] transition-all duration-300; 97 | } 98 | } 99 | 100 | @media (max-width: 640px) { 101 | .container { 102 | @apply px-[10px]; 103 | } 104 | } 105 | 106 | @media (max-width: 280px) { 107 | .container { 108 | @apply min-w-[280px] overflow-auto; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/styles/play-animation.module.css: -------------------------------------------------------------------------------- 1 | .now.playing .bar { 2 | display: inline-block; 3 | position: relative; 4 | margin-right: 1px; 5 | width: 3px; 6 | height: 1px; 7 | overflow: hidden; 8 | background: linear-gradient(to bottom, #03fa5d, #16a34a); 9 | color: transparent; 10 | animation-name: pulse; 11 | animation-duration: 2s; 12 | animation-iteration-count: infinite; 13 | } 14 | .n1 { 15 | animation-delay: 0.5s; 16 | } 17 | .n2 { 18 | animation-delay: 0.2s; 19 | } 20 | .n3 { 21 | animation-delay: 1.2s; 22 | } 23 | .n4 { 24 | animation-delay: 0.9s; 25 | } 26 | .n5 { 27 | animation-delay: 2.3s; 28 | } 29 | .n6 { 30 | animation-delay: 1.3s; 31 | } 32 | .n7 { 33 | animation-delay: 3.1s; 34 | } 35 | .n8 { 36 | animation-delay: 1.9s; 37 | } 38 | @keyframes pulse { 39 | 0% { 40 | height: 1px; 41 | margin-top: 0; 42 | } 43 | 10% { 44 | height: 10px; 45 | margin-top: -40px; 46 | } 47 | 50% { 48 | height: 5px; 49 | margin-top: -20px; 50 | } 51 | 60% { 52 | height: 15px; 53 | margin-top: -30px; 54 | } 55 | 80% { 56 | height: 25px; 57 | margin-top: -60px; 58 | } 59 | 100% { 60 | height: 1px; 61 | margin-top: 0; 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/styles/progress-loader.module.css: -------------------------------------------------------------------------------- 1 | .progress_loader { 2 | display: block; 3 | --height-of-loader: 2px; 4 | --loader-color: #16a34a; 5 | width: 100%; 6 | height: var(--height-of-loader); 7 | border-radius: 30px; 8 | background-color: rgba(201, 201, 201, 0.2); 9 | position: relative; 10 | } 11 | 12 | .progress_loader::before { 13 | content: ""; 14 | position: absolute; 15 | background: var(--loader-color); 16 | top: 0; 17 | left: 0; 18 | width: 0%; 19 | height: 100%; 20 | border-radius: 30px; 21 | animation: moving 1s ease-in-out infinite; 22 | ; 23 | } 24 | 25 | @keyframes moving { 26 | 50% { 27 | width: 100%; 28 | } 29 | 30 | 100% { 31 | width: 0; 32 | right: 0; 33 | left: unset; 34 | } 35 | } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config = { 4 | future: { 5 | hoverOnlyWhenSupported: true, 6 | }, 7 | darkMode: "class", 8 | content: [ 9 | "./pages/**/*.{ts,tsx}", 10 | "./components/**/*.{ts,tsx}", 11 | "./app/**/*.{ts,tsx}", 12 | "./src/**/*.{ts,tsx}", 13 | ], 14 | prefix: "", 15 | theme: { 16 | container: { 17 | center: true, 18 | padding: "32px", 19 | screens: { 20 | "2xl": "1400px", 21 | }, 22 | }, 23 | extend: { 24 | colors: { 25 | border: "hsl(var(--border))", 26 | input: "hsl(var(--input))", 27 | ring: "hsl(var(--ring))", 28 | background: "hsl(var(--background))", 29 | foreground: "hsl(var(--foreground))", 30 | primary: { 31 | DEFAULT: "hsl(var(--primary))", 32 | foreground: "hsl(var(--primary-foreground))", 33 | }, 34 | secondary: { 35 | DEFAULT: "hsl(var(--secondary))", 36 | foreground: "hsl(var(--secondary-foreground))", 37 | }, 38 | destructive: { 39 | DEFAULT: "hsl(var(--destructive))", 40 | foreground: "hsl(var(--destructive-foreground))", 41 | }, 42 | muted: { 43 | DEFAULT: "hsl(var(--muted))", 44 | foreground: "hsl(var(--muted-foreground))", 45 | }, 46 | accent: { 47 | DEFAULT: "hsl(var(--accent))", 48 | foreground: "hsl(var(--accent-foreground))", 49 | }, 50 | popover: { 51 | DEFAULT: "hsl(var(--popover))", 52 | foreground: "hsl(var(--popover-foreground))", 53 | }, 54 | card: { 55 | DEFAULT: "hsl(var(--card))", 56 | foreground: "hsl(var(--card-foreground))", 57 | }, 58 | }, 59 | borderRadius: { 60 | lg: "var(--radius)", 61 | md: "calc(var(--radius) - 2px)", 62 | sm: "calc(var(--radius) - 4px)", 63 | }, 64 | keyframes: { 65 | "accordion-down": { 66 | from: { height: "0" }, 67 | to: { height: "var(--radix-accordion-content-height)" }, 68 | }, 69 | "accordion-up": { 70 | from: { height: "var(--radix-accordion-content-height)" }, 71 | to: { height: "0" }, 72 | }, 73 | }, 74 | animation: { 75 | "accordion-down": "accordion-down 0.2s ease-out", 76 | "accordion-up": "accordion-up 0.2s ease-out", 77 | }, 78 | screens: { 79 | xxxs: "320px", 80 | xxs: "360px", 81 | xs: "480px", 82 | }, 83 | }, 84 | }, 85 | plugins: [require("tailwindcss-animate")], 86 | } satisfies Config; 87 | 88 | export default config; 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | }, 29 | "target": "ES2017" 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "deploymentEnabled": { 4 | "main": true 5 | } 6 | } 7 | } --------------------------------------------------------------------------------