├── LEARN.md
├── .eslintrc.json
├── app
├── favicon.ico
├── globals.css
├── (routes)
│ ├── product
│ │ ├── loading.tsx
│ │ └── [productId]
│ │ │ └── page.tsx
│ ├── category
│ │ ├── loading.tsx
│ │ └── [categoryId]
│ │ │ ├── components
│ │ │ ├── mobile-filters.tsx
│ │ │ └── filter.tsx
│ │ │ └── page.tsx
│ └── cart
│ │ ├── page.tsx
│ │ └── components
│ │ ├── cart-item.tsx
│ │ └── summary.tsx
├── layout.tsx
└── page.tsx
├── public
└── images
│ ├── image-1.jpg
│ ├── image-2.jpg
│ ├── nike-reactx.png
│ ├── nike-just-do-it.jpg
│ └── graph-paper.svg
├── postcss.config.js
├── fonts
├── BRFirma-font
│ ├── BRFirma-Bold.woff2
│ ├── BRFirma-Light.woff2
│ ├── BRFirma-Medium.woff2
│ ├── BRFirma-Regular.woff2
│ └── BRFirma-SemiBold.woff2
└── Futura-Condensed-Extra-Bold.woff2
├── lib
└── utils.ts
├── next.config.js
├── providers
├── toast-provider.tsx
└── modal-provider.tsx
├── components
├── ui
│ ├── no-results.tsx
│ ├── container.tsx
│ ├── skeleton.tsx
│ ├── currency.tsx
│ ├── icon-button.tsx
│ ├── button.tsx
│ ├── modal.tsx
│ └── product-card.tsx
├── product-list.tsx
├── billboard.tsx
├── preview-modal.tsx
├── navbar-actions.tsx
├── main-nav.tsx
├── gallery
│ ├── gallery-tab.tsx
│ └── index.tsx
├── navbar.tsx
├── info.tsx
└── footer.tsx
├── actions
├── get-sizes.tsx
├── get-colors.tsx
├── get-categories.tsx
├── get-product.tsx
├── get-category.tsx
├── get-billboard.tsx
└── get-products.tsx
├── .gitignore
├── hooks
├── use-preview-modal.ts
└── use-cart.tsx
├── README.md
├── tailwind.config.js
├── types.ts
├── tsconfig.json
├── package.json
└── LICENSE
/LEARN.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/public/images/image-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/public/images/image-1.jpg
--------------------------------------------------------------------------------
/public/images/image-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/public/images/image-2.jpg
--------------------------------------------------------------------------------
/public/images/nike-reactx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/public/images/nike-reactx.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/nike-just-do-it.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/public/images/nike-just-do-it.jpg
--------------------------------------------------------------------------------
/fonts/BRFirma-font/BRFirma-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/fonts/BRFirma-font/BRFirma-Bold.woff2
--------------------------------------------------------------------------------
/fonts/BRFirma-font/BRFirma-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/fonts/BRFirma-font/BRFirma-Light.woff2
--------------------------------------------------------------------------------
/fonts/BRFirma-font/BRFirma-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/fonts/BRFirma-font/BRFirma-Medium.woff2
--------------------------------------------------------------------------------
/fonts/Futura-Condensed-Extra-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/fonts/Futura-Condensed-Extra-Bold.woff2
--------------------------------------------------------------------------------
/fonts/BRFirma-font/BRFirma-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/fonts/BRFirma-font/BRFirma-Regular.woff2
--------------------------------------------------------------------------------
/fonts/BRFirma-font/BRFirma-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OgaDavid/nike-shop/HEAD/fonts/BRFirma-font/BRFirma-SemiBold.woff2
--------------------------------------------------------------------------------
/lib/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 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ["res.cloudinary.com"],
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/providers/toast-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Toaster } from "react-hot-toast";
4 |
5 | const ToastProvider = () => {
6 | return ;
7 | };
8 |
9 | export default ToastProvider;
10 |
--------------------------------------------------------------------------------
/components/ui/no-results.tsx:
--------------------------------------------------------------------------------
1 | const NoResults = () => {
2 | return (
3 |
4 | No Results Found!
5 |
6 | );
7 | }
8 |
9 | export default NoResults;
--------------------------------------------------------------------------------
/actions/get-sizes.tsx:
--------------------------------------------------------------------------------
1 | import { Size } from "@/types";
2 |
3 | const URL = `${process.env.NEXT_PUBLIC_API_URL}/sizes`
4 |
5 | const getSizes = async (): Promise => {
6 | const res = await fetch(URL);
7 |
8 | return res.json();
9 | }
10 |
11 | export default getSizes
--------------------------------------------------------------------------------
/actions/get-colors.tsx:
--------------------------------------------------------------------------------
1 | import { Color } from "@/types";
2 |
3 | const URL = `${process.env.NEXT_PUBLIC_API_URL}/colors`
4 |
5 | const getColors = async (): Promise => {
6 | const res = await fetch(URL);
7 |
8 | return res.json();
9 | }
10 |
11 | export default getColors
--------------------------------------------------------------------------------
/actions/get-categories.tsx:
--------------------------------------------------------------------------------
1 | import { Category } from "@/types";
2 |
3 | const URL = `${process.env.NEXT_PUBLIC_API_URL}/categories`
4 |
5 | const getCategories = async (): Promise => {
6 | const res = await fetch(URL);
7 |
8 | return res.json();
9 | }
10 |
11 | export default getCategories
--------------------------------------------------------------------------------
/actions/get-product.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from "@/types";
2 |
3 | const URL = `${process.env.NEXT_PUBLIC_API_URL}/products`;
4 |
5 | const getProduct = async ( id: string ): Promise => {
6 | const res = await fetch(`${URL}/${id}`);
7 |
8 | return res.json();
9 | };
10 |
11 | export default getProduct;
12 |
--------------------------------------------------------------------------------
/actions/get-category.tsx:
--------------------------------------------------------------------------------
1 | import { Category } from "@/types";
2 |
3 | const URL = `${process.env.NEXT_PUBLIC_API_URL}/categories`;
4 |
5 | const getCategory = async ( id: string ): Promise => {
6 | const res = await fetch(`${URL}/${id}`);
7 |
8 | return res.json();
9 | };
10 |
11 | export default getCategory;
12 |
--------------------------------------------------------------------------------
/actions/get-billboard.tsx:
--------------------------------------------------------------------------------
1 | import { Billboard } from "@/types";
2 |
3 | const URL = `${process.env.NEXT_PUBLIC_API_URL}/billboards`;
4 |
5 | const getBillboard = async ( id: string ): Promise => {
6 | const res = await fetch(`${URL}/${id}`);
7 |
8 | return res.json();
9 | };
10 |
11 | export default getBillboard;
12 |
--------------------------------------------------------------------------------
/components/ui/container.tsx:
--------------------------------------------------------------------------------
1 | interface ContainerProps {
2 | children: React.ReactNode
3 | }
4 |
5 | const Container: React.FC = ({ children }) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
13 | export default Container;
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | const Skeleton = ({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) => {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export default Skeleton;
--------------------------------------------------------------------------------
/providers/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import PreviewModal from "@/components/preview-modal";
4 | import { useEffect, useState } from "react";
5 |
6 | const ModalProvider = () => {
7 | const [isMounted, setisMounted] = useState(false);
8 |
9 | useEffect(() => {
10 | setisMounted(true);
11 | }, []);
12 |
13 | if (!isMounted) {
14 | return null;
15 | }
16 | return (
17 | <>
18 |
19 | >
20 | );
21 | };
22 |
23 | export default ModalProvider;
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 | .env
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/hooks/use-preview-modal.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { Product } from "@/types";
3 |
4 | interface PreviewModalStore {
5 | isOpen: boolean;
6 | data?: Product;
7 | onOpen: (data: Product) => void;
8 | onClose: () => void;
9 | }
10 |
11 | const usePreviewModal = create((set) => ({
12 | isOpen: false,
13 | data: undefined,
14 | onOpen: (data: Product) => set({ data, isOpen: true }),
15 | onClose: () => set({ isOpen: false }),
16 | }));
17 |
18 | export default usePreviewModal;
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | `PS: The hosted link does not work because planetscale discontinued their hobby/free plan. I can't export the data to migrate to another database like railway or another mySQL database because i have to add a valid credit card and pay about $39 to wake the database before i can export.`
2 |
3 | # Watch Demo Here ✨
4 | Click to watch the video
5 |
6 | [](https://vimeo.com/1029679616)
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | .scrollbar-hide::-webkit-scrollbar {
12 | display: none;
13 | }
14 |
15 | .social-icons {
16 | color: '#888888'
17 | }
18 |
19 | .social-icons:hover {
20 | color: white;
21 | cursor: pointer;
22 | transition: all .3s ease-in-out;
23 | }
24 |
25 | .graph {
26 | background-image: url('/images/graph-paper.svg');
27 | }
28 |
29 | .subtle {
30 | background: rgba(255, 255, 255, 0.7);
31 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic':
13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | }
19 |
--------------------------------------------------------------------------------
/components/ui/currency.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | export const Formatter = new Intl.NumberFormat("en-NG", {
6 | style: "currency",
7 | currency: "NGN",
8 | });
9 |
10 | interface CurrencyProps {
11 | value: string | number;
12 | }
13 |
14 | const Currency: React.FC = ({ value }) => {
15 | const [isMounted, setIsMounted] = useState(false);
16 |
17 | useEffect(() => {
18 | setIsMounted(true);
19 | }, []);
20 |
21 | if (!isMounted) {
22 | return null;
23 | }
24 | return {Formatter.format(Number(value))}
;
25 | };
26 |
27 | export default Currency;
28 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | export interface Billboard {
2 | id: string;
3 | label: string;
4 | imageUrl: string;
5 | }
6 |
7 | export interface Category {
8 | id: string;
9 | name: string;
10 | billboard: Billboard;
11 | }
12 |
13 | export interface Image {
14 | id: string;
15 | url: string;
16 | }
17 |
18 | export interface Size {
19 | id: string;
20 | name: string;
21 | value: string;
22 | }
23 | export interface Color {
24 | id: string;
25 | name: string;
26 | value: string;
27 | }
28 |
29 | export interface Product {
30 | id: string;
31 | category: Category;
32 | description: string,
33 | name: string;
34 | price: string;
35 | size: Size;
36 | color: Color;
37 | images: Image[];
38 | }
39 |
--------------------------------------------------------------------------------
/components/ui/icon-button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { MouseEventHandler } from "react";
3 |
4 | interface IconButtonProps {
5 | onClick?: MouseEventHandler | undefined;
6 | icon: React.ReactElement;
7 | className?: string;
8 | }
9 |
10 | const IconButton: React.FC = ({
11 | onClick,
12 | icon,
13 | className,
14 | }) => {
15 | return (
16 |
23 | {icon}
24 |
25 | );
26 | };
27 |
28 | export default IconButton;
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/components/product-list.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from "@/types";
2 | import NoResults from "@/components/ui/no-results";
3 | import ProductCard from "./ui/product-card";
4 |
5 | interface ProductListProps {
6 | title: string;
7 | items: Product[];
8 | }
9 |
10 | const ProductList: React.FC = ({ title, items }) => {
11 | return (
12 |
13 |
{title}
14 | { items.length === 0 &&
}
15 |
16 | {items.map((item) => (
17 |
18 | ))}
19 |
20 |
21 | );
22 | };
23 |
24 | export default ProductList;
25 |
--------------------------------------------------------------------------------
/actions/get-products.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from "@/types";
2 | import qs from 'query-string'
3 |
4 | const URL = `${process.env.NEXT_PUBLIC_API_URL}/products`;
5 |
6 | interface Query {
7 | categoryId?: string;
8 | colorId?: string;
9 | sizeId?: string;
10 | description?: string;
11 | isFeatured?: boolean;
12 | }
13 |
14 | const getProducts = async (query: Query): Promise => {
15 | const url = qs.stringifyUrl({
16 | url: URL,
17 | query: {
18 | description: query.description,
19 | colorId: query.colorId,
20 | sizeId: query.sizeId,
21 | categoryId: query.categoryId,
22 | isFeatured: query.isFeatured
23 | }
24 | });
25 |
26 | const res = await fetch(url);
27 |
28 | return res.json();
29 | };
30 |
31 | export default getProducts;
32 |
--------------------------------------------------------------------------------
/components/billboard.tsx:
--------------------------------------------------------------------------------
1 | import { Billboard as BillboardType } from "@/types";
2 |
3 | interface BillboardProps {
4 | data: BillboardType;
5 | }
6 |
7 | export const Billboard: React.FC = ({ data }) => {
8 | return (
9 |
10 |
14 |
15 |
16 | {data.label}
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sneaker-head-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@headlessui/react": "^1.7.15",
13 | "@types/node": "20.4.0",
14 | "@types/react": "18.2.14",
15 | "@types/react-dom": "18.2.6",
16 | "autoprefixer": "10.4.14",
17 | "axios": "^1.4.0",
18 | "clsx": "^1.2.1",
19 | "eslint": "8.44.0",
20 | "eslint-config-next": "13.4.9",
21 | "lucide-react": "^0.258.0",
22 | "next": "13.4.9",
23 | "postcss": "8.4.25",
24 | "query-string": "^8.1.0",
25 | "react": "18.2.0",
26 | "react-dom": "18.2.0",
27 | "react-hot-toast": "^2.4.1",
28 | "tailwind-merge": "^1.13.2",
29 | "tailwindcss": "3.3.2",
30 | "typescript": "5.1.6",
31 | "zustand": "^4.3.9"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { forwardRef } from "react";
3 |
4 | export interface ButtonProps
5 | extends React.ButtonHTMLAttributes {}
6 |
7 | const Button = forwardRef(
8 | ({ className, children, disabled, type = "button", ...props }, ref) => {
9 | return (
10 |
32 | {children}
33 |
34 | );
35 | }
36 | );
37 |
38 | Button.displayName = "Button";
39 |
40 | export default Button;
41 |
--------------------------------------------------------------------------------
/components/preview-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import usePreviewModal from "@/hooks/use-preview-modal";
4 | import Modal from "./ui/modal";
5 | import Gallery from "./gallery";
6 | import Info from "./info";
7 |
8 | const PreviewModal = () => {
9 |
10 | const previewModal = usePreviewModal();
11 | const product = usePreviewModal((state) => state.data)
12 |
13 | if (!product) {
14 | return null
15 | }
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 |
31 | export default PreviewModal;
--------------------------------------------------------------------------------
/components/navbar-actions.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Button from "@/components/ui/button";
4 | import useCart from "@/hooks/use-cart";
5 | import { ShoppingBag } from "lucide-react";
6 | import { useRouter } from "next/navigation";
7 | import { useEffect, useState } from "react";
8 |
9 | const NavbarActions = () => {
10 | const [isMounted, setIsMounted] = useState(false);
11 |
12 | useEffect(() => {
13 | setIsMounted(true);
14 | }, []);
15 |
16 | const cart = useCart();
17 | const router = useRouter();
18 |
19 | if (!isMounted) {
20 | return null;
21 | }
22 | return (
23 |
24 | router.push("/cart")} className="flex items-center rounded-full bg-black px-4 py-2">
25 |
26 |
27 | {cart.items.length}
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default NavbarActions;
35 |
--------------------------------------------------------------------------------
/app/(routes)/product/loading.tsx:
--------------------------------------------------------------------------------
1 | import Container from "@/components/ui/container";
2 | import Skeleton from "@/components/ui/skeleton";
3 |
4 | const Loading = () => {
5 | return (
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default Loading;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 David oga
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 |
--------------------------------------------------------------------------------
/components/main-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { Category } from "@/types";
5 | import Link from "next/link";
6 | import { usePathname } from "next/navigation";
7 |
8 | interface MainNavProps {
9 | data: Category[];
10 | }
11 |
12 | const MainNav: React.FC = ({ data }) => {
13 | const pathName = usePathname();
14 |
15 | const routes = data.map((route) => ({
16 | href: `/category/${route.id}`,
17 | label: route.name,
18 | active: pathName === `/category/${route.id}`,
19 | }));
20 | return (
21 |
22 | {routes.map((route) => (
23 |
31 | {route.label}
32 |
33 | ))}
34 |
35 | );
36 | };
37 |
38 | export default MainNav;
39 |
--------------------------------------------------------------------------------
/components/gallery/gallery-tab.tsx:
--------------------------------------------------------------------------------
1 | import NextImage from "next/image";
2 | import { Tab } from "@headlessui/react";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { Image } from "@/types";
6 |
7 | interface GalleryTabProps {
8 | image: Image;
9 | }
10 |
11 | const GalleryTab: React.FC = ({
12 | image
13 | }) => {
14 | return (
15 |
18 | {({ selected }) => (
19 |
20 |
21 |
27 |
28 |
34 |
35 | )}
36 |
37 | );
38 | }
39 |
40 | export default GalleryTab;
--------------------------------------------------------------------------------
/app/(routes)/category/loading.tsx:
--------------------------------------------------------------------------------
1 | import Container from "@/components/ui/container";
2 | import Skeleton from "@/components/ui/skeleton";
3 |
4 | const Loading = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default Loading;
--------------------------------------------------------------------------------
/hooks/use-cart.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { toast } from "react-hot-toast";
3 | import { persist, createJSONStorage } from "zustand/middleware";
4 |
5 | import { Product } from "@/types";
6 |
7 | interface CartStore {
8 | items: Product[];
9 | addItem: (data: Product) => void;
10 | removeItem: (id: string) => void;
11 | removeAll: () => void;
12 | }
13 |
14 | const useCart = create(
15 | persist(
16 | (set, get) => ({
17 | items: [],
18 | addItem: (data: Product) => {
19 | const currentItems = get().items;
20 | const existingItem = currentItems.find((item) => item.id === data.id);
21 |
22 | if (existingItem) {
23 | return toast.error("Item already in cart.");
24 | }
25 |
26 | set({ items: [...get().items, data] });
27 | toast.success("Item added to cart.");
28 | },
29 | removeItem: (id: string) => {
30 | set({ items: [...get().items.filter((item) => item.id !== id)] });
31 | toast.success("Item removed from cart.");
32 | },
33 | removeAll: () => set({ items: [] }),
34 | }),
35 | {
36 | name: "cart-storage",
37 | storage: createJSONStorage(() => localStorage),
38 | }
39 | )
40 | );
41 |
42 | export default useCart;
43 |
--------------------------------------------------------------------------------
/components/gallery/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import NextImage from "next/image";
4 | import { Tab } from "@headlessui/react";
5 |
6 | import { Image } from "@/types";
7 |
8 | import GalleryTab from "./gallery-tab";
9 |
10 | interface GalleryProps {
11 | images: Image[];
12 | }
13 |
14 | const Gallery: React.FC = ({
15 | images = []
16 | }) => {
17 | return (
18 |
19 |
20 |
21 | {images.map((image) => (
22 |
23 | ))}
24 |
25 |
26 |
27 | {images.map((image) => (
28 |
29 |
30 |
36 |
37 |
38 | ))}
39 |
40 |
41 | );
42 | }
43 |
44 | export default Gallery;
--------------------------------------------------------------------------------
/app/(routes)/cart/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import Container from "@/components/ui/container";
6 | import useCart from "@/hooks/use-cart";
7 |
8 | import CartItem from "./components/cart-item";
9 | import Summary from "./components/summary";
10 |
11 | export const revalidate = 0;
12 |
13 | const CartPage = () => {
14 | const [isMounted, setIsMounted] = useState(false);
15 | const cart = useCart();
16 |
17 | useEffect(() => {
18 | setIsMounted(true);
19 | }, []);
20 |
21 | if (!isMounted) {
22 | return null;
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 |
Shopping Cart
30 |
31 |
32 | {cart.items.length === 0 && (
33 |
No items added to cart.
34 | )}
35 |
36 | {cart.items.map((item) => (
37 |
38 | ))}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default CartPage;
50 |
--------------------------------------------------------------------------------
/public/images/graph-paper.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/(routes)/product/[productId]/page.tsx:
--------------------------------------------------------------------------------
1 | import getProduct from "@/actions/get-product";
2 | import getProducts from "@/actions/get-products";
3 | import Gallery from "@/components/gallery";
4 | import Info from "@/components/info";
5 | import ProductList from "@/components/product-list";
6 | import Container from "@/components/ui/container";
7 |
8 | interface ProductPageProps {
9 | params: {
10 | productId: string;
11 | };
12 | }
13 |
14 | export const revalidate = 0;
15 |
16 | const ProductPage: React.FC = async ({ params }) => {
17 | const product = await getProduct(params.productId);
18 |
19 | const Products = await getProducts({
20 | categoryId: product.category.id,
21 | });
22 |
23 | // remove product selected from suggested products, randomize the suggested products and then reduce to 8
24 | const suggestedProducts = await Products.filter(
25 | (item) => item.id !== params.productId
26 | )
27 | .sort(() => Math.random() - 0.5)
28 | .slice(0, 8);
29 |
30 | if (!product) {
31 | return null;
32 | }
33 | return (
34 |
48 | );
49 | };
50 |
51 | export default ProductPage;
52 |
--------------------------------------------------------------------------------
/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import Container from "@/components/ui/container";
2 | import Link from "next/link";
3 | import MainNav from "@/components/main-nav";
4 | import getCategories from "@/actions/get-categories";
5 | import NavbarActions from "@/components/navbar-actions";
6 |
7 | const Navbar = async () => {
8 | const categories = await getCategories();
9 | return (
10 |
11 |
12 |
13 |
18 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default Navbar;
45 |
--------------------------------------------------------------------------------
/app/(routes)/cart/components/cart-item.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { toast } from "react-hot-toast";
3 | import { X } from "lucide-react";
4 |
5 | import IconButton from "@/components/ui/icon-button";
6 | import Currency from "@/components/ui/currency";
7 | import useCart from "@/hooks/use-cart";
8 | import { Product } from "@/types";
9 |
10 | interface CartItemProps {
11 | data: Product;
12 | }
13 |
14 | const CartItem: React.FC = ({ data }) => {
15 | const cart = useCart();
16 |
17 | const onRemove = () => {
18 | cart.removeItem(data.id);
19 | };
20 |
21 | return (
22 |
23 |
24 |
30 |
31 |
32 |
33 | } />
34 |
35 |
36 |
39 |
40 |
41 |
{data.color.name}
42 |
43 | {data.size.name}
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default CartItem;
54 |
--------------------------------------------------------------------------------
/app/(routes)/category/[categoryId]/components/mobile-filters.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Button from "@/components/ui/button";
4 | import IconButton from "@/components/ui/icon-button";
5 | import { Color, Size } from "@/types";
6 | import { Dialog } from "@headlessui/react";
7 | import { Plus, X } from "lucide-react";
8 | import { useState } from "react";
9 | import Filter from "./filter";
10 |
11 | interface MobileFiltersProps {
12 | sizes: Size[];
13 | colors: Color[];
14 | }
15 |
16 | const MobileFilters: React.FC = ({ sizes, colors }) => {
17 | const [open, setOpen] = useState(false);
18 |
19 | const onOpen = () => setOpen(true);
20 | const onClose = () => setOpen(false);
21 |
22 | return (
23 | <>
24 |
25 | Filters
26 |
27 |
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 | } />
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | >
51 | );
52 | };
53 |
54 | export default MobileFilters;
55 |
--------------------------------------------------------------------------------
/components/ui/modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Dialog, Transition } from "@headlessui/react";
4 | import { Fragment } from "react";
5 | import IconButton from "./icon-button";
6 | import { X } from "lucide-react";
7 |
8 | interface ModalProps {
9 | open: boolean;
10 | onClose: () => void;
11 | children: React.ReactNode;
12 | }
13 |
14 | const Modal: React.FC = ({ open, onClose, children }) => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
30 |
31 |
32 |
33 | } />
34 |
35 | {children}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default Modal;
47 |
--------------------------------------------------------------------------------
/components/info.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Product } from "@/types";
4 | import Currency from "./ui/currency";
5 | import Button from "./ui/button";
6 | import { ShoppingCart } from "lucide-react";
7 | import useCart from "@/hooks/use-cart";
8 | import { MouseEventHandler } from "react";
9 |
10 | interface InfoProps {
11 | data: Product;
12 | }
13 |
14 | const Info: React.FC = ({ data }) => {
15 | const cart = useCart();
16 |
17 | const onAddToCart: MouseEventHandler = (event) => {
18 | event.stopPropagation();
19 |
20 | cart.addItem(data);
21 | };
22 | return (
23 |
24 |
25 | {data.name}
26 |
27 |
32 |
33 |
{data.description}
34 |
35 |
36 |
Size:
37 |
{data.size.name}
38 |
39 |
46 |
47 |
48 |
49 | Add to Cart
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default Info;
58 |
--------------------------------------------------------------------------------
/app/(routes)/category/[categoryId]/components/filter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import qs from "query-string";
4 | import { useRouter, useSearchParams } from "next/navigation";
5 |
6 | import Button from "@/components/ui/button";
7 | import { cn } from "@/lib/utils";
8 | import { Color, Size } from "@/types";
9 |
10 | interface FilterProps {
11 | data: (Size | Color)[];
12 | name: string;
13 | valueKey: string;
14 | }
15 |
16 | const Filter: React.FC = ({ data, name, valueKey }) => {
17 | const searchParams = useSearchParams();
18 | const router = useRouter();
19 |
20 | const selectedValue = searchParams.get(valueKey);
21 |
22 | const onClick = (id: string) => {
23 | const current = qs.parse(searchParams.toString());
24 |
25 | const query = {
26 | ...current,
27 | [valueKey]: id,
28 | };
29 |
30 | if (current[valueKey] === id) {
31 | query[valueKey] = null;
32 | }
33 |
34 | const url = qs.stringifyUrl(
35 | {
36 | url: window.location.href,
37 | query,
38 | },
39 | { skipNull: true }
40 | );
41 |
42 | router.push(url);
43 | };
44 |
45 | return (
46 |
47 |
{name}
48 |
49 |
50 | {data.map((filter) => (
51 |
52 | onClick(filter.id)}
58 | >
59 | {filter.name}
60 |
61 |
62 | ))}
63 |
64 |
65 | );
66 | };
67 |
68 | export default Filter;
69 |
--------------------------------------------------------------------------------
/app/(routes)/cart/components/summary.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import axios from "axios";
4 | import { useEffect } from "react";
5 | import { useSearchParams } from "next/navigation";
6 |
7 | import Button from "@/components/ui/button";
8 | import Currency from "@/components/ui/currency";
9 | import useCart from "@/hooks/use-cart";
10 | import { toast } from "react-hot-toast";
11 |
12 | const Summary = () => {
13 | const searchParams = useSearchParams();
14 | const items = useCart((state) => state.items);
15 | const removeAll = useCart((state) => state.removeAll);
16 |
17 | useEffect(() => {
18 | if (searchParams.get("success")) {
19 | toast.success("Payment completed.");
20 | removeAll();
21 | }
22 |
23 | if (searchParams.get("canceled")) {
24 | toast.error("Something went wrong.");
25 | }
26 | }, [searchParams, removeAll]);
27 |
28 | const totalPrice = items.reduce((total, item) => {
29 | return total + Number(item.price);
30 | }, 0);
31 |
32 | const onCheckout = async () => {
33 | const response = await axios.post(
34 | `${process.env.NEXT_PUBLIC_API_URL}/checkout`,
35 | {
36 | productIds: items.map((item) => item.id),
37 | }
38 | );
39 |
40 | window.location = response.data.url;
41 | };
42 |
43 | return (
44 |
45 |
Order summary
46 |
47 |
48 |
Order total
49 |
50 |
51 |
52 |
57 | Checkout
58 |
59 |
60 | );
61 | };
62 |
63 | export default Summary;
64 |
--------------------------------------------------------------------------------
/app/(routes)/category/[categoryId]/page.tsx:
--------------------------------------------------------------------------------
1 | import getCategory from "@/actions/get-category";
2 | import getColors from "@/actions/get-colors";
3 | import getProducts from "@/actions/get-products";
4 | import getSizes from "@/actions/get-sizes";
5 | import { Billboard } from "@/components/billboard";
6 | import Container from "@/components/ui/container";
7 | import Filter from "./components/filter";
8 | import NoResults from "@/components/ui/no-results";
9 | import ProductCard from "@/components/ui/product-card";
10 | import MobileFilters from "./components/mobile-filters";
11 |
12 | export const revalidate = 0;
13 |
14 | interface CategoryPageProps {
15 | params: {
16 | categoryId: string;
17 | };
18 | searchParams: {
19 | colorId: string;
20 | sizeId: string;
21 | };
22 | }
23 |
24 | const CategoryPage: React.FC = async ({
25 | params,
26 | searchParams,
27 | }) => {
28 | const products = await getProducts({
29 | categoryId: params.categoryId,
30 | colorId: searchParams.colorId,
31 | sizeId: searchParams.sizeId,
32 | });
33 | const sizes = await getSizes();
34 | const colors = await getColors();
35 | const category = await getCategory(params.categoryId);
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {products.length === 0 &&
}
50 |
51 | {products.map((item) => (
52 |
53 | ))}
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default CategoryPage;
64 |
--------------------------------------------------------------------------------
/components/ui/product-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { MouseEventHandler } from "react";
5 | import { Expand, ShoppingCart } from "lucide-react";
6 | import { useRouter } from "next/navigation";
7 |
8 | import Currency from "@/components/ui/currency";
9 | import IconButton from "@/components/ui/icon-button";
10 | import usePreviewModal from "@/hooks/use-preview-modal";
11 | import useCart from "@/hooks/use-cart";
12 | import { Product } from "@/types";
13 |
14 | interface ProductCard {
15 | data: Product;
16 | }
17 |
18 | const ProductCard: React.FC = ({ data }) => {
19 | const previewModal = usePreviewModal();
20 | const cart = useCart();
21 | const router = useRouter();
22 |
23 | const handleClick = () => {
24 | router.push(`/product/${data?.id}`);
25 | };
26 |
27 | const onPreview: MouseEventHandler = (event) => {
28 | event.stopPropagation();
29 |
30 | previewModal.onOpen(data);
31 | };
32 |
33 | const onAddToCart: MouseEventHandler = (event) => {
34 | event.stopPropagation();
35 |
36 | cart.addItem(data);
37 | };
38 |
39 | return (
40 |
44 | {/* Image & actions */}
45 |
46 |
52 |
53 |
54 | }
57 | />
58 | }
61 | />
62 |
63 |
64 |
65 | {/* Description */}
66 |
67 |
{data.name}
68 |
{data.category?.name}
69 |
70 | {/* Price & Reiew */}
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default ProductCard;
79 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "@/components/footer";
2 | import "./globals.css";
3 | import type { Metadata } from "next";
4 | import localFont from "next/font/local";
5 | import Navbar from "@/components/navbar";
6 | import ModalProvider from "@/providers/modal-provider";
7 | import ToastProvider from "@/providers/toast-provider";
8 |
9 |
10 | const BRFrima = localFont({
11 | src: [
12 | {
13 | path: "../fonts/BRFirma-font/BRFirma-Light.woff2",
14 | weight: "100",
15 | style: "normal",
16 | },
17 | {
18 | path: "../fonts/BRFirma-font/BRFirma-Regular.woff2",
19 | weight: "200",
20 | style: "normal",
21 | },
22 | {
23 | path: "../fonts/BRFirma-font/BRFirma-Medium.woff2",
24 | weight: "300",
25 | style: "normal",
26 | },
27 | {
28 | path: "../fonts/BRFirma-font/BRFirma-SemiBold.woff2",
29 | weight: "400",
30 | style: "normal",
31 | },
32 | {
33 | path: "../fonts/BRFirma-font/BRFirma-Bold.woff2",
34 | weight: "500",
35 | style: "normal",
36 | },
37 | ],
38 | });
39 |
40 | export const metadata: Metadata = {
41 | title: {
42 | default: "Nike. Just Do It. Nike.com",
43 | template: "%s | Nike",
44 | },
45 | keywords: [
46 | "Nike",
47 | "Air Force 1",
48 | "Jordans",
49 | "SB Dunks",
50 | "Air Max",
51 | "New arrivals",
52 | "Dunks and Blazers",
53 | "athletics",
54 | "Nike.com",
55 | "Converse",
56 | ],
57 | description:
58 | "Nike delivers innovative products, experiences and services to inspire athletes.",
59 | applicationName: "Nike.com",
60 | authors: [{ name: "Nike" }],
61 | openGraph: {
62 | title: "Nike. Just Do It",
63 | description:
64 | "Nike delivers innovative products, experiences and services to inspire athletes.",
65 | url: "https://nike.com",
66 | siteName: "",
67 | images: [
68 | {
69 | url: "https://c.static-nike.com/a/images/w_1920,c_limit/bzl2wmsfh7kgdkufrrjq/image.jpg",
70 | },
71 | ],
72 | locale: "en-US",
73 | type: "website",
74 | },
75 | twitter: {
76 | card: "summary_large_image",
77 | title: "NIke. Just Do It.",
78 | description:
79 | "Nike delivers innovative products, experiences and services to inspire athletes.",
80 | site: "@nike",
81 | creator: "@nike",
82 | images: [
83 | "https://c.static-nike.com/a/images/w_1920,c_limit/bzl2wmsfh7kgdkufrrjq/image.jpg",
84 | ],
85 | },
86 | category: "e-commerce",
87 | };
88 |
89 | export default function RootLayout({
90 | children,
91 | }: {
92 | children: React.ReactNode;
93 | }) {
94 | return (
95 |
96 |
97 |
98 |
99 |
100 | {children}
101 |
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import getProducts from "@/actions/get-products";
2 | import ProductList from "@/components/product-list";
3 | import Container from "@/components/ui/container";
4 | import { cn } from "@/lib/utils";
5 | import Image from "next/image";
6 | import localFont from "next/font/local";
7 | import Button from "@/components/ui/button";
8 | import { ArrowRight } from "lucide-react";
9 | import { ArrowRightCircle } from "lucide-react";
10 |
11 | const FuturaCondensedExtraBold = localFont({
12 | src: "../fonts/Futura-Condensed-Extra-Bold.woff2",
13 | display: "swap",
14 | });
15 |
16 | export const revalidate = 0;
17 |
18 | const HomePage = async () => {
19 | const products = await getProducts({ isFeatured: true });
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 | NIKE SHOP
35 |
36 |
37 | Welcome to the ultimate destination for athletic excellence,
38 | where dreams take flight and champions are made. Step into a
39 | world of limitless possibilities with Nike.
40 |
41 |
42 |
43 |
44 | Shop Now
45 |
46 |
47 |
48 |
52 | Join our Newsletter
53 |
54 |
55 |
60 | Subscribe
61 |
62 |
63 |
64 |
65 |
73 |
74 |
75 |
76 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default HomePage;
85 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | const Links = [
4 | {
5 | name: "Guides",
6 | href: "",
7 | },
8 | {
9 | name: "Terms of Sale",
10 | href: "",
11 | },
12 | {
13 | name: "Terms of Use",
14 | href: "",
15 | },
16 | {
17 | name: "Nike Privacy Policy",
18 | href: "",
19 | },
20 | {
21 | name: "Your Privacy Choices",
22 | href: "",
23 | },
24 | ];
25 |
26 | const Footer = () => {
27 | const year = new Date().getFullYear();
28 |
29 | return (
30 |
31 |
32 |
33 |
41 |
45 |
46 |
54 |
58 |
59 |
67 |
71 |
72 |
80 |
84 |
85 |
86 |
87 | {Links.map((link, idx) => (
88 |
93 | {link.name}
94 |
95 | ))}
96 |
97 |
98 |
99 | © {year} Nike Inc, All Rights Reserved
100 |
101 |
102 | Built by{" "}
103 |
107 | Oga David.
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default Footer;
115 |
--------------------------------------------------------------------------------