Fullstack web elektronického obchodu pomocí Nextjs, Supabase, prisma a chatwoot.🔥🔥

V tomto článku tutoriálu budeme vytvářet aplikaci SuperbaseEcommerce full-stack. Tato aplikace je jednoduše online nákupní web elektronického obchodu, kde mohou uživatelé procházet všechny produkty, nahrávat své vlastní produkty a dokonce si produkty kupovat (This functionality will be added in the next series of articles ). Je podobná aplikaci Amazon, ale je jednodušší, protože nebudeme implementovat žádné skutečné platební nebo přepravní postupy. Zde je živá ukázka finální verze aplikace. Takto by měla vaše aplikace vypadat po dokončení tohoto tutoriálu. Nebojte se s ním experimentovat, abyste získali představu o všech funkcích, které budeme implementovat.

Živá ukázka => https://supabase-ecommerce.vercel.app

V tomto tutoriálu se tedy naučíme, jak vytvořit tuto kompletní aplikaci s Next.js , framework pro reakce, NextAuth.js , pro implementaci ověřování bez hesla a OAuth, Supabase , pro uchování dat aplikace do databáze PostgreSQL a uchovávání mediálních souborů a informací a Prisma , pro usnadnění čtení a zápisu dat z a do databáze z naší aplikace.

Tento tutoriál k článku pokrývá mnoho témat a technických konceptů nezbytných k vytvoření moderní kompletní aplikace, i když je tato aplikace zjednodušenou verzí pokročilejšího webu elektronického obchodu, jako je Amazon. Měli byste být schopni používat všechny technologie popsané v tomto tutoriálu, včetně reagovat, nextjs, prisma, supabase a dalších, ale co je nejdůležitější, měli byste být schopni vytvořit jakoukoli full-stack aplikaci pomocí těchto technologií. Půjdete svou vlastní rychlostí a intenzitou a my vás po cestě povedeme. Po dokončení tohoto průvodce je cílem tohoto článku poskytnout vám nástroje a techniky, které budete potřebovat, abyste si podobnou aplikaci vytvořili sami. Jinak řečeno, tento tutoriál vás nejen naučí, jak tyto technologie používat. velmi podrobně, ale také vám poskytne správnou kombinaci principů a aplikací, které vám pomohou pochopit všechny klíčové koncepty, abyste mohli hrdě vytvářet své vlastní aplikace od nuly v další části tohoto článku.

Začněme s částí reakce a sestavme naši aplikaci. Prvním krokem je nainstalovat Node.js, pokud ještě není na vašem počítači. Přejděte tedy na oficiální web Node.js a stáhněte si nejnovější verzi. Node js je vyžadován pro použití správce balíčků uzlů, zkráceně npm. Nyní spusťte preferovaný editor kódu a přejděte do složky. Pro tento tutoriál k článku použijeme editor kódu VScode.

Nastavení projektu SupabaseEcommerce.

Tomuto projektu je věnováno úložiště Github, které se skládá ze tří větví. Klonujte SupabaseEcommerce-starter větev, abyste mohli začít.

Main větev obsahuje celý final zdrojový kód aplikace, takže naklonujte SupabaseEcommerce-starter větev, pokud chcete pokračovat spolu s tímto návodem.

git clone --branch SupabaseEcommerce-starter https://github.com/pramit-marattha/SupabaseEcommerce.git

Poté přejděte do klonovaného adresáře a před spuštěním Next.js nainstalujte závislosti vývojový server:

cd SupabaseEcommerce
yarn add all
yarn dev

Nyní můžete zkontrolovat, zda vše funguje správně na http://localhost:3000 a editaci pages/index.js a poté si v prohlížeči prohlédněte aktualizovaný výsledek. Další informace o použití create-next-app , můžete si prohlédnout dokumentaci k vytvoření další aplikace.

Obvykle trvá jen pár minut, než vše nastavíte. Takže pro tento projekt budeme používat yarn přidat balíčky do projektu, který za nás vše nainstaluje a nakonfiguruje, abychom mohli hned začít s vynikající startovací šablonou. Je čas spustit náš vývojový server, takže přejděte na SupabaseEcommerce složku a zadejte yarn add all a poté yarn dev a prohlížeč okamžitě otevře naši úvodní šablonu Next.js aplikace.

Struktura složek vaší aplikace by měla vypadat nějak takto.

Možná vás tedy zajímá zdroj obsahu. Pamatujte, že veškerý náš zdrojový kód je umístěn ve složce Stránky a reakce/další jej vloží do kořenového prvku div. Podívejme se na naši složku Stránky, která obsahuje některé soubory javascriptu a jednu složku API.

Než se ponoříme dál, pojďme vytvořit vstupní stránku pro náš web.

takže než vůbec začneme, musíte nainstalovat framer-motion knihovna.

Pojďme se ponořit a vytvořit krásně vypadající uživatelské rozhraní pro naši aplikaci elektronického obchodu, než začneme s integrační částí backendu. Začněme vytvořením vstupní stránky pro aplikaci a poté přejdeme k vytvoření produktové stránky pro aplikaci. Takže uvnitř components složku, vytvořte Layout komponentu a přidejte do ní následující kód. Tato komponenta je jednoduše základním rozvržením naší aplikace, která zahrnuje navigační lištu a nabídky a také funkcionalitu pro zobrazení registračního/přihlašovacího modu naší aplikace.

// components/Layout.js
import { Fragment, useState } from "react";
import { useRouter } from "next/router";
import Head from "next/head";
import Link from "next/link";
import Image from "next/image";
import PropTypes from "prop-types";
import AuthModal from "./AuthModal";
import { Menu, Transition } from "@headlessui/react";
import {
  HeartIcon,
  HomeIcon,
  LogoutIcon,
  PlusIcon,
  UserIcon,
  ShoppingCartIcon,
} from "@heroicons/react/outline";
import { ChevronDownIcon } from "@heroicons/react/solid";

const menuItems = [
  {
    label: "List a new home",
    icon: PlusIcon,
    href: "/list",
  },
  {
    label: "My homes",
    icon: HomeIcon,
    href: "/homes",
  },
  {
    label: "Favorites",
    icon: HeartIcon,
    href: "/favorites",
  },
  {
    label: "Logout",
    icon: LogoutIcon,
    onClick: () => null,
  },
];

const Layout = ({ children = null }) => {
  const router = useRouter();

  const [showModal, setShowModal] = useState(false);

  const user = null;
  const isLoadingUser = false;

  const openModal = () => setShowModal(true);
  const closeModal = () => setShowModal(false);

  return (
    <>
      <Head>
        <title>SupaaShop | A new way to shop!</title>
        <meta name="title" content="SupaaShopp" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="min-h-screen flex flex-col font-['Poppins'] bg-[linear-gradient(90deg, #161122 21px, transparent 1%) center, linear-gradient(#161122 21px, transparent 1%) center, #a799cc]">
        <header className="h-28 w-full shadow-lg">
          <div className="h-full container mx-auto">
            <div className="h-full px-5 flex justify-between items-center space-x-5">
              <Link href="/">
                <a className="flex items-center space-x-1">
                  <img
                    className="shrink-0 w-24 h-24 text-primary"
                    src="https://user-images.githubusercontent.com/37651620/158058874-6a86646c-c60e-4c39-bc6a-d81974afe635.png"
                    alt="Logo"
                  />
                  <span className="text-2xl font-semibold tracking-wide text-white">
                    <span className="text-3xl text-success">S</span>upabase
                    <span className="text-3xl text-success">E</span>commerce
                  </span>
                </a>
              </Link>
              <div className="flex items-center space-x-4">
                <Link href="/create">
                  <a className="ml-4 px-4 py-5 rounded-md bg-info text-primary hover:bg-primary hover:text-info focus:outline-none focus:ring-4 focus:ring-primaryfocus:ring-opacity-50  font-semibold transition">
                    Register shop !
                  </a>
                </Link>
                {isLoadingUser ? (
                  <div className="h-8 w-[75px] bg-gray-200 animate-pulse rounded-md" />
                ) : user ? (
                  <Menu as="div" className="relative z-50">
                    <Menu.Button className="flex items-center space-x-px group">
                      <div className="shrink-0 flex items-center justify-center rounded-full overflow-hidden relative bg-gray-200 w-9 h-9">
                        {user?.image ? (
                          <Image
                            src={user?.image}
                            alt={user?.name || "Avatar"}
                            layout="fill"
                          />
                        ) : (
                          <UserIcon className="text-gray-400 w-6 h-6" />
                        )}
                      </div>
                      <ChevronDownIcon className="w-5 h-5 shrink-0 text-gray-500 group-hover:text-current" />
                    </Menu.Button>
                    <Transition
                      as={Fragment}
                      enter="transition ease-out duration-100"
                      enterFrom="opacity-0 scale-95"
                      enterTo="opacity-100 scale-100"
                      leave="transition ease-in duration-75"
                      leaveFrom="opacity-100 scale-100"
                      leaveTo="opacity-0 scale-95"
                    >
                      <Menu.Items className="absolute right-0 w-72 overflow-hidden mt-1 divide-y divide-gray-100 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
                        <div className="flex items-center space-x-2 py-4 px-4 mb-2">
                          <div className="shrink-0 flex items-center justify-center rounded-full overflow-hidden relative bg-gray-200 w-9 h-9">
                            {user?.image ? (
                              <Image
                                src={user?.image}
                                alt={user?.name || "Avatar"}
                                layout="fill"
                              />
                            ) : (
                              <UserIcon className="text-gray-400 w-6 h-6" />
                            )}
                          </div>
                          <div className="flex flex-col truncate">
                            <span>{user?.name}</span>
                            <span className="text-sm text-gray-500">
                              {user?.email}
                            </span>
                          </div>
                        </div>
                        <div className="py-2">
                          {menuItems.map(
                            ({ label, href, onClick, icon: Icon }) => (
                              <div
                                key={label}
                                className="px-2 last:border-t last:pt-2 last:mt-2"
                              >
                                <Menu.Item>
                                  {href ? (
                                    <Link href={href}>
                                      <a className="flex items-center space-x-2 py-2 px-4 rounded-md hover:bg-gray-100">
                                        <Icon className="w-5 h-5 shrink-0 text-gray-500" />
                                        <span>{label}</span>
                                      </a>
                                    </Link>
                                  ) : (
                                    <button
                                      className="w-full flex items-center space-x-2 py-2 px-4 rounded-md hover:bg-gray-100"
                                      onClick={onClick}
                                    >
                                      <Icon className="w-5 h-5 shrink-0 text-gray-500" />
                                      <span>{label}</span>
                                    </button>
                                  )}
                                </Menu.Item>
                              </div>
                            )
                          )}
                        </div>
                      </Menu.Items>
                    </Transition>
                  </Menu>
                ) : (
                  <button
                    type="button"
                    onClick={openModal}
                    className="ml-4 px-4 py-5 rounded-md bg-info hover:bg-primary focus:outline-none focus:ring-4 focus:ring-primary focus:ring-opacity-50 text-primary hover:text-info font-extrabold transition"
                  >
                    Login
                  </button>
                )}
              </div>
            </div>
          </div>
        </header>

        <main className="flex-grow container mx-auto">
          <div className="px-4 py-12">
            {typeof children === "function" ? children(openModal) : children}
          </div>
        </main>

        <AuthModal show={showModal} onClose={closeModal} />
      </div>
    </>
  );
};

Layout.propTypes = {
  children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
};

export default Layout;

Po úspěšném vytvoření rozvržení pro aplikaci vytvoříme na vstupní stránce sekci „Hrdina“. Chcete-li tak učinit, jednoduše vložte následující kód do této sekce. V této sekci tedy přidáme obrázek napravo, velký textový nadpis a dvě tlačítka nalevo. Všimněte si, že náš projekt stylizujeme s absolutní silou tailwind css a framer-motion přidat do obrázku krásnou přechodovou animaci. Protože jsme již vytvořili tlačítka v naší úvodní šabloně, nebudete se muset starat o jejich vytvoření od začátku; místo toho je můžete jednoduše importovat z komponent a používat je.

// components/Hero.js
import React from "react";
import PrimaryButton from "@/components/PrimaryButton";
import SecondaryButton from "@/components/SecondaryButton";
import { motion } from "framer-motion";

const Hero = () => {
  return (
    <div className="max-w-6xl mx-auto py-12 flex flex-col md:flex-row space-y-8 md:space-y-0">
      <div className="w-full md:w-1/2 flex flex-col justify-center items-center">
        <div className="max-w-xs lg:max-w-md space-y-10 w-5/6 mx-auto md:w-full text-center md:text-left">
          <h1 className="font-primary font-extrabold text-white text-3xl sm:text-4xl md:text-5xl md:leading-tight">
            Shop <span className="text-success">whenever</span> and{" "}
            <span className="text-success">however</span> you want from,{" "}
            <span className="text-success">wherever</span> you are..{" "}
          </h1>
          <p className="font-secondary text-gray-500 text-base md:text-lg lg:text-xl">
            SuperbaseEcommerce improves and streamlines your shopping
            experience..
          </p>
          <div className="flex space-x-4">
            <PrimaryButton text="Register" link="/" />
            <SecondaryButton text="Let's Shop!" link="/products" />
          </div>
        </div>
      </div>
      <motion.div
        className="w-full md:w-1/2 transform scale-x-125 lg:scale-x-100"
        initial={{ opacity: 0, translateY: 60 }}
        animate={{ opacity: 1, translateY: 0 }}
        transition={{ duration: 0.8, translateY: 0 }}
      >
        <img
          alt="hero-img"
          src="./assets/shop.svg"
          className="mx-auto object-cover shadow rounded-tr-extraLarge rounded-bl-extraLarge w-full h-96 sm:h-112 md:h-120"
        />
      </motion.div>
    </div>
  );
};

export default Hero;

Nyní před opětovným spuštěním serveru naimportujte toto Hero komponentu do index.js a zabalte jej do komponenty Layout, abyste viděli změny, které jste provedli.

// index.js
import Layout from "@/components/Layout";
import Hero from "@/components/Hero";

export default function Home() {
  return (
    <Layout>
      <Hero />
    </Layout>
  );
}

Takto by měla vypadat vaše vstupní stránka.

Po dokončení s Hero Pokračujte a vytvořte ShopCards komponentu, kde jednoduše uvedeme seznam demo funkcí, které tato aplikace nabízí, a přidáme nějaké obrázky, takže váš konečný kód pro ShopCards komponenta by měla vypadat takto.

// components/ShopCards.js
import React, { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";

const ShopCards = () => {
  const [tab, setTab] = useState(1);

  const tabs = useRef(null);

  const heightFix = () => {
    if (tabs.current.children[tab]) {
      tabs.current.style.height =
        tabs.current.children[tab - 1].offsetHeight + "px";
    }
  };

  useEffect(() => {
    heightFix();
  }, [tab]);
  return (
    <section className="relative">
      <div
        className="absolute inset-0 pointer-events-none pb-26"
        aria-hidden="true"
      ></div>

      <div className="relative max-w-6xl mx-auto px-4 sm:px-6">
        <div className="pt-12 md:pt-20">
          <div className="max-w-3xl mx-auto text-center pb-12 md:pb-16">
            <h1 className="text-3xl mb-4">Features</h1>
            <p className="text-xl text-gray-500">
              List of features that SuperbaseEcommerce provides.
            </p>
          </div>
          <div className="relative max-w-6xl mx-auto px-4 sm:px-6">
            <div className="pt-12 md:pt-20">
              <div className="max-w-3xl mx-auto text-center pb-6 md:pb-16">
                <div className="" data-aos="zoom-y-out" ref={tabs}>
                  <motion.div
                    className="relative w-full h-full"
                    initial={{ opacity: 0, translateY: 60 }}
                    animate={{ opacity: 1, translateY: 0 }}
                    transition={{ duration: 0.8, translateY: 0 }}
                  >
                    <img
                      alt="hero-img"
                      src="./assets/webShop.svg"
                      className="mx-auto object-cover shadow rounded-tr-extraLarge rounded-bl-extraLarge w-full h-96 sm:h-112 md:h-120"
                    />
                  </motion.div>
                </div>
              </div>
            </div>
          </div>

          <div className="max-w-6xl mx-auto py-12 flex flex-col md:flex-row space-y-8 md:space-y-0">
            <div
              className="max-w-xl md:max-w-none md:w-full mx-auto md:col-span-7 lg:col-span-6 md:mt-6 pr-12"
              data-aos="fade-right"
            >
              <div className="md:pr-4 lg:pr-12 xl:pr-16 mb-8">
                <h3 className="h3 mb-3">All of our awesome features</h3>
                <p className="text-xl text-black"></p>
              </div>
              <div className="mb-8 md:mb-0">
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 1
                      ? "bg-white shadow-md border-success hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(1);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Register/Login Feature
                    </div>
                    <div className="text-gray-600">
                      User can login and save their products for later purchase.
                    </div>
                  </div>
                </a>
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 2
                      ? "bg-white shadow-md border-purple-200 hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(2);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Add to cart
                    </div>
                    <div className="text-gray-600">
                      User can add the products/items to their cart
                    </div>
                  </div>
                </a>
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 3
                      ? "bg-white shadow-md border-purple-200 hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(3);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Security
                    </div>
                    <div className="text-gray-600">
                      Hassle free secure login and registration process.
                    </div>
                  </div>
                </a>
                <a
                  className={`flex items-center text-lg p-5 rounded border transition duration-300 ease-in-out mb-3 ${
                    tab !== 4
                      ? "bg-white shadow-md border-purple-200 hover:shadow-lg"
                      : "bg-success border-transparent"
                  }`}
                  href="#0"
                  onClick={(e) => {
                    e.preventDefault();
                    setTab(4);
                  }}
                >
                  <div>
                    <div className="font-bold leading-snug tracking-tight mb-1 text-gray-600">
                      Personalized shops
                    </div>
                    <div className="text-gray-600">
                      User can create/register their very own shop and add their
                      own products.
                    </div>
                  </div>
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

export default ShopCards;

Před opětovným spuštěním serveru znovu naimportujte toto ShopCards komponentu do index.js a zabalte jej do Layout komponentu &pod Hero komponentu, abyste viděli změny, které jste provedli.

// index.js
import Layout from "@/components/Layout";
import Hero from "@/components/Hero";
import ShopCards from "@/components/ShopCards";

export default function Home() {
  return (
    <Layout>
      <Hero />
      <ShopCards />
    </Layout>
  );
}

Prozatím by vaše vstupní stránka měla vypadat takto.

Nakonec přidejte sekci Zápatí, takže vytvořte Footer komponentu a vložte do ní níže uvedený kód.

// components/Footer.js
import Link from "next/link";

const Footer = () => {
  return (
    <footer>
      <div className="max-w-6xl mx-auto px-4 sm:px-6 pt-10">
        <div className="sm:col-span-6 md:col-span-3 lg:col-span-3">
          <section>
            <div className="max-w-6xl mx-auto px-4 sm:px-6">
              <div className="pb-12 md:pb-20">
                <div
                  className="relative bg-success rounded py-10 px-8 md:py-16 md:px-12 shadow-2xl overflow-hidden"
                  data-aos="zoom-y-out"
                >
                  <div
                    className="absolute right-0 bottom-0 pointer-events-none hidden lg:block"
                    aria-hidden="true"
                  ></div>

                  <div className="relative flex flex-col lg:flex-row justify-between items-center">
                    <div className="text-center lg:text-left lg:max-w-xl">
                      <h6 className="text-gray-600 text-3xl font-medium mb-2">
                        Sign-up for the early access!{" "}
                      </h6>
                      <p className="text-gray-100 text-lg mb-6">
                        SuperbaseEcommerce improves and streamlines your
                        shopping experience.. !
                      </p>
                      <form className="w-full lg:w-auto">
                        <div className="flex flex-col sm:flex-row justify-center max-w-xs mx-auto sm:max-w-xl lg:mx-0">
                          <input
                            type="email"
                            className="w-full appearance-none bg-purple-100 border border-gray-700 focus:border-gray-600 rounded-sm px-4 py-3 mb-2 sm:mb-0 sm:mr-2 text-black placeholder-gray-500"
                            placeholder="Enter your email…"
                            aria-label="Enter your email…"
                          />
                          <a
                            className="btn text-white bg-info hover:bg-success shadow"
                            href="#"
                          >
                            Sign-Up!
                          </a>
                        </div>
                      </form>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </section>
        </div>
        <div className="md:flex md:items-center md:justify-between py-4 md:py-8 border-t-2 border-solid">
          <ul className="flex mb-4 md:order-1 md:ml-4 md:mb-0">
            <li>
              <Link
                href="#"
                className="flex justify-center items-center text-blue-400 hover:text-gray-900 bg-blue-100 hover:bg-white-100 rounded-full shadow transition duration-150 ease-in-out"
                aria-label="Twitter"
              >
                <svg
                  className="w-8 h-8 fill-current "
                  viewBox="0 0 32 32"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path d="M24 11.5c-.6.3-1.2.4-1.9.5.7-.4 1.2-1 1.4-1.8-.6.4-1.3.6-2.1.8-.6-.6-1.5-1-2.4-1-1.7 0-3.2 1.5-3.2 3.3 0 .3 0 .5.1.7-2.7-.1-5.2-1.4-6.8-3.4-.3.5-.4 1-.4 1.7 0 1.1.6 2.1 1.5 2.7-.5 0-1-.2-1.5-.4 0 1.6 1.1 2.9 2.6 3.2-.3.1-.6.1-.9.1-.2 0-.4 0-.6-.1.4 1.3 1.6 2.3 3.1 2.3-1.1.9-2.5 1.4-4.1 1.4H8c1.5.9 3.2 1.5 5 1.5 6 0 9.3-5 9.3-9.3v-.4c.7-.5 1.3-1.1 1.7-1.8z" />
                </svg>
              </Link>
            </li>
            <li className="ml-4">
              <Link
                href="#"
                className="flex justify-center items-center text-white hover:text-gray-900 bg-black hover:bg-white-100 rounded-full shadow transition duration-150 ease-in-out"
                aria-label="Github"
              >
                <svg
                  className="w-8 h-8 fill-current"
                  viewBox="0 0 32 32"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path d="M16 8.2c-4.4 0-8 3.6-8 8 0 3.5 2.3 6.5 5.5 7.6.4.1.5-.2.5-.4V22c-2.2.5-2.7-1-2.7-1-.4-.9-.9-1.2-.9-1.2-.7-.5.1-.5.1-.5.8.1 1.2.8 1.2.8.7 1.3 1.9.9 2.3.7.1-.5.3-.9.5-1.1-1.8-.2-3.6-.9-3.6-4 0-.9.3-1.6.8-2.1-.1-.2-.4-1 .1-2.1 0 0 .7-.2 2.2.8.6-.2 1.3-.3 2-.3s1.4.1 2 .3c1.5-1 2.2-.8 2.2-.8.4 1.1.2 1.9.1 2.1.5.6.8 1.3.8 2.1 0 3.1-1.9 3.7-3.7 3.9.3.4.6.9.6 1.6v2.2c0 .2.1.5.6.4 3.2-1.1 5.5-4.1 5.5-7.6-.1-4.4-3.7-8-8.1-8z" />
                </svg>
              </Link>
            </li>
          </ul>

          <div className="flex-shrink-0 mr-2">
            <Link href="/" className="block" aria-label="SuperbaseEcommerce">
              <img
                className="object-cover h-20 w-full"
                src="https://user-images.githubusercontent.com/37651620/159121520-fe42bbf1-a2af-4baf-bdd8-7efad8523202.png"
                alt="SupabaseEcommerce"
              />
            </Link>
          </div>
        </div>
      </div>
    </footer>
  );
};

export default Footer;
// index.js
import Layout from "@/components/Layout";
import Hero from "@/components/Hero";
import ShopCards from "@/components/ShopCards";
import Footer from "@/components/Footer";

export default function Home() {
  return (
    <Layout>
      <Hero />
      <ShopCards />
      <Footer />
    </Layout>
  );
}

Pokud tedy znovu spustíte server, vaše aplikace by měla vypadat takto.

Struktura složek vašich komponent by měla připomínat něco takového.

Gratulujeme!! Nyní, když jste úspěšně vytvořili vstupní stránku pro aplikaci, přejděme k jádru věci:vytvoření produktové části aplikace.

Nyní se tedy podíváme na _app.js soubor.

// _app.js
import "../styles/globals.css";
import { Toaster } from "react-hot-toast";

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Toaster />
    </>
  );
}

export default MyApp;

Komponentu App používá Next.js k vytvoření stránek. Inicializaci stránky můžete ovládat tak, že ji jednoduše přepíšete. Umožňuje vám dělat úžasné věci jako:Persisting layout across page changes , Keeping state while navigating pages , Custom error handling using componentDidCatch ,Inject additional data into pages and Add global styles/CSS je jen několik skvělých věcí, které s ním můžete dosáhnout.

Ve výše uvedeném \_app.js kód parametr Komponenta představuje aktivní stránku, při přepnutí tras se Komponenta změní na novou stránku. V důsledku toho stránka obdrží všechny rekvizity, které předáte komponentě. Mezitím pageProps je prázdný objekt, který obsahuje počáteční rekvizity, které byly pro vaši stránku předem načteny jednou z metod načítání dat.

Nyní uvnitř pages vytvořte novou stránku s názvem products.js a importujte Layout a Grid komponenty a poté importujte data.json soubor jako produkty a proveďte v něm následující změny.

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";

import products from "data.json";

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}

Konfigurace databáze

Než skočíme přímo do naší aplikace, využijeme sílu Supabase vytvořit PostgreSQL databáze, Prisma schema k definování datového modelu aplikace a Next.js k propojení těchto dvou. Začněme tedy budovat naši databázi.

Konfigurace Supabase

Vytvoření PostgreSQL databáze v Supabase je stejně jednoduché jako spuštění nového projektu. Přejděte na supabase.com a Sign-in na váš účet.

Po úspěšném přihlášení byste měli vidět něco podobného.

Nyní vyberte New project knoflík. Vyplňte požadované údaje projektu a znovu klikněte na Create Project a počkejte na načtení nové databáze.

Poté, co supabase nakonfiguruje projekt, váš řídicí panel by měl vypadat podobně jako tento.

Vytvoření adresy URL připojení

Chcete-li po úspěšném vytvoření databáze získat adresu URL připojení k databázi, postupujte podle níže uvedených kroků. Budeme jej potřebovat, abychom mohli používat Prisma v naší aplikaci Next.js k dotazování a vytváření dat.

  • Krok 1 :Přejděte na Settings tab (Nachází se na levé straně)

  • Krok 2 :Klikněte na Database na postranním panelu (nachází se na levé straně)

  • Krok 3 :Přejděte na konec stránky a vyhledejte Connection string a poté vyberte Nodejs a zkopírujte adresu URL.

Inicializace Prisma

Prisma je ORM nové generace, který lze použít v aplikacích Node.js a TypeScript pro přístup k databázi. Pro naši aplikaci budeme používat prisma, protože obsahuje veškerý kód, který potřebujeme ke spouštění našich dotazů. Ušetří nám to spoustu času a zabrání nám to psát spoustu standardních kódů.

Instalace hranolu

Instalace Prisma CLI

Rozhraní příkazového řádku Prisma (CLI) je primární rozhraní příkazového řádku pro interakci s vaším projektem Prisma. Může vytvářet nové projektové prostředky, generovat Prisma Client a analyzovat existující databázové struktury prostřednictvím introspekce, aby se automaticky vytvořily vaše aplikační modely.

npm i prisma

Inicializovat hranol

Jakmile nainstalujete Prisma CLI, spusťte následující příkaz a získejte Prisma začalo ve vašem Next.js aplikace. Poté vytvoří /prisma adresář a schema.prisma soubor v něm uvnitř vaší konkrétní projektové složky. takže do něj přidáme veškerou konfiguraci pro naši aplikaci.

npx prisma init


// prisma.schema
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Prisma-client-js , Prisma JavaScript klient, je nakonfigurovaný klient reprezentovaný generator blok.

generator client {
  provider = "prisma-client-js"
}

Další je vlastnost provider tohoto bloku představuje typ databáze, kterou chceme použít, a adresa URL připojení představuje, jak se k ní Prisma připojuje.

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Proměnná prostředí

Použití proměnných prostředí ve schématu vám umožňuje udržet tajemství mimo soubor schématu, což zase zlepšuje přenositelnost schématu tím, že je můžete používat v různých prostředích. Proměnné prostředí se vytvoří automaticky poté, co spustíme npx prisma init příkaz.

DATABASE_URL="postgresql://test:test@localhost:5432/test?schema=foo"

Jak můžete vidět, existuje DATABASE_URL proměnná s fiktivní adresou URL připojení v této proměnné prostředí .env . Nahraďte tedy tuto hodnotu připojovacím řetězcem, který jste získali od Supabase.

DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.bboujxbwamqvgypibdkh.supabase.co:5432/postgres"

Prisma schémata a modely

Nyní, když je databáze konečně připojena k vašemu Next.js, můžeme začít pracovat na datových modelech naší aplikace . V Prisma by měly být naše aplikační modely definovány v rámci schématu Prisma pomocí modelů Prisma. Tyto modely představují entity naší aplikace a jsou definovány bloky modelu v schema.prisma soubor. Každý blok obsahuje několik polí, která představují data pro každou entitu. Začněme tedy vytvořením Product model, který bude definovat datové schéma pro vlastnosti našich produktů.

Definování modelů

Modely představují entity vaší aplikační domény. Modely jsou reprezentovány modelovými bloky a definují řadu polí. V tomto datovém modelu Product je model.

// prisma.schema
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Product {
  id          String   @id @default(cuid())
  image       String?
  title       String
  description String
  status      String?
  price       Float
  authenticity        Int?
  returnPolicy        Int?
  warranty       Int?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

Každé pole, jak je znázorněno v našem modelu produktu, má alespoň název a svůj typ. Chcete-li se dozvědět více o skalárních typech a referencích schématu Prisma, navštivte následující odkazy.

  • Datový model
  • Schéma prisma
  • Odkaz na schéma prisma

Vygenerovat klienta Prisma

Po navržení modelu Prisma můžeme začít generovat našeho klienta Prisma. K interakci s našimi daty z našeho Next.js budeme muset použít knihovnu JavaScript společnosti Prisma dále v článku aplikace, aniž bychom museli psát všechny SQL dotazy sami. Ale je toho víc. Prisma Client je ve skutečnosti automaticky generované typově bezpečné API navržené speciálně pro naši aplikaci, které nám poskytne kód JavaScript, který potřebujeme ke spouštění dotazů na naše data.

  • Krok 1 :Instalace klienta prisma
  npm install @prisma/client

  • krok 2 :Generování klienta Prisma
  npx prisma generate

Balíček @prisma/client npm

Balíček @prisma/client npm se skládá ze dvou klíčových částí:

  • @prisma/client samotný modul, který se změní pouze při opětovné instalaci balíčku
  • Číslo .prisma/client složku, což je výchozí umístění pro jedinečného klienta Prisma vygenerovaného z vašeho schématu

@prisma/client/index.d.ts exportuje .prisma/client

Nakonec, až to uděláte uvnitř vašeho ./node_modules nyní byste měli najít vygenerovaný kód klienta Prisma.

Zde je grafické znázornění typického pracovního postupu pro generaci Prisma Client:

Klient Prisma je generován ze schématu Prisma a je jedinečný pro váš projekt. Pokaždé, když změníte schéma a spustíte generování prisma, klientský kód se sám změní.

Prořezávání v Node.js správci balíčků nemají žádný vliv na .prisma složka.

Vytvoření tabulky v Supabase

Pokud se podíváte na svou databázi v Supabase, všimnete si, že v ní není žádná tabulka. Je to proto, že jsme ještě nevytvořili Product tabulka.

Model Prisma jsme definovali v našem schema.prisma soubor se ještě neprojevil v naší databázi. V důsledku toho musíme změny v našem datovém modelu ručně přenést do naší databáze.

Posouvání datového modelu

Prisma umožňuje opravdu velmi snadno synchronizovat schéma s naší databází. Chcete-li to provést, postupujte podle příkazu uvedeného níže.

npx prisma db push

Tento příkaz je vhodný pouze pro lokální prototypování schémat.

NEBO,

npx prisma migrate dev

Tato metoda (npx prisma migrate dev ) bude v tomto článku použit, protože je velmi užitečný v tom, že nám umožňuje přímo synchronizovat naše schéma Prisma s naší databází a zároveň nám umožňuje snadno sledovat změny, které provedeme.

Chcete-li tedy začít používat Prisma Migrate, zadejte do příkazového řádku následující příkaz a poté na výzvu zadejte název pro tuto první migraci.

Po úspěšném dokončení tohoto procesu prisma automaticky vygeneruje soubory migrace databáze SQL a měli byste být schopni vidět SQL, který by měl vypadat nějak takto, pokud se podíváte dovnitř prisma složka.


-- CreateTable
CREATE TABLE "Product" (
    "id" TEXT NOT NULL,
    "image" TEXT,
    "title" TEXT NOT NULL,
    "description" TEXT NOT NULL,
    "status" TEXT NOT NULL,
    "price" DOUBLE PRECISION NOT NULL,
    "authenticity" INTEGER,
    "returnPolicy" INTEGER,
    "warranty" INTEGER,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
---

Nakonec zkontrolujte řídicí panel Supabase, abyste zjistili, zda bylo vše úspěšně synchronizováno.

Prisma Studio

Prisma Studio je vizuální rozhraní pro data uložená ve vaší databázi, které můžete použít k rychlé vizualizaci a manipulaci s daty. Skvělé na tom je, že běží výhradně ve vašem prohlížeči a nemusíte nastavovat žádná připojení, protože je již součástí balíčku prisma. Nejen to, ze studia můžete rychle otevřít všechny modely vaší aplikace a přímo s nimi pracovat. samotné studio.

Spuštění Prisma Studio

Spuštění prisma studia je opravdu velmi snadné. Doslova vše, co musíte udělat, je spustit následující příkaz z projektu Prisma.

npx prisma studio

Nyní otevřete prohlížeč a přejděte na http://localhost:5555/ . Pokud jste správně provedli všechny kroky, měli byste vidět jedinou tabulku, kterou jsme vytvořili dříve.

Ruční přidání záznamů

Pojďme ručně přidat nějaké záznamy a uložit změny, které jsme provedli.

Nakonec pojďme vytvořit funkci pro přístup k těmto datům z naší aplikace Next.js, kde můžeme vytvářet nové záznamy, aktualizovat stávající a mazat staré.

Interakce s daty pomocí Next.js

Pokud se podíváte na Product, měli byste vidět ukázková data stránce vaší aplikace.

Nyní otevřete soubor pages/products.js , soubor, který představuje stránku produktu naší aplikace.

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";

import products from "products.json";

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}

Jak vidíte, údaje o produktech pocházejí z products.json soubor.

// products.json
[
  {
    "id": "001",
    "image": "/products/ballpen_300.png",
    "title": "Ball Pen",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 1,
    "price": 50
  },
  {
    "id": "002",
    "image": "/products/actioncamera_300.png",
    "title": "Go-pro cam",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 1,
    "price": 30
  },
  {
    "id": "003",
    "image": "/products/alarmclock_300.png",
    "title": "Alarm Clock",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 1,
    "price": 20
  },
  {
    "id": "004",
    "image": "/products/bangle_600.png",
    "title": "Bangle",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "new",
    "warranty": 2,
    "price": 200
  },
  {
    "id": "005",
    "image": "/products/bed_600.png",
    "title": "Large Bed",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "out of stock!",
    "warranty": 1,
    "price": 105
  },
  {
    "id": "006",
    "image": "/products/binderclip_600.png",
    "title": "Binder clip",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 2,
    "status": "new",
    "warranty": 1,
    "price": 2
  },
  {
    "id": "007",
    "image": "/products/beyblade_600.png",
    "title": "BeyBlade Burst",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 1,
    "status": "out of stock!",
    "warranty": 1,
    "price": 15
  },
  {
    "id": "008",
    "image": "/products/boxinggloves_600.png",
    "title": "Boxing gloves",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "authenticity": 100,
    "returnPolicy": 2,
    "status": "new",
    "warranty": 1,
    "price": 45
  }
]

Tato data a informace jsou pak předány jako rekvizita z Product komponentu na Grid komponent. Grid komponenta pak má na starosti vykreslení těchto dat jako mřížky karty na obrazovce.

// Products.js
import PropTypes from "prop-types";
import Card from "@/components/Card";
import { ExclamationIcon } from "@heroicons/react/outline";

const Grid = ({ products = [] }) => {
  const isEmpty = products.length === 0;

  return isEmpty ? (
    <p className="text-purple-700 bg-amber-100 px-4 rounded-md py-2 max-w-max inline-flex items-center space-x-1">
      <ExclamationIcon className="shrink-0 w-5 h-5 mt-px" />
      <span>No data to be displayed.</span>
    </p>
  ) : (
    <div className="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
      {products.map((product) => (
        <Card key={product.id} {...product} onClickFavorite={toggleFavorite} />
      ))}
    </div>
  );
};

Grid.propTypes = {
  products: PropTypes.array,
};

export default Grid;

Nyní chceme načíst data z naší databáze a uděláme to pomocí Server-Side Rendering (SSR). Schopnost aplikace převádět soubory HTML na serveru na plně vykreslenou stránku HTML pro klienta se nazývá vykreslování na straně serveru (SSR). Webový prohlížeč odešle požadavek na informace na server, který okamžitě odpoví odesláním klientovi plně vykreslenou stránku.

Aby bylo možné použít server Side Rendering (SSR) s Next.js , musíme exportovat asynchronní funkci getServerSideProps ze souboru, který exportuje stránku, kde chceme vykreslit naše data. Data vrácená getServerSideProps funkci pak použije Next.js k předběžnému vykreslení naší stránky při každém jednotlivém požadavku. Začněme a exportujeme tuto funkci z Prodcuts naší aplikace strana.

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";

import products from "products.json";

export async function getServerSideProps() {
  return {
    props: {
      // props for the Home component
    },
  };
}

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}

Chcete-li získat data ze supabase, importujte a vytvořte instanci generated Prisma client .

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";
import { PrismaClient } from "@prisma/client";

import products from "products.json";

const prisma = new PrismaClient();

export async function getServerSideProps() {
  return {
    props: {
      // props for the Home component
    },
  };
}

export default function Products() {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}

Nyní pomocí findMany dotazu, můžeme získat všechny záznamy v naší tabulce produktů:

// pages/products.js
import Layout from "@/components/Layout";
import Grid from "@/components/Grid";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export async function getServerSideProps() {
  const products = await prisma.product.findMany();
  return {
    props: {
      products: JSON.parse(JSON.stringify(products)),
    },
  };
}

export default function Products({ products = [] }) {
  return (
    <Layout>
      <div className="mt-8 p-5">
        <Grid products={products} />
      </div>
    </Layout>
  );
}

Jednoduše spusťte aplikaci znovu, ale pokud se zobrazí chyba, která vypadá jako níže uvedená, budete muset znovu vygenerovat prisma a poté znovu spustit server.

Jak můžete vidět, je nyní opraveno

Nakonec by vaše aplikace měla vypadat nějak takto:

Umožňuje uživatelům poskytnout funkcionalitu pro skutečné vytváření záznamů ze samotné aplikace. Takže prvním krokem je skutečně vytvořit.

Vytvořit nové záznamy

Přejděte na pages/ a vytvořte nový soubor s názvem addProduct.js .


// addProducts.js
import Layout from "@/components/Layout";
import ProductList from "@/components/ProductList";

const addProducts = () => {
  const createProduct = () => null;

  return (
    <Layout>
      <div className="max-w-screen-xl mx-auto flex-col">
        <h1 className="text-3xl font-medium text-gray-200 justify-center">
          Add your Products
        </h1>
        <div className="mt-8">
          <ProductList
            buttonText="Add Product"
            redirectPath="/products"
            onSubmit={createProduct}
          />
        </div>
      </div>
    </Layout>
  );
};

export default addProducts;

Poté přejděte na ProductList komponentu a proveďte v této komponentě následující změny.

//components/ProductList.js
import { useState } from "react";
import { useRouter } from "next/router";
import PropTypes from "prop-types";
import * as Yup from "yup";
import { toast } from "react-hot-toast";
import { Formik, Form } from "formik";
import Input from "@/components/Input";
import AddProductImage from "@/components/AddProductImage";

const ProductSchema = Yup.object().shape({
  title: "Yup.string().trim().required(),"
  description: "Yup.string().trim().required(),"
  status: Yup.string().trim().required(),
  price: Yup.number().positive().integer().min(1).required(),
  authenticity: Yup.number().positive().integer().min(1).required(),
  returnPolicy: Yup.number().positive().integer().min(1).required(),
  warranty: Yup.number().positive().integer().min(1).required(),
});

const ProductList = ({
  initialValues = null,
  redirectPath = "",
  buttonText = "Submit",
  onSubmit = () => null,
}) => {
  const router = useRouter();

  const [disabled, setDisabled] = useState(false);
  const [imageUrl, setImageUrl] = useState(initialValues?.image ?? "");

  const upload = async (image) => {
    // TODO: Upload image to remote storage
  };

  const handleOnSubmit = async (values = null) => {
    let toastId;
    try {
      setDisabled(true);
      toastId = toast.loading("Submitting...");
      // Submit data
      if (typeof onSubmit === "function") {
        await onSubmit({ ...values, image: imageUrl });
      }
      toast.success("Successfully submitted", { id: toastId });
      // Redirect user
      if (redirectPath) {
        router.push(redirectPath);
      }
    } catch (e) {
      toast.error("Unable to submit", { id: toastId });
      setDisabled(false);
    }
  };

  const { image, ...initialFormValues } = initialValues ?? {
    image: "",
    title: "\"\","
    description: "\"\","
    status: "",
    price: 0,
    authenticity: 1,
    returnPolicy: 1,
    warranty: 1,
  };

  return (
    <div>
      <Formik
        initialValues={initialFormValues}
        validationSchema={ProductSchema}
        validateOnBlur={false}
        onSubmit={handleOnSubmit}
      >
        {({ isSubmitting, isValid }) => (
          <Form className="space-y-6">
            <div className="space-y-6">
              <Input
                name="title"
                type="text"
                label="Title"
                placeholder="Entire your product name..."
                disabled={disabled}
              />

              <Input
                name="description"
                type="textarea"
                label="Description"
                placeholder="Enter your product description...."
                disabled={disabled}
                rows={3}
              />

              <Input
                name="status"
                type="text"
                label="Status(new/out-of-stock/used)"
                placeholder="Enter your product status...."
                disabled={disabled}
              />

              <Input
                name="price"
                type="number"
                min="0"
                label="Price of the product..."
                placeholder="100"
                disabled={disabled}
              />

              <div className="justify-center">
                <Input
                  name="authenticity"
                  type="number"
                  min="0"
                  label="authenticity(%)"
                  placeholder="2"
                  disabled={disabled}
                />
                <Input
                  name="returnPolicy"
                  type="number"
                  min="0"
                  label="returnPolicy(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
                <Input
                  name="warranty"
                  type="number"
                  min="0"
                  label="warranty(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
              </div>
            </div>

            <div className="flex justify-center">
              <button
                type="submit"
                disabled={disabled || !isValid}
                className="bg-success text-white py-2 px-6 rounded-md focus:outline-none focus:ring-4 focus:ring-teal-600 focus:ring-opacity-50 hover:bg-teal-500 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-teal-600"
              >
                {isSubmitting ? "Submitting..." : buttonText}
              </button>
            </div>
          </Form>
        )}
      </Formik>
      <div className="mb-6 max-w-full">
        <AddProductImage
          initialImage={{ src: image, alt: initialFormValues.title }}
          onChangePicture={upload}
        />
      </div>
    </div>
  );
};

ProductList.propTypes = {
  initialValues: PropTypes.shape({
    image: PropTypes.string,
    title: "PropTypes.string,"
    description: "PropTypes.string,"
    status: PropTypes.string,
    price: PropTypes.number,
    authenticity: PropTypes.number,
    returnPolicy: PropTypes.number,
    warranty: PropTypes.number,
  }),
  redirectPath: PropTypes.string,
  buttonText: PropTypes.string,
  onSubmit: PropTypes.func,
};

export default ProductList;

Poté přejděte na AddProductImage soubor uvnitř složky komponenty a zkopírujte následující kód.

// AddProductImage.js
import { useState, useRef } from "react";
import PropTypes from "prop-types";
import Image from "next/image";
import toast from "react-hot-toast";
import classNames from "classnames";
import { CloudUploadIcon } from "@heroicons/react/outline";

const AddProductImage = ({
  label = "Image",
  initialImage = null,
  objectFit = "cover",
  accept = ".png, .jpg, .jpeg, .gif .jiff",
  sizeLimit = 10 * 1024 * 1024,
  onChangePicture = () => null,
}) => {
  const pictureRef = useRef();
  const [image, setImage] = useState(initialImage ?? null);
  const [updatingPicture, setUpdatingPicture] = useState(false);
  const [pictureError, setPictureError] = useState(null);

  const handleOnChangePicture = (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    const fileName = file?.name?.split(".")?.[0] ?? "New file";

    reader.addEventListener(
      "load",
      async function () {
        try {
          setImage({ src: reader.result, alt: fileName });
          if (typeof onChangePicture === "function") {
            await onChangePicture(reader.result);
          }
        } catch (err) {
          toast.error("Unable to update image");
        } finally {
          setUpdatingPicture(false);
        }
      },
      false
    );

    if (file) {
      if (file.size <= sizeLimit) {
        setUpdatingPicture(true);
        setPictureError("");
        reader.readAsDataURL(file);
      } else {
        setPictureError("File size is exceeding 10MB.");
      }
    }
  };

  const handleOnClickPicture = () => {
    if (pictureRef.current) {
      pictureRef.current.click();
    }
  };

  return (
    <div className="flex flex-col space-y-2">
      <label className="text-gray-200 ">{label}</label>

      <button
        disabled={updatingPicture}
        onClick={handleOnClickPicture}
        className={classNames(
          "relative aspect-video overflow-hidden rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition group focus:outline-none",
          image?.src
            ? "hover:opacity-50 disabled:hover:opacity-100"
            : "border-2 border-dotted hover:border-gray-400 focus:border-gray-400 disabled:hover:border-gray-200"
        )}
      >
        {image?.src ? (
          <Image
            src={image.src}
            alt={image?.alt ?? ""}
            layout="fill"
            objectFit={objectFit}
          />
        ) : null}

        <div className="flex items-center justify-center">
          {!image?.src ? (
            <div className="flex flex-col items-center space-y-2">
              <div className="shrink-0 rounded-full p-2 bg-gray-200 group-hover:scale-110 group-focus:scale-110 transition">
                <CloudUploadIcon className="w-4 h-4 text-gray-500 transition" />
              </div>
              <span className="text-xs font-semibold text-gray-500 transition">
                {updatingPicture
                  ? "Image Uploading..."
                  : "Upload product Image"}
              </span>
            </div>
          ) : null}
          <input
            ref={pictureRef}
            type="file"
            accept={accept}
            onChange={handleOnChangePicture}
            className="hidden"
          />
        </div>
      </button>

      {pictureError ? (
        <span className="text-red-600 text-sm">{pictureError}</span>
      ) : null}
    </div>
  );
};

AddProductImage.propTypes = {
  label: PropTypes.string,
  initialImage: PropTypes.shape({
    src: PropTypes.string,
    alt: PropTypes.string,
  }),
  objectFit: PropTypes.string,
  accept: PropTypes.string,
  sizeLimit: PropTypes.number,
  onChangePicture: PropTypes.func,
};

export default AddProductImage;

Toto addProduct komponenta vykresluje rozložení celé stránky, které se skládá z formuláře, ze kterého můžete přidat podrobnosti o produktu a informace.

Koncový bod rozhraní API

Pojďme vlastně vytvořit koncový bod API, který ve skutečnosti vytvoří nový záznam v naší databázi prostřednictvím addProduct funkce.

const createProduct = () => null;

Nejprve však v rámci našeho Next.js aplikační projekt, pojďme vytvořit API koncový bod pro zpracování našeho POST žádost o vytvoření nových záznamů. Next.js poskytuje směrování API založené na souborech, takže jakýkoli soubor v pages/api složka je namapována na /api/* a zachází se s ním jako s koncovým bodem API, nikoli se stránkou. Jsou pouze server-side svazky, takže nepřidají k velikosti client-side svazek. Vytvořte tedy název souboru s názvem products.js uvnitř pages/api složku a uvnitř ní vytvořte funkci obsluhy požadavků, jak je znázorněno níže.

export default async function handler(req, res) {}

Zpracování POST požadavek na products

Než půjdeme dále, použijte req.method zkontrolujte HTTP metoda požadavku uvnitř toho request handler funkce. Poté vraťte klientovi stavový kód 405, protože nezpracováváme žádnou metodu HTTP.

// pages/api/products.js
export default async function handler(req, res) {
  if (req.method === "POST") {
    // TODO
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}

Přidávání nových záznamů pomocí klienta Prisma

Nyní pomocí klienta Prisma vytvořte nový Product záznam do databáze pomocí dat z aktuálního HTTP požadavku.

// pages/api/products.js
export default async function handler(req, res) {
  if (req.method === "POST") {
    const {
      image,
      title,
      description,
      status,
      price,
      authenticity,
      returnPolicy,
      warranty,
    } = req.body;
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}

Poté pojďme inicializovat Prisma a zavolejte create funkce, kterou prisma poskytuje.

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default async function handler(req, res) {
  if (req.method === "POST") {
    const {
      image,
      title,
      description,
      status,
      price,
      authenticity,
      returnPolicy,
      warranty,
    } = req.body;

    const home = await prisma.product.create({
      data: {
        image,
        title,
        description,
        status,
        price,
        authenticity,
        returnPolicy,
        warranty,
      },
    });
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}

Nakonec přidáme nějaký blok pokusu zachytit chybu, aby se vyřešila chyba.

// pages/api/products.js
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

export default async function handler(req, res) {
  if (req.method === "POST") {
    try {
      const {
        image,
        title,
        description,
        status,
        price,
        authenticity,
        returnPolicy,
        warranty,
      } = req.body;

      const product = await prisma.product.create({
        data: {
          image,
          title,
          description,
          status,
          price,
          authenticity,
          returnPolicy,
          warranty,
        },
      });
      res.status(200).json(product);
    } catch (e) {
      res.status(500).json({ message: "Something went wrong" });
    }
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method ${req.method} is not supported.` });
  }
}

Nyní, když jsme vytvořili naše API , zavolejte koncový bod API. Chcete-li tak učinit, otevřete addProduct.js soubor v pages a proveďte následující změny v kódu, ale nejprve budeme muset nainstalovat axios balíček, tak to udělejte jako první.

npm i axios

NEBO

yarn add axios


//pages/addProducts.js
import Layout from "@/components/Layout";
import ProductList from "@/components/ProductList";

const addProducts = () => {
  const createProduct = () => (data) => axios.post("/api/products", data);

  return (
    <Layout>
      <div className="max-w-screen-xl mx-auto flex-col">
        <h1 className="text-3xl font-medium text-gray-200 justify-center">
          Add your Products
        </h1>
        <div className="mt-8">
          <ProductList
            buttonText="Add Product"
            redirectPath="/products"
            onSubmit={createProduct}
          />
        </div>
      </div>
    </Layout>
  );
};

export default addProducts;

Nyní znovu spusťte server.

Poté přejděte do prohlížeče a přejděte na http://localhost:3000/addProducts trasu a vyplňte všechny informace o produktu a Submit to.



Automaticky vás přesměruje na /products a měli byste vidět produkt, který jste právě přidali.

Předběžné vykreslování stránek

Použili jsme getServerSideProps funkce pro předběžné vykreslení product naší aplikace pomocí Server-Side Rendering(SSR) . Na druhou stranu Next.js je dodáván s built-in metoda předběžného vykreslování s názvem Static Generation (SSG) .

Když stránka používá statické generování, HTML pro tuto stránku se vygeneruje během procesu sestavování. To znamená, že když spustíte další sestavení v produkci, vygeneruje se HTML stránky. Každý požadavek pak bude doručen se stejným HTML. A CDN může to uložit do mezipaměti. Pomocí Next.js můžete staticky generovat stránky s daty nebo bez nich .

Můžeme použít různé pre-rendering techniky na našich aplikacích, když používáme framework jako Next.js . Pro něco jednoduššího a nedynamického můžeme použít static site generation(SSG) . Pro dynamický obsah a složitější stránky můžeme použít server-side rendering(SSR) .

Dynamické směrování s SSG

Stále můžeme staticky generovat stránky pomocí SSG po načtení některých externích dat během procesu sestavování, i když SSG generuje HTML v době sestavování. zjistěte více o statickém generování a dynamickém směrování.

Pojďme získat data v době sestavování exportem async funkce s názvem getStaticProps ze stránek, které chceme staticky generovat.

Například

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  );
}
// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries.
export async function getStaticProps() {
  // Call an external API endpoint to get posts.
  // You can use any data fetching library
  const res = await fetch("https://.../posts");
  const posts = await res.json();
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  };
}
export default Blog;

Nechme v naší aplikaci pracovat Static Generation (SSG). Stránky, které vykreslují jednotlivé Product výpisy jsou ty, které staticky vygenerujeme při sestavení. Nicméně, protože product výpisy jsou generovány přes uživatele, mohli bychom skončit s obrovským množstvím stránek. V důsledku toho nebudeme moci tyto trasy definovat pomocí předdefinovaných cest. V opačném případě skončíme se spoustou zbytečných souborů, které zaplní náš projekt.

Můžeme snadno vytvářet dynamické trasy v Next.js . Potřebujeme pouze přidat závorky k názvu souboru stránky, [id].js k vytvoření dynamické trasy. V našem projektu to však umístíme do Products složku. Výsledkem je, že každá trasa má ids bude shodná s jejich specifickou hodnotou id a hodnota id bude k dispozici uvnitř komponenty React, která vykresluje přidruženou stránku.

Nyní přejděte do složky Stránky a vytvořte novou složku s názvem products , pak vytvořte nový soubor s názvem [id].js uvnitř.

A nakonec do tohoto souboru vložte následující kód.

// pages/products/[id].jsx
import Image from "next/image";
import Layout from "@/components/Layout";

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export default ListedProducts;

Nyní poskytněme seznamy cest stránek, které chceme staticky vygenerovat, a ve skutečnosti načteme nějaká data a porovnejme je s počty cest. Abychom tak učinili, musíme poskytnout cesty k Next.js, které chceme předběžně vykreslit v době sestavování. Tato funkce by měla vrátit všechny cesty stránek do předběžného vykreslení v době sestavování spolu s odpovídajícím id hodnotu ve vlastnosti params vráceného objektu. Za tímto účelem použijeme Prisma k načtení ID pro všechny products sídlící v naší databázi.

// pages/products/[id].jsx
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
// Instantiate Prisma Client
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    fallback: false,
  };
}

export default ListedProducts;

getStaticProps funkce musí být nyní implementována. Takže, pojďme začít. Jak vidíte, první věc, kterou uděláme, je použití funkce Prisma findUnique s id získaným z objektu query params k získání dat požadované trasy. Poté, pokud je v databázi nalezen odpovídající domov, vrátíme jej na ListedProducts Reagovat komponent jako rekvizita. Pokud je požadováno products nelze nalézt, vrátíme objekt, který sdělí Next.js, aby uživatele přesměroval na 'products' naší aplikace strana.

// pages/products/[id].jsx
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
// Instantiate Prisma Client
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const product = await prisma.product.findUnique({
    where: { id: params.id },
  });

  if (product) {
    return {
      props: JSON.parse(JSON.stringify(product)),
    };
  }

  return {
    redirect: {
      destination: "/products",
      permanent: false,
    },
  };
}

export default ListedProducts;

Nyní znovu spusťte server a vraťte se zpět do prohlížeče a otevřete aplikaci.

Implementace přírůstkové statické generování (ISR)

Pokud se pokusíte o přístup na stránku pro nový product výpis ve výrobě, získáte 404 error page namísto. Chcete-li to vidět v akci, vytvořte si aplikaci a spusťte ji jako v produkci, protože getStaticProps běží na každý požadavek ve vývoji. Ve vývoji tedy máme odlišné chování, které se liší od toho, co bychom viděli v production . Chcete-li obsloužit produkční sestavení vaší aplikace, jednoduše spusťte následující příkaz, ale nejprve zastavte server.

yarn build


yarn start

Hlavní důvod pro 404 page je, že jsme použili Statické generování k definování tras /products/[id].js a generovali jsme stránky pouze pro produkty, které byly v té době v naší databázi. Jinými slovy, po tomto procesu sestavení žádný z produktů, které naši uživatelé vytvoří, nevygeneruje novou stránku. Proto máme 404 page místo toho, protože stránka prostě vůbec neexistuje. Abychom to napravili, budeme muset definovat záložní řešení, které nám umožní pokračovat ve vytváření stránek líně za běhu.

// pages/products/[id].js
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
// Instantiate Prisma Client
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    // ----- SET to TRUE ------
    fallback: true,
  };
}

export async function getStaticProps({ params }) {
  const product = await prisma.product.findUnique({
    where: { id: params.id },
  });

  if (product) {
    return {
      props: JSON.parse(JSON.stringify(product)),
    };
  }

  return {
    redirect: {
      destination: "/products",
      permanent: false,
    },
  };
}

export default ListedProducts;

Nyní, když jsme nastavili fallback na true , 404 stránka se již nebude zobrazovat.

It's also possible to detect whether the fallback version of the page is being rendered with the Next.js router and, if so, conditionally render something else, such as a loading spinner, while we wait for the props to get loaded.

const router = useRouter();

if (router.isFallback) {
  return (
    <svg
      role="status"
      class="mr-2 w-14 h-14 text-gray-200 animate-spin dark:text-gray-600 fill-success"
      viewBox="0 0 100 101"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
        fill="currentColor"
      />
      <path
        d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
        fill="currentFill"
      />
    </svg>
  );
}

Finally your [id].js code should look something like this.

// pages/products/[id].js
import Image from "next/image";
import Layout from "@/components/Layout";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

const ListedProducts = (product = null) => {
  const router = useRouter();

  if (router.isFallback) {
    return (
      <svg
        role="status"
        class="mr-2 w-14 h-14 text-gray-200 animate-spin dark:text-gray-600 fill-success"
        viewBox="0 0 100 101"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
          fill="currentColor"
        />
        <path
          d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
          fill="currentFill"
        />
      </svg>
    );
  }

  return (
    <Layout>
      <div className="max-w-screen-lg mx-auto">
        <div className="mt-6 relative aspect-video bg-gray-400 rounded-lg shadow-md overflow-hidden">
          {product?.image ? (
            <Image
              src={product.image}
              alt={product.title}
              layout="fill"
              objectFit="cover"
            />
          ) : null}
        </div>
        <div className="flex flex-col sm:flex-row sm:justify-between sm:space-x-4 space-y-4 pt-10">
          <div>
            <h1 className="text-2xl font-semibold truncate">
              {product?.title ?? ""}
            </h1>
            <ol className="inline-flex items-center space-x-1 text-info">
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.status ?? 0} product</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.authenticity ?? 0}% Authentic</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.returnPolicy ?? 0} year return policy</span>
                <span aria-hidden="true"> ) </span>
                <span aria-hidden="true"> - </span>
              </li>
              <li>
                <span aria-hidden="true"> ( </span>
                <span>{product?.warranty ?? 0} year warranty</span>
                <span aria-hidden="true"> ) </span>
              </li>
            </ol>
            <p className="mt-8 text-lg">{product?.description ?? ""}</p>
          </div>
        </div>
      </div>
    </Layout>
  );
};

export async function getStaticPaths() {
  const products = await prisma.product.findMany({
    select: { id: true },
  });

  return {
    paths: products.map((product) => ({
      params: { id: product.id },
    })),
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const product = await prisma.product.findUnique({
    where: { id: params.id },
  });

  if (product) {
    return {
      props: JSON.parse(JSON.stringify(product)),
    };
  }

  return {
    redirect: {
      destination: "/products",
      permanent: false,
    },
  };
}

export default ListedProducts;

Uploading image in Supabase

We've created product records up to this point, but without any images because we haven't yet implemented aby media storage. We'll use Supabase Storage, a fantastic service from Supabase, to store and use media files in our project.

Creating a bucket in supabase

Buckets are distinct containers for files and folders. It is like a super folders . Generally you would create distinct buckets for different Security and Access Rules. For example, you might keep all public files in a public bucket, and other files that require logged-in access in a restricted bucket.

To create a bucket in Supabase, first navigate to the storage section of the dashboard.

After that, select Create Bucket tlačítko.

Next, give the bucket a name; for now, we'll call it supabase-ecommerce , and remember to make it public and click on that Create Button tlačítko.

Manually uploading image on database

  • Step 1 :Head over to the supabase Storage and upload the products images.




  • Step 2 :Select the product image and copy the image url

  • Step 3 :Open up the Prisma Studio by typing npx prisma studio inside the command line terminal.

  • Step 3 :Now, paste all of the image urls you copied in 'Step 2' inside the image row.

Go back to the application and refresh the page now that you've added all of the image urls . You may encounter the error shown below.

Copy the hostname of your file URL and paste it into the images.domains config in the next.config.js file to fix the error.

module.exports = {
  reactStrictMode: true,
  images: {
    domains: ["ezkjatblqzjynrebjkpq.supabase.co"],
  },
};

After that, restart the server, and you should see images.



Security Rules

We must define some security rules to be able to deal with our image files inside our bucket using the Supabase API . So, add the security rules from our Supabase dashboard .

  • Step 1 :Head over to the Storage section and go to the Policies section.

  • Step 2 :Create a New Policy .

  • Step 3 :Select Get started quickly .

  • Step 4 :Use Allow access to JPG images in a public folder to anonymous users this template.

  • Step 5 :Give the Policy Name select all the Operation and give bucket_id and Hit Review .

  • Step 6 :Review the policy and save it.

  • Step 8 :Finally you've successfully created a Storage Policy .

Upload a file from application

Let's keep going and add the ability for our application to upload and store our products images. Let's begin by adding a new API endpoint to your project's pages/api/productsImage.js adresář.

// pages/api/productsImage.js
export default async function handler(req, res) {
  if (req.method === "POST") {
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method :${req.method}: not supported.` });
  }
}

Now, let's use Supabase JS Client for uploading the image to our Supabase Storage Bucket.To do so, you need to install @supabase/supabase-js client library.

npm i @supabase/supabase-js

Then, inside your pages/api/productsImage.js file , import it and create a new Supabase Client.

// pages/api/productsImage.js
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.SUPABASE_API_URL,
  process.env.SUPABASE_API_KEY
);
export default async function handler(req, res) {
  if (req.method === "POST") {
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method :${req.method}: not supported.` });
  }
}

After that, go to the Supabase dashboard and click on Setting > API .

and add all those API keys to your env file.

SUPABASE_API_KEY =
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV6a2phdGJscXpqeW5yZWJ-";
SUPABASE_API_URL = "https://ezkjatblqzjynrebjkpq.supabase.co";
SUPABASE_STORAGE_BUCKET = "supabase-ecommerce";

Now you need to add three packages to your application. The first one is base64-arraybuffer which encodes and decodes base64 to and from ArrayBuffers and another package called nanoid which is a very tiny, secure, URL-friendly, unique string ID generator for JavaScript .

yarn add nanoid base64-arraybuffer

Return to our API endpoint and upload a file to our bucket using the Supabase Client. Obtain the image data from the request's body and verify that it is not empty, then inspect the image data for Base64 encoding . After that, save the file to your Supbase storage bucket. With the SUPABASE_STORAGE_BUCKET env, you must provide the storage bucket name, the file path, and the decoded Base64 data, as well as the contentType . Once the image has been successfully uploaded, we can generate its public URL and return it to the client who initiated the HTTP request and then do some Error handling .So finally, your API endpoint for productsImage should look like this.

// pages/api/productsImage.js
import { supabase } from "@/lib/supabase";
import { nanoid } from "nanoid";
import { decode } from "base64-arraybuffer";

export default async function handler(req, res) {
  if (req.method === "POST") {
    let { image } = req.body;

    if (!image) {
      return res.status(500).json({ message: "There is no image" });
    }
    try {
      const imageType = image.match(/data:(.*);base64/)?.[1];
      const base64FileData = image.split("base64,")?.[1];
      if (!imageType || !base64FileData) {
        return res.status(500).json({ message: "Image data not valid" });
      }
      const fileName = nanoid();
      const ext = imageType.split("/")[1];
      const path = `${fileName}.${ext}`;
      const { data, error: uploadError } = await supabase.storage
        .from(process.env.SUPABASE_STORAGE_BUCKET)
        .upload(path, decode(base64FileData), {
          imageType,
          upsert: true,
        });
      if (uploadError) {
        console.log(uploadError);
        throw new Error("Image upload Failed!!");
      }
      const url = `${process.env.SUPABASE_API_URL.replace(
        ".co"
      )}/storage/v1/object/public/${data.Key}`;

      return res.status(200).json({ url });
    } catch (e) {
      res.status(500).json({ message: "Something went horribly wrong" });
    }
  } else {
    res.setHeader("Allow", ["POST"]);
    res
      .status(405)
      .json({ message: `HTTP method :${req.method}: is not supported.` });
  }
}

export const config = {
  api: {
    bodyParser: {
      sizeLimit: "15mb",
    },
  },
};

After you have added the API endpoint make the following chnages to the ProductList .

import { useState } from "react";
import { useRouter } from "next/router";
import PropTypes from "prop-types";
import * as Yup from "yup";
import { toast } from "react-hot-toast";
import { Formik, Form } from "formik";
import Input from "@/components/Input";
import AddProductImage from "@/components/AddProductImage";
import axios from "axios";

const ProductSchema = Yup.object().shape({
  title: Yup.string().trim().required(),
  description: Yup.string().trim().required(),
  status: Yup.string().trim().required(),
  price: Yup.number().positive().integer().min(1).required(),
  authenticity: Yup.number().positive().integer().min(1).required(),
  returnPolicy: Yup.number().positive().integer().min(1).required(),
  warranty: Yup.number().positive().integer().min(1).required(),
});

const ProductList = ({
  initialValues = null,
  redirectPath = "",
  buttonText = "Submit",
  onSubmit = () => null,
}) => {
  const router = useRouter();

  const [disabled, setDisabled] = useState(false);
  const [imageUrl, setImageUrl] = useState(initialValues?.image ?? "");

  const upload = async (image) => {
    if (!image) return;

    let toastId;
    try {
      setDisabled(true);
      toastId = toast.loading("Uploading...");
      const { data } = await axios.post("/api/productsImage", { image });
      setImageUrl(data?.url);
      toast.success("Successfully uploaded Image", { id: toastId });
    } catch (e) {
      toast.error("Unable to upload Image", { id: toastId });
      setImageUrl("");
    } finally {
      setDisabled(false);
    }
  };

  const handleOnSubmit = async (values = null) => {
    let toastId;
    try {
      setDisabled(true);
      toastId = toast.loading("Submitting...");
      // Submit data
      if (typeof onSubmit === "function") {
        await onSubmit({ ...values, image: imageUrl });
      }
      toast.success("Successfully submitted", { id: toastId });
      // Redirect user
      if (redirectPath) {
        router.push(redirectPath);
      }
    } catch (e) {
      toast.error("Unable to submit", { id: toastId });
      setDisabled(false);
    }
  };

  const { image, ...initialFormValues } = initialValues ?? {
    image: "",
    title: "",
    description: "",
    status: "",
    price: 0,
    authenticity: 1,
    returnPolicy: 1,
    warranty: 1,
  };

  return (
    <div>
      <Formik
        initialValues={initialFormValues}
        validationSchema={ProductSchema}
        validateOnBlur={false}
        onSubmit={handleOnSubmit}
      >
        {({ isSubmitting, isValid }) => (
          <Form className="space-y-6">
            <div className="space-y-6">
              <Input
                name="title"
                type="text"
                label="Title"
                placeholder="Entire your product name..."
                disabled={disabled}
              />

              <Input
                name="description"
                type="textarea"
                label="Description"
                placeholder="Enter your product description...."
                disabled={disabled}
                rows={3}
              />

              <Input
                name="status"
                type="text"
                label="Status(new/out-of-stock/used)"
                placeholder="Enter your product status...."
                disabled={disabled}
              />

              <Input
                name="price"
                type="number"
                min="0"
                label="Price of the product..."
                placeholder="100"
                disabled={disabled}
              />

              <div className="justify-center">
                <Input
                  name="authenticity"
                  type="number"
                  min="0"
                  label="authenticity(%)"
                  placeholder="2"
                  disabled={disabled}
                />
                <Input
                  name="returnPolicy"
                  type="number"
                  min="0"
                  label="returnPolicy(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
                <Input
                  name="warranty"
                  type="number"
                  min="0"
                  label="warranty(? years)"
                  placeholder="1"
                  disabled={disabled}
                />
              </div>
            </div>

            <div className="flex justify-center">
              <button
                type="submit"
                disabled={disabled || !isValid}
                className="bg-success text-white py-2 px-6 rounded-md focus:outline-none focus:ring-4 focus:ring-teal-600 focus:ring-opacity-50 hover:bg-teal-500 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-teal-600"
              >
                {isSubmitting ? "Submitting..." : buttonText}
              </button>
            </div>
          </Form>
        )}
      </Formik>
      <div className="mb-6 max-w-full">
        <AddProductImage
          initialImage={{ src: image, alt: initialFormValues.title }}
          onChangePicture={upload}
        />
      </div>
    </div>
  );
};

ProductList.propTypes = {
  initialValues: PropTypes.shape({
    image: PropTypes.string,
    title: PropTypes.string,
    description: PropTypes.string,
    status: PropTypes.string,
    price: PropTypes.number,
    authenticity: PropTypes.number,
    returnPolicy: PropTypes.number,
    warranty: PropTypes.number,
  }),
  redirectPath: PropTypes.string,
  buttonText: PropTypes.string,
  onSubmit: PropTypes.func,
};

export default ProductList;

Now lets actually test our final application

Chatwoot Configuration

Chatwoot configuration on Heroku

Let's get started by creating a chatwoot instance on Heroku.

  • Step First :Create a free Heroku account by going to https://www.heroku.com/ and then going to the chatwoot GitHub repository and clicking the Deploy to Heroku button in the readme section.

  • Step Second :After you click that button, you'll be able to see the basic setup that chatwoot has already completed. Give the App name and replace the FRONTEND_URL with the App name you just gave, then click Deploy App .

  • Step Third :Depending on your PC, network status, and server location, the program may take 10 to 15 minutes to install.

  • Step Fourth :After the app has been deployed, go to the settings panel in the dashboard.

  • Step Fifth :The domain section can be found in the settings menu. In a new window, open that URL. Finally, you've configured chatwoot in Heroku successfully.

  • Step Sixth :Inside the Resources section, make sure the web and worker resources are enabled.

  • Step Seventh :You should be able to log onto your chatwoot account if everything went smoothly.

So, your first account has been created successfully.The main benefit of deploying chatwoot on Heroku is that you have full control over your entire application and your entire data.

Chatwoot cloud setup

There is another way to get started with chatwoot which is the cloud way so this is the most straightforward way to get started is to register directly on the chatwoots website.

  • Step First :Fill out all of the required information to create an account.

  • Step Second :You'll get an email asking you to confirm your account after you've signed up.

  • Step Third :Proceed to login after you've confirmed your account by clicking the "Confirm my account" option.

  • Step Fourth :You may now visit the Chatwoot dashboard and begin connecting it with plethora of platform (websites, Facebook, Twitter, etc.).

Chatwoot Cloud Configuration
  • Step First :Let's set up an inbox. The inbox channel acts as a communication hub where everything can be managed, including live-chat, a Facebook page, a Twitter profile, email, and WhatsApp.

  • Step Second :Now, configure a website and domain name, as well as all of the heading and tagline information like shown below

  • Step Third :Finally, to control your mailbox, add "Agents." Keep in mind that only the "Agents" who have been authorized will have full access to the inbox.

  • Step Fourth :Blammmm!. The website channel has been created successfully.

The website channel must now be connected. Simply copy and paste the entire javascript code provided by chatwoot.Now, head back to our react app and create a new component folder and inside that folder create a new file/component called ChatwootWidget and inside it create a script which helps to loads the Chatwoot asynchronously. Simply follow the exact same steps outlined in the following code below.

// ChatwootWidget.js
import { useEffect } from "react";

const ChatwootWidget = () => {
  useEffect(() => {
    // Add Chatwoot Settings
    window.chatwootSettings = {
      hideMessageBubble: false,
      position: "right",
      locale: "en",
      type: "expanded_bubble",
    };

    (function (d, t) {
      var BASE_URL = "https://app.chatwoot.com";
      var g = d.createElement(t),
        s = d.getElementsByTagName(t)[0];
      g.src = BASE_URL + "/packs/js/sdk.js";
      g.defer = true;
      g.async = true;
      s.parentNode.insertBefore(g, s);
      g.onload = function () {
        window.chatwootSDK.run({
          websiteToken: ""// add you secret token here,
          baseUrl: BASE_URL,
        });
      };
    })(document, "script");
  }, []);

  return null;
};

export default ChatwootWidget;

The best part about chatwoot is that you can customize it to your liking. For example, you can modify the position of the floating bubble, extend it, change the language, and hide the message bubble. All it takes is the addition of the following line of code.

window.chatwootSettings = {
  hideMessageBubble: false,
  position: "right",
  locale: "en",
  type: "expanded_bubble",
};

Finally, it's time to import the ChatwootWidget component into our _app_.js file. To do so, simply navigate to the _app_.js file and import the chatwoot widget, then render that component. Your final code of _app_.js should look like this.

// _app.js.js
import "../styles/globals.css";
import { Toaster } from "react-hot-toast";
import ChatwootWidget from "@/components/ChatwootWidget";

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Toaster />
      <ChatwootWidget />
    </>
  );
}

export default MyApp;

Now that you've completed the chatwoot integration, your finished project should resemble something like this.

Deploying to netlify

First, sign in to netlify or create an account if you don't already have one.

You can also log in using a variety of other platforms.

Import your project from github now.

Sign-in and connect to your GitHub account.

Look for your project on Github.

Add all of the configuration, and don't forget to include the environment variables.

Yayyy!! 🎉 🎉 Its deployed on Netlify!

Conclusion

Congratulations 🎉 🎉!!. You've successfully created a fullstack application with Next.js, Supabase, Prisma and chatwoot.This article may have been entertaining as well as instructive in terms of creating a fully fgledged working ecommerce site from absolute scratch.

Aviyel is a collaborative platform that assists open source project communities in monetizing and long-term sustainability. To know more visit Aviyel.com and find great blogs and events, just like this one! Sign up now for early access, and don't forget to follow us on our socials

Refrences

  • Managing .env files and setting variables
  • A first look at Prisma Studio
  • Pre-rendering and Data Fetching
  • Data Model
  • Generating the client
  • Instantiating the client
  • Prisma schema
  • Prisma schema reference