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é vyberteNodejs
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 theproducts
images.
- Step 2 :Select the product image and copy the
image url
- Step 3 :Open up the
Prisma Studio
by typingnpx 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 thePolicies
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 theOperation
and givebucket_id
and HitReview
.
- Step 6 :
Review
the policy andsave
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 theDeploy 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 theFRONTEND_URL
with theApp name
you just gave, then clickDeploy 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
andworker
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