,
7 | apiVersion: "2022-11-27",
8 | useCdn: true,
9 | token: process.env.NEXT_PUBLIC_SANITY_TOKEN,
10 | });
11 |
12 | const builder = imageUrlBuilder(client);
13 |
14 | export const urlFor = (source) => builder.image(source);
15 |
--------------------------------------------------------------------------------
/Ch10_Code_Nextjs_eCommerce/Ch10_Code_Nextjs_eCommerce/cypress tests/cypress tests pipeline file.txt:
--------------------------------------------------------------------------------
1 | name: Cypress Tests
2 |
3 | on: push
4 |
5 | jobs:
6 | cypress-run:
7 | runs-on: ubuntu-22.04
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v3
11 | # Install NPM dependencies, cache them correctly
12 | # and run all Cypress tests
13 | - name: Cypress run
14 | uses: cypress-io/github-action@v5
15 | with:
16 | build: npm run build
17 | start: npm start
18 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding stripe/lib/client.js:
--------------------------------------------------------------------------------
1 | import { createClient } from "@sanity/client";
2 | import imageUrlBuilder from "@sanity/image-url";
3 |
4 | export const client = createClient({
5 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
6 | dataset: "production",
7 | apiVersion: "2022-11-27",
8 | useCdn: true,
9 | token: process.env.NEXT_PUBLIC_SANITY_TOKEN,
10 | });
11 |
12 | const builder = imageUrlBuilder(client);
13 |
14 | export const urlFor = (source) => builder.image(source);
15 |
--------------------------------------------------------------------------------
/Ch7_Code_Nextjs_eCommerce/Ch7_Code_Nextjs_eCommerce/updated star review/styles.css.txt:
--------------------------------------------------------------------------------
1 | /* UPDATED STAR COUNT */
2 | .star-rating button {
3 | background-color: transparent;
4 | border: none;
5 | outline: none;
6 | cursor: pointer;
7 | }
8 |
9 | .star {
10 | width: 16px;
11 | height: 16px;
12 | display: flex;
13 | }
14 |
15 | .on {
16 | color: #f02d34;
17 | fill: #f02d34;
18 | }
19 |
20 | .off {
21 | color: #d3d3d3;
22 | fill: #ffffff;
23 | }
24 |
25 | .on > path {
26 | color: #f02d34;
27 | fill: #f02d34;
28 | }
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Apress Source Code
2 |
3 | This repository accompanies [*Practical Next.js for E-Commerce*](https://link.springer.com/book/10.1007/978-1-4842-9612-7) by Alex Libby (Apress, 2023).
4 |
5 | [comment]: #cover
6 | 
7 |
8 | Download the files as a zip using the green button, or clone the repository to your machine using Git.
9 |
10 | ## Releases
11 |
12 | Release v1.0 corresponds to the code in the published book, without corrections or updates.
13 |
14 | ## Contributions
15 |
16 | See the file Contributing.md for more information on how you can contribute to this repository.
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/cypress/e2e/app.cy.js:
--------------------------------------------------------------------------------
1 | describe("Navigation", () => {
2 | it("should navigate to the about page", () => {
3 | // Start from the index page
4 | cy.visit("http://localhost:3000/");
5 |
6 | // Find a link with an href attribute containing "about" and click it
7 | cy.get('a[href*="about"]').click();
8 |
9 | // The new url should include "/about"
10 | cy.url().should("include", "/about");
11 |
12 | // The new page should contain a div with "About Macaron Magic"
13 | cy.get("div.about-us").contains("About Macaron Magic");
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/Ch12_Code_Nextjs_eCommerce/Ch12_Code_Nextjs_eCommerce/adding login options/styles for globals.css.txt:
--------------------------------------------------------------------------------
1 | /* CHANGES FOR AUTHENTICATION */
2 | .demo-banner-container {
3 | display: flex;
4 | justify-content: space-evenly;
5 | }
6 |
7 | .demo-banner-container > span:nth-child(2) {
8 | display: flex;
9 | }
10 |
11 | .demo-banner-container > span:nth-child(2) > img {
12 | width: 20px;
13 | height: 20px;
14 | margin-right: 5px;
15 | }
16 |
17 | .demo-banner-container > span:nth-child(2) > p {
18 | margin-right: 15px;
19 | }
20 |
21 | .demo-banner-container > span:nth-child(2) > a {
22 | border-left: 2px solid #ffffff;
23 | padding-left: 15px;
24 | }
25 |
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/cypress/e2e/starRating.cy.js:
--------------------------------------------------------------------------------
1 | describe("Star Rating", () => {
2 | it("should navigate to a product page", () => {
3 | cy.visit("http://localhost:3000/product/chocolate-orange");
4 |
5 | // confirm that we have number of counts of ratings to right of stars
6 | cy.get("div.reviews > p").contains("(20)");
7 |
8 | // div should contain 5 stars
9 | cy.get("div.star-rating > button").should("have.length", 5);
10 |
11 | // click on star 3 from left
12 | cy.get("div.star-rating > button").eq(2).click();
13 |
14 | // should have 3 red stars, 2 grey
15 | cy.get("div.star-rating > button").eq(2).should("have.class", "on");
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/Contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to Apress Source Code
2 |
3 | Copyright for Apress source code belongs to the author. However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author and other readers.
4 |
5 | ## How to Contribute
6 |
7 | 1. Make sure you have a GitHub account.
8 | 2. Fork the repository for the relevant book.
9 | 3. Create a new branch on which to make your change, e.g.
10 | `git checkout -b my_code_contribution`
11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted.
12 | 5. Submit a pull request.
13 |
14 | Thank you for your contribution!
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/cypress/e2e/component/demobanner.cy.js:
--------------------------------------------------------------------------------
1 | import DemoBanner from "../../src/components/DemoBanner.jsx";
2 |
3 | const mockText =
4 | "This is a demo store - no orders will be accepted or delivered";
5 |
6 | describe(" ", () => {
7 | it("should render and display expected content", () => {
8 | // Mount the DemoBanner component
9 | cy.mount( );
10 |
11 | // The component should contain an element of "footer"
12 | cy.get("div.demo-banner-container").should("be.visible");
13 |
14 | // Validate that the correct text is displayed in the banner
15 | cy.get("div.demo-banner-container").should("have.text", mockText);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/Ch11_Code_Nextjs_eCommerce/Ch11_Code_Nextjs_eCommerce/updating a page file/snippets for updating a page file.txt:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "next-i18next";
2 | import { serverSideTranslations } from "next-i18next/serverSideTranslations";
3 |
4 |
5 | const Home = () => {
6 | const { t } = useTranslation("home");
7 |
8 | return (
9 |
10 |
11 |
12 | ...(await serverSideTranslations(locale, ["common", "test", "home"])),
13 |
14 |
15 |
16 |
17 |
18 | {t("shop-now")}
19 |
20 |
21 |
22 |
23 |
24 |
{t("welcome.para1")}
25 |
{t("welcome.para2")}
26 |
{t("welcome.para3")}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "es5",
6 | "dom"
7 | ],
8 | "types": [
9 | "cypress",
10 | "node"
11 | ],
12 | "baseUrl": "./",
13 | "allowJs": true,
14 | "skipLibCheck": true,
15 | "strict": false,
16 | "forceConsistentCasingInFileNames": true,
17 | "noEmit": true,
18 | "incremental": true,
19 | "esModuleInterop": true,
20 | "module": "esnext",
21 | "moduleResolution": "node",
22 | "resolveJsonModule": true,
23 | "isolatedModules": true,
24 | "jsx": "preserve"
25 | },
26 | "include": [
27 | "**/*.ts",
28 | "**/*.js"
29 | ],
30 | "exclude": [
31 | "node_modules"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/Ch11_Code_Nextjs_eCommerce/Ch11_Code_Nextjs_eCommerce/translating into dutch/nl-NL/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "luxury": "Luxe Macarons met de hand gemaakt",
3 | "shop-now": "Winkel nu",
4 | "welcome": {
5 | "para1": "Welkom bij Macaron Magic - de thuisbasis van heerlijk smakende, luxe macarons, met de hand gemaakt hier in onze werkplaats in het Peak District.",
6 | "para2": "We hebben zorgvuldig een selectie van smaken uitgekozen voor uw plezier, klaar om van te genieten - stel u eens voor... dat u in elke smaak bijt, waar het praktisch smelt in uw mond... jammie!",
7 | "para3": "Blader om te beginnen naar onze winkel waar u het volledige beschikbare assortiment ziet - we zullen er in de loop van de tijd meer aan toevoegen. Als u vragen heeft, laat het ons dan weten - onze contactgegevens staan onderaan deze pagina."
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Ch8_Code_Nextjs_eCommerce/Ch8_Code_Nextjs_eCommerce/next-seo.config file.txt:
--------------------------------------------------------------------------------
1 | export default {
2 | openGraph: {
3 | type: "website",
4 | locale: "en_IE",
5 | url: "https://www.url.ie/",
6 | siteName: "SiteName",
7 | },
8 | twitter: {
9 | handle: "@handle",
10 | site: "@site",
11 | cardType: "summary_large_image",
12 | },
13 | };
14 |
15 |
16 |
17 |
32 |
--------------------------------------------------------------------------------
/Ch12_Code_Nextjs_eCommerce/Ch12_Code_Nextjs_eCommerce/adding account page/styles for global.css.txt:
--------------------------------------------------------------------------------
1 | /* CHANGES FOR YOUR ACCOUNT */
2 | .your-account {
3 | width: 70%;
4 | margin: 0 auto;
5 | text-align: center;
6 | }
7 |
8 | .your-account p {
9 | margin: 40px 0 0 0;
10 | font-family: "Oswald", sans-serif;
11 | font-size: 42px;
12 | }
13 |
14 | .display-profile {
15 | width: 60%;
16 | margin: 20px auto 50px auto;
17 | text-align: center;
18 | }
19 |
20 | .display-profile div:nth-child(1) {
21 | display: flex;
22 | }
23 |
24 | .profile-image {
25 | margin-right: 20px;
26 | }
27 |
28 | .profile-image img {
29 | width: 100px;
30 | height: 100px;
31 | }
32 |
33 | .profile-details {
34 | text-align: left;
35 | border-left: 1px solid #dcdcdc;
36 | display: block;
37 | padding-left: 15px;
38 | min-height: 200px;
39 | }
--------------------------------------------------------------------------------
/Ch6_Code_Nextjs_eCommerce/Ch6_Code_Nextjs_eCommerce/mobile.css:
--------------------------------------------------------------------------------
1 | @media (max-width: 414px) {
2 | html {
3 | display: flex;
4 | width: 100%;
5 | }
6 |
7 | footer > div:nth-child(1) {
8 | padding: 30px;
9 | }
10 |
11 | .product-detail-container {
12 | margin: 60px;
13 | }
14 |
15 | .cart-wrapper {
16 | top: 190px;
17 | width: 100%;
18 | min-height: 2000px;
19 | }
20 |
21 | @keyframes movemobile {
22 | 0% {
23 | transform: translateY(-40%);
24 | }
25 | 100% {
26 | transform: translateY(0%);
27 | }
28 | }
29 |
30 | .cart-container {
31 | min-height: 500px;
32 | width: 100%;
33 | animation-name: movemobile;
34 | animation-duration: 2s;
35 | animation-iteration-count: 1;
36 | animation-fill-mode: forwards;
37 | display: flex;
38 | flex-direction: column;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/components/Newsletter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import newsletter from "../../public/images/newsletter.jpg";
4 |
5 | const Newsletter = () => (
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default Newsletter;
22 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/components/PerfectBanner.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import perfect from "../../public/images/perfect.jpg";
4 |
5 | const PerfectBanner = () => (
6 |
7 |
8 |
9 |
10 |
11 |
Perfect for
12 |
special occasions
13 |
14 | Share the love and give every guest a little explosion of sweetness with
15 | our show stopping macaron towers. Perfect for weddings, anniversaries
16 | and parties. You could even add a touch of luxury to party bags and
17 | wedding favors with these perfect bite sized treats.
18 |
19 |
20 |
21 | );
22 |
23 | export default PerfectBanner;
24 |
--------------------------------------------------------------------------------
/Ch11_Code_Nextjs_eCommerce/Ch11_Code_Nextjs_eCommerce/guidance - converting pages/snippets for converting pages.txt:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "next-i18next";
2 | import { serverSideTranslations } from "next-i18next/serverSideTranslations";
3 |
4 |
5 | const { t } = useTranslation("home");
6 |
7 |
8 |
9 | const Home = () => {
10 | const { t } = useTranslation("home");
11 |
12 | return (
13 | <>
14 | ...
15 | >
16 | );
17 | };
18 |
19 |
20 |
21 | export async function getServerSideProps({ locale }) {
22 | return {
23 | props: {
24 | ...(await serverSideTranslations(locale, [
25 | "home",
26 | "demobanner",
27 | "navbar",
28 | "perfect",
29 | "newsletter",
30 | "footer",
31 | "minicart",
32 | "emptycart",
33 | ])),
34 | // Will be passed to the page component as props
35 | },
36 | };
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/Ch7_Code_Nextjs_eCommerce/Ch7_Code_Nextjs_eCommerce/cancel page/canceled.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Link from "next/link";
3 |
4 | import { useStateContext } from "../context/StateContext";
5 |
6 | const Canceled = () => {
7 | const { setCartItems, setTotalPrice, setTotalQuantities } = useStateContext();
8 |
9 | useEffect(() => {
10 | localStorage.clear();
11 | setCartItems([]);
12 | setTotalPrice(0);
13 | setTotalQuantities(0);
14 | }, []);
15 |
16 | return (
17 |
18 |
19 |
Your order is canceled - you have not been charged.
20 |
21 |
22 | Continue Shopping
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Canceled;
31 |
--------------------------------------------------------------------------------
/Ch6_Code_Nextjs_eCommerce/Ch6_Code_Nextjs_eCommerce/tablet.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 414px) and (max-width: 820px) {
2 | body {
3 | width: 100%;
4 | display: flex;
5 | }
6 |
7 | .product-detail-container {
8 | margin: 0;
9 | padding: 40px;
10 | }
11 |
12 | footer > div:nth-child(1) {
13 | padding: 30px;
14 | }
15 |
16 | .cart-wrapper {
17 | top: 190px;
18 | width: 100%;
19 | min-height: 2000px;
20 | }
21 |
22 | @keyframes movemobile {
23 | 0% {
24 | transform: translateY(-40%);
25 | }
26 | 100% {
27 | transform: translateY(0%);
28 | }
29 | }
30 |
31 | .cart-container {
32 | height: 0;
33 | min-height: 500px;
34 | width: 100%;
35 | animation-name: movemobile;
36 | animation-duration: 2s;
37 | animation-iteration-count: 1;
38 | animation-fill-mode: forwards;
39 | display: flex;
40 | flex-direction: column;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Ch3_Code_Nextjs_eCommerce/Ch3_Code_Nextjs_eCommerce/building shop/shop.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { client } from "../../lib/client";
3 |
4 | import { Product } from "../components";
5 |
6 | const Shop = ({ products }) => (
7 |
8 |
9 |
Shop
10 |
Browse for products
11 |
12 |
13 | {products?.map((product) => (
14 |
15 | ))}
16 |
17 |
18 | );
19 |
20 | export const getServerSideProps = async ({ req, res }) => {
21 | const query = '*[_type == "product"]';
22 | const products = await client.fetch(query);
23 |
24 | res.setHeader(
25 | "Cache-Control",
26 | "public, s-maxage=10, stale-while-revalidate=59"
27 | );
28 |
29 | return {
30 | props: { products },
31 | };
32 | };
33 |
34 | export default Shop;
35 |
--------------------------------------------------------------------------------
/Ch8_Code_Nextjs_eCommerce/Ch8_Code_Nextjs_eCommerce/adding dynamic names.txt:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { NextSeo } from "next-seo";
3 |
4 |
5 | function toTitleCase(str) {
6 | return str.replace(/\w\S*/g, function (txt) {
7 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
8 | });
9 | }
10 |
11 |
12 | const Product = ({ product: { image, name, slug, price } }) => {
13 | const { asPath } = useRouter();
14 |
15 | let seoProductSlug = asPath.split("/")[2];
16 | let seoProductName = "";
17 |
18 | if (seoProductSlug != null) {
19 | seoProductName = seoProductSlug.replace("-", " ");
20 |
21 | if (seoProductSlug === slug.current) {
22 | seoProductName = toTitleCase(seoProductSlug.replace("-", " "));
23 | }
24 | }
25 |
26 |
27 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Ch3_Code_Nextjs_eCommerce/Ch3_Code_Nextjs_eCommerce/creating individual page/Info.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
3 |
4 | const Info = ({ ingredients, weight, delivery }) => (
5 |
6 |
7 | Description
8 | Additional Information
9 |
10 |
11 |
12 | Ingredients
13 | {ingredients}
14 |
15 |
16 | Additional Information
17 |
18 |
19 |
20 | Weight:
21 | {weight}
22 |
23 |
24 | Delivery:
25 |
26 | {delivery}
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 |
35 | export default Info;
36 |
--------------------------------------------------------------------------------
/Ch7_Code_Nextjs_eCommerce/Ch7_Code_Nextjs_eCommerce/updated star review/StarRating.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const StarRating = () => {
4 | const [rating, setRating] = useState(0);
5 | const [hover, setHover] = useState(0);
6 |
7 | return (
8 |
9 | {[...Array(5)].map((star, index) => {
10 | index += 1;
11 | return (
12 | setRating(index)}
17 | onDoubleClick={() => {
18 | setRating(0);
19 | setHover(0);
20 | }}
21 | onMouseEnter={() => setHover(index)}
22 | onMouseLeave={() => setHover(rating)}
23 | >
24 | ★
25 |
26 | );
27 | })}
28 |
29 | );
30 | };
31 |
32 | export default StarRating;
33 |
--------------------------------------------------------------------------------
/Ch11_Code_Nextjs_eCommerce/Ch11_Code_Nextjs_eCommerce/changes for adding locale support/snippets for adding locale support.txt:
--------------------------------------------------------------------------------
1 | i18n: {
2 | defaultLocale: "en",
3 | locales: ["en", "nl-NL"],
4 | },
5 |
6 |
7 | import { useRouter } from "next/router";
8 |
9 |
10 | Hello world: {locale}
11 |
12 |
13 | const handleLocaleChange = (event) => {
14 | const value = event.target.value;
15 |
16 | router.push(router.route, router.asPath, {
17 | locale: value,
18 | });
19 | };
20 |
21 |
22 |
23 | Home
24 |
25 |
26 |
30 | About
31 |
32 |
33 |
34 | English
35 | Nederlands
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/cypress/e2e/component/info.cy.js:
--------------------------------------------------------------------------------
1 | import Info from "../../src/components/Info.jsx";
2 |
3 | const mockTextFirstTab = "Description";
4 | const mockTextSecondTab = "Additional Information";
5 |
6 | describe(" ", () => {
7 | it("should render and display expected content", () => {
8 | // Mount the Info component
9 | cy.mount( );
10 |
11 | // The component should contain an element of "footer"
12 | cy.get("div.react-tabs").should("be.visible");
13 |
14 | // The component should contain 2 tabs
15 | cy.get("ul[role='tablist'] > li").should("have.length", 2);
16 |
17 | // Validate that four links with the expected URLs are present
18 | cy.get("ul[role='tablist'] > li:nth-child(1)").should(
19 | "have.text",
20 | mockTextFirstTab
21 | );
22 |
23 | // Validate that four links with the expected URLs are present
24 | cy.get("ul[role='tablist'] > li:nth-child(2)").should(
25 | "have.text",
26 | mockTextSecondTab
27 | );
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/Ch6_Code_Nextjs_eCommerce/Ch6_Code_Nextjs_eCommerce/portable.css:
--------------------------------------------------------------------------------
1 | @keyframes movemobile {
2 | 0% {
3 | transform: translateY(-40%);
4 | }
5 | 100% {
6 | transform: translateY(0%);
7 | }
8 | }
9 |
10 | @media (max-width: 820px) {
11 | html {
12 | display: flex;
13 | width: 100%;
14 | }
15 |
16 | footer > div:nth-child(1) {
17 | padding: 30px;
18 | }
19 |
20 | .cart-wrapper {
21 | top: 190px;
22 | width: 100%;
23 | min-height: 2000px;
24 | }
25 |
26 | .cart-container {
27 | min-height: 500px;
28 | width: 100%;
29 | animation-name: movemobile;
30 | animation-duration: 2s;
31 | animation-iteration-count: 1;
32 | animation-fill-mode: forwards;
33 | display: flex;
34 | flex-direction: column;
35 | }
36 | }
37 |
38 | @media (max-width: 414px) {
39 | .product-detail-container {
40 | margin: 60px;
41 | }
42 | }
43 |
44 | @media (min-width: 414px) and (max-width: 820px) {
45 | .product-detail-container {
46 | margin: 0;
47 | padding: 40px;
48 | }
49 |
50 | .cart-container {
51 | height: 0;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/pages/about.js:
--------------------------------------------------------------------------------
1 | const About = () => (
2 |
3 |
About Macaron Magic
4 |
5 |
6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vulputate
7 | justo ac tellus egestas dictum. Aenean vestibulum diam eu risus cursus
8 | mollis. Nulla dapibus ante in felis vulputate mattis. Mauris sed lacus
9 | eget sapien rutrum interdum. Proin a semper magna. Aliquam eget purus
10 | fringilla, rutrum augue at, porta sem.
11 |
12 |
13 |
14 | Aenean aliquam lectus tellus, sed venenatis nulla pretium ac. Vivamus
15 | cursus purus quam, eu placerat est rutrum quis. Nunc sit amet urna sed
16 | libero auctor imperdiet nec consequat sem. Quisque mauris est, fermentum
17 | in tristique quis, tincidunt a odio. Fusce porttitor eu est interdum
18 | consectetur. Ut aliquam semper dui, vulputate hendrerit sapien interdum
19 | sit amet. Nam vitae nulla et lorem ornare auctor. Nulla facilisi. Nam sed
20 | sollicitudin leo.
21 |
22 |
23 | );
24 |
25 | export default About;
26 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { AiOutlineShopping } from "react-icons/ai";
4 | import { Cart } from "./";
5 |
6 | import { useStateContext } from "../../context/StateContext";
7 |
8 | const NavBar = () => {
9 | const { showCart, setShowCart, totalQuantities } = useStateContext();
10 |
11 | return (
12 |
13 |
14 |
Mangez Macaron
15 |
16 |
17 |
Home
18 |
About
19 |
Shop
20 |
Contact
21 |
setShowCart(true)}
25 | >
26 |
27 | {totalQuantities}
28 |
29 | {showCart &&
}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default NavBar;
37 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding context/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { AiOutlineShopping } from "react-icons/ai";
4 | import { Cart } from "./";
5 |
6 | import { useStateContext } from "../../context/StateContext";
7 |
8 | const NavBar = () => {
9 | const { showCart, setShowCart, totalQuantities } = useStateContext();
10 |
11 | return (
12 |
13 |
14 |
Mangez Macaron
15 |
16 |
17 |
Home
18 |
About
19 |
Shop
20 |
Contact
21 |
setShowCart(true)}
25 | >
26 |
27 | {totalQuantities}
28 |
29 | {showCart &&
}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default NavBar;
37 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/constructing the cart/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { AiOutlineShopping } from "react-icons/ai";
4 | import { Cart } from "./";
5 |
6 | import { useStateContext } from "../../context/StateContext";
7 |
8 | const NavBar = () => {
9 | const { showCart, setShowCart, totalQuantities } = useStateContext();
10 |
11 | return (
12 |
13 |
14 |
Mangez Macaron
15 |
16 |
17 |
Home
18 |
About
19 |
Shop
20 |
Contact
21 |
setShowCart(true)}
25 | >
26 |
27 | {totalQuantities}
28 |
29 | {showCart &&
}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default NavBar;
37 |
--------------------------------------------------------------------------------
/Ch5_Code_Nextjs_eCommerce/Ch5_Code_Nextjs_eCommerce/alternative method - css modules/Footer/Footer.module.css:
--------------------------------------------------------------------------------
1 | .footerContainer {
2 | color: #caa34d;
3 | background-color: #000000;
4 | text-align: center;
5 | margin-top: 20px;
6 | padding: 30px 10px;
7 | font-weight: 700;
8 | display: flex;
9 | flex-direction: column;
10 | align-items: center;
11 | gap: 10px;
12 | justify-content: center;
13 | border-bottom: 1px solid #c7c7c7;
14 | }
15 |
16 | .footerContainer .footerContent {
17 | display: flex;
18 | margin-left: auto;
19 | margin-right: auto;
20 | width: 800px;
21 | }
22 |
23 | .footerContainer .footerContent div {
24 | flex-direction: column;
25 | display: flex;
26 | text-align: left;
27 | min-width: 250px;
28 | margin-right: 20px;
29 | }
30 |
31 | .footerContainer .footerContent div:last-child {
32 | margin-right: 0;
33 | }
34 |
35 | .footerContainer .mini-cart-bottom {
36 | width: 100%;
37 | }
38 |
39 | .footerContainer .icons {
40 | font-size: 30px;
41 | display: flex;
42 | gap: 10px;
43 | }
44 |
45 | .footerContainer .iconContainer {
46 | width: 800px;
47 | display: flex;
48 | justify-content: space-between;
49 | }
50 |
51 | .copyright {
52 | margin: 10px;
53 | }
54 |
--------------------------------------------------------------------------------
/Ch12_Code_Nextjs_eCommerce/Ch12_Code_Nextjs_eCommerce/adding account page/DisplayProfile.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSession, signIn } from "next-auth/react";
3 |
4 | function DisplayProfile() {
5 | const { data: session } = useSession();
6 |
7 | const handleSignin = (e) => {
8 | e.preventDefault();
9 | signIn();
10 | };
11 |
12 | return (
13 | <>
14 |
38 | >
39 | );
40 | }
41 |
42 | export default DisplayProfile;
43 |
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/cypress/e2e/addingProducts.cy.js:
--------------------------------------------------------------------------------
1 | describe("Adding Products", () => {
2 | it("should navigate to the about page", () => {
3 | // Start from the index page
4 | cy.visit("http://localhost:3000/");
5 |
6 | // Click on the Shop link
7 | cy.get("div.navbar > a:nth-child(3)").click(true);
8 |
9 | // The new url should include "/shop"
10 | cy.url().should("include", "/shop");
11 |
12 | // Click on a product - Spiced Pumpkin
13 | cy.get('a[href*="spiced-pumpkin"]').click();
14 |
15 | // Increase product quantity by 2
16 | cy.get("span.plus").click();
17 |
18 | // Click on Add to Cart
19 | cy.get("button.add-to-cart").click();
20 |
21 | // Verify that cart icon shows 2
22 | cy.get("span.cart-item-qty").contains(2);
23 |
24 | // Open cart - verify details
25 | cy.get("span.cart-item-qty").click();
26 | cy.get("span.cart-num-items").contains("2 items");
27 |
28 | // Verify mini cart shows relevant details
29 | cy.get("span.cart-num-items").contains("(2 items)");
30 | cy.get("span.item-desc > span").contains("Spiced Pumpkin");
31 | cy.get("span.totals").contains("$18.50");
32 | cy.get("div.total > h3").contains("$37.00");
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding stripe/lib/utils.js:
--------------------------------------------------------------------------------
1 | import confetti from "canvas-confetti";
2 |
3 | export const runFireworks = () => {
4 | var duration = 5 * 1000;
5 | var animationEnd = Date.now() + duration;
6 | var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
7 |
8 | function randomInRange(min, max) {
9 | return Math.random() * (max - min) + min;
10 | }
11 |
12 | var interval = setInterval(function () {
13 | var timeLeft = animationEnd - Date.now();
14 |
15 | if (timeLeft <= 0) {
16 | return clearInterval(interval);
17 | }
18 |
19 | var particleCount = 50 * (timeLeft / duration);
20 | // since particles fall down, start a bit higher than random
21 | confetti(
22 | Object.assign({}, defaults, {
23 | particleCount,
24 | origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
25 | })
26 | );
27 | confetti(
28 | Object.assign({}, defaults, {
29 | particleCount,
30 | origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
31 | })
32 | );
33 | }, 250);
34 | };
35 |
36 | export const eUSLocale = (x) => {
37 | return x.toLocaleString("en-US", {
38 | maximumFractionDigits: 2,
39 | minimumFractionDigits: 2,
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/Ch12_Code_Nextjs_eCommerce/Ch12_Code_Nextjs_eCommerce/adding login options/DemoBanner.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSession, signIn, signOut } from "next-auth/react";
3 |
4 | const DemoBanner = () => {
5 | const { data: session } = useSession();
6 |
7 | const handleSignin = (e) => {
8 | e.preventDefault();
9 | signIn();
10 | };
11 | const handleSignout = (e) => {
12 | e.preventDefault();
13 | signOut();
14 | };
15 |
16 | return (
17 |
18 |
19 | This is a demo store - no orders will be accepted or delivered
20 |
21 |
22 |
23 | {session && (
24 | <>
25 |
26 | Welcome, {session.user.name ?? session.user.email}
27 |
28 | Sign out
29 |
30 | >
31 | )}
32 |
33 | {!session && (
34 | <>
35 | Welcome
36 |
37 | Sign in
38 |
39 | >
40 | )}
41 |
42 |
43 | );
44 | };
45 |
46 | export default DemoBanner;
47 |
--------------------------------------------------------------------------------
/Ch3_Code_Nextjs_eCommerce/Ch3_Code_Nextjs_eCommerce/configuring Sanity/product.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'product',
3 | title: 'Product',
4 | type: 'document',
5 | fields: [
6 | {
7 | name: 'image',
8 | title: 'Image',
9 | type: 'array',
10 | of: [{type: 'image'}],
11 | options: {
12 | hotspot: true,
13 | },
14 | },
15 | {
16 | name: 'name',
17 | title: 'Name',
18 | type: 'string',
19 | },
20 | {
21 | name: 'slug',
22 | title: 'Slug',
23 | type: 'slug',
24 | options: {
25 | source: 'name',
26 | maxLength: 90,
27 | },
28 | },
29 | {
30 | name: 'price',
31 | title: 'Price',
32 | type: 'number',
33 | },
34 | {
35 | name: 'details',
36 | title: 'Details',
37 | type: 'string',
38 | },
39 | {
40 | name: 'sku',
41 | title: 'SKU',
42 | type: 'string',
43 | },
44 | {
45 | name: 'ingredients',
46 | title: 'Ingredients',
47 | type: 'string',
48 | },
49 | {
50 | name: 'weight',
51 | title: 'Weight',
52 | type: 'string',
53 | },
54 | {
55 | name: 'delivery',
56 | title: 'Delivery',
57 | type: 'string',
58 | },
59 | ],
60 | }
61 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Toaster } from "react-hot-toast";
3 | import { Layout } from "../components";
4 | import { DefaultSeo } from "next-seo";
5 |
6 | import "../styles/globals.css";
7 | import "../styles/index.scss"; /* main styles */
8 | // import "../styles/overrides/mobile.css"; /* styles for cell phones */
9 | // import "../styles/overrides/tablet.css"; /* styles for cell phones */
10 | import "../styles/overrides/portable.css";
11 |
12 | import { StateContext } from "../../context/StateContext";
13 |
14 | function MyApp({ Component, pageProps }) {
15 | return (
16 | <>
17 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | >
39 | );
40 | }
41 |
42 | export default MyApp;
43 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding context/_app.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Toaster } from "react-hot-toast";
3 | import { Layout } from "../components";
4 | import { DefaultSeo } from "next-seo";
5 |
6 | import "../styles/globals.css";
7 | import "../styles/index.scss"; /* main styles */
8 | // import "../styles/overrides/mobile.css"; /* styles for cell phones */
9 | // import "../styles/overrides/tablet.css"; /* styles for cell phones */
10 | import "../styles/overrides/portable.css";
11 |
12 | import { StateContext } from "../../context/StateContext";
13 |
14 | function MyApp({ Component, pageProps }) {
15 | return (
16 | <>
17 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | >
39 | );
40 | }
41 |
42 | export default MyApp;
43 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import PaymentIcons from "../PaymentIcons";
4 | import MiniCart from "../MiniCart";
5 |
6 | import { useStateContext } from "../../../context/StateContext";
7 |
8 | import { AiFillInstagram, AiOutlineTwitter } from "react-icons/ai";
9 |
10 | import styles from "./Footer.module.css";
11 |
12 | const Footer = () => {
13 | const { showCart } = useStateContext();
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 | Delivery
21 | Privacy
22 | Terms and Conditions of Sale
23 | Contact Us
24 |
25 |
Contact: hello@macaronmagic.com
26 |
27 |
28 |
29 |
36 |
37 | 2022 Macaron Magic All rights reserved
38 | >
39 | );
40 | };
41 |
42 | export default Footer;
43 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Freeware License, some rights reserved
2 |
3 | Copyright (c) 2023 Alex Libby
4 |
5 | Permission is hereby granted, free of charge, to anyone obtaining a copy
6 | of this software and associated documentation files (the "Software"),
7 | to work with the Software within the limits of freeware distribution and fair use.
8 | This includes the rights to use, copy, and modify the Software for personal use.
9 | Users are also allowed and encouraged to submit corrections and modifications
10 | to the Software for the benefit of other users.
11 |
12 | It is not allowed to reuse, modify, or redistribute the Software for
13 | commercial use in any way, or for a user’s educational materials such as books
14 | or blog articles without prior permission from the copyright holder.
15 |
16 | The above copyright notice and this permission notice need to be included
17 | in all copies or substantial portions of the software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding context/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import PaymentIcons from "../PaymentIcons";
4 | import MiniCart from "../MiniCart";
5 |
6 | import { useStateContext } from "../../../context/StateContext";
7 |
8 | import { AiFillInstagram, AiOutlineTwitter } from "react-icons/ai";
9 |
10 | import styles from "./Footer.module.css";
11 |
12 | const Footer = () => {
13 | // const { showCart } = useStateContext();
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 | Delivery
21 | Privacy
22 | Terms and Conditions of Sale
23 | Contact Us
24 |
25 |
Contact: hello@macaronmagic.com
26 |
27 |
28 |
29 |
36 |
37 | 2022 Macaron Magic All rights reserved
38 | >
39 | );
40 | };
41 |
42 | export default Footer;
43 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding stripe/success.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import Link from "next/link";
3 | import { BsBagCheckFill } from "react-icons/bs";
4 |
5 | import { useStateContext } from "../context/StateContext";
6 | import { runFireworks } from "../lib/utils";
7 |
8 | const Success = () => {
9 | const { setItems, setTotalPrice, setTotalQuantities } = useStateContext();
10 |
11 | useEffect(() => {
12 | localStorage.clear();
13 | setCartItems([]);
14 | setTotalPrice(0);
15 | setTotalQuantities(0);
16 | runFireworks();
17 | }, []);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
Thank you for your order!
26 |
Check your email inbox for the receipt.
27 |
28 | If you have any questions, please email
29 |
30 | hello@macaronmagic.com
31 |
32 |
33 |
34 |
35 | Continue Shopping
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default Success;
44 |
--------------------------------------------------------------------------------
/Ch5_Code_Nextjs_eCommerce/Ch5_Code_Nextjs_eCommerce/alternative method - css modules/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import PaymentIcons from "../PaymentIcons";
4 | import MiniCart from "../MiniCart";
5 |
6 | import { useStateContext } from "../../../context/StateContext";
7 |
8 | import { AiFillInstagram, AiOutlineTwitter } from "react-icons/ai";
9 |
10 | import styles from "./Footer.module.css";
11 |
12 | const Footer = () => {
13 | // const { showCart } = useStateContext();
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 | Delivery
21 | Privacy
22 | Terms and Conditions of Sale
23 | Contact Us
24 |
25 |
Contact: hello@macaronmagic.com
26 |
27 |
28 |
29 |
36 |
37 | 2022 Macaron Magic All rights reserved
38 | >
39 | );
40 | };
41 |
42 | export default Footer;
43 |
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/cypress/e2e/removingProducts.cy.js:
--------------------------------------------------------------------------------
1 | describe("Adding Products", () => {
2 | it("should navigate to the about page", () => {
3 | // Start from the index page
4 | cy.visit("http://localhost:3000/");
5 |
6 | // Click on the Shop link
7 | cy.get("div.navbar > a:nth-child(3)").click(true);
8 |
9 | // The new url should include "/shop"
10 | cy.url().should("include", "/shop");
11 |
12 | // Click on a product - Spiced Pumpkin
13 | cy.get('a[href*="spiced-pumpkin"]').click();
14 |
15 | // Increase product quantity by 2
16 | cy.get("span.plus").click();
17 |
18 | // Click on Add to Cart
19 | cy.get("button.add-to-cart").click();
20 |
21 | // Verify that cart icon shows 2
22 | cy.get("span.cart-item-qty").contains(2);
23 |
24 | // Open cart to remove item
25 | cy.get("span.cart-item-qty").click();
26 |
27 | // Remove one item from basket
28 | cy.get("span.minus").eq(0).click();
29 |
30 | // Close Cart
31 | cy.get("div.cart-wrapper").click();
32 |
33 | // Verify that selected item shows 1
34 | cy.get("span.cart-num-items").contains("1 items");
35 |
36 | // Verify mini cart shows relevant details
37 | cy.get("span.cart-num-items").contains("(1 items)");
38 | cy.get("span.item-desc > span").contains("Spiced Pumpkin");
39 | cy.get("span.totals").contains("$18.50");
40 | cy.get("div.total > h3").contains("$18.50");
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/pages/contact.js:
--------------------------------------------------------------------------------
1 | const Contact = () => (
2 |
3 |
Contact Us
4 |
5 | If you have an enquiry about any of our products, we'd love to hear from
6 | you.
7 |
8 |
47 |
48 | );
49 |
50 | export default Contact;
51 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | // import { client } from "../lib/client";
4 | import PerfectBanner from "../components/PerfectBanner";
5 | import Newsletter from "../components/Newsletter";
6 |
7 | const Home = () => (
8 |
9 |
10 |
11 | Luxury macarons made by hand
12 |
13 |
14 | Shop Now
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Welcome to Macaron Magic - the home of great-tasting, luxurious
23 | macarons, made by hand here in our workshop in the Peak District.
24 |
25 |
26 | We have carefully chosen a select range of flavors for your delight,
27 | ready for you to enjoy - just imagine...biting into each one, where it
28 | practically melts in your mouth...yum!
29 |
30 |
31 | To start, browse over to our shop where you will see the full range
32 | available - we'll be adding more over time. If you have any questions,
33 | please do let us know - our contact details are at the bottom of this
34 | page.
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 |
43 | /* ADD SERVERSIDE PROPS HERE */
44 |
45 | export default Home;
46 |
--------------------------------------------------------------------------------
/Ch7_Code_Nextjs_eCommerce/Ch7_Code_Nextjs_eCommerce/flipping product images/flipping product images.txt:
--------------------------------------------------------------------------------
1 | /* FLIP IMAGES */
2 | .fliptile {
3 | color: #fff;
4 | position: relative;
5 | overflow: hidden;
6 | margin: 10px;
7 | min-width: 220px;
8 | max-width: 310px;
9 | width: 100%;
10 | color: #000000;
11 | text-align: left;
12 | font-size: 16px;
13 | perspective: 50em;
14 | }
15 |
16 | .fliptile * {
17 | box-sizing: padding-box;
18 | transition: all 0.2s ease-out;
19 | }
20 |
21 | .fliptile img {
22 | max-width: 100%;
23 | vertical-align: top;
24 | }
25 |
26 | .fliptile figcaption {
27 | top: 20px;
28 | left: 20px;
29 | right: 20px;
30 | bottom: 20px;
31 | padding: 20px;
32 | position: absolute;
33 | opacity: 0;
34 | z-index: 1;
35 | transform: translateY(40px);
36 | }
37 |
38 | .fliptile h2 {
39 | margin: 0 0 5px;
40 | }
41 |
42 | .fliptile h2 {
43 | font-weight: 600;
44 | }
45 |
46 | .fliptile:after {
47 | background-color: rgb(0, 0, 0, 0.1);
48 | position: absolute;
49 | content: "";
50 | display: block;
51 | top: 20px;
52 | left: 20px;
53 | right: 20px;
54 | bottom: 20px;
55 | transition: all 0.4s ease-in-out;
56 | transform: rotateX(-90deg);
57 | transform-origin: 50% 50%;
58 | opacity: 0;
59 | }
60 |
61 | .fliptile:hover figcaption,
62 | .fliptile.hover figcaption {
63 | transform: translateY(0%);
64 | opacity: 1;
65 | transition-delay: 0.2s;
66 | }
67 |
68 | .fliptile:hover:after,
69 | .fliptile.hover:after {
70 | transform: rotateX(0);
71 | opacity: 0.9;
72 | }
73 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Head from "next/head";
3 |
4 | import NavBar from "./NavBar";
5 | import DemoBanner from "./DemoBanner";
6 | import Footer from "./Footer";
7 |
8 | const Layout = ({ children }) => {
9 | return (
10 | <>
11 |
12 | Macaron Magic | great tasting home-made macarons
13 |
18 |
24 |
30 |
31 |
36 |
37 |
38 |
39 |
43 |
44 | {children}
45 |
48 |
49 | >
50 | );
51 | };
52 |
53 | export default Layout;
54 |
--------------------------------------------------------------------------------
/Ch5_Code_Nextjs_eCommerce/Ch5_Code_Nextjs_eCommerce/alternative method - css modules/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Head from "next/head";
3 |
4 | import NavBar from "./NavBar";
5 | import DemoBanner from "./DemoBanner";
6 | import Footer from "./Footer/Footer";
7 |
8 | const Layout = ({ children }) => {
9 | return (
10 | <>
11 |
12 | Macaron Magic | great tasting home-made macarons
13 |
18 |
24 |
30 |
31 |
36 |
37 |
38 |
39 |
43 |
44 | {children}
45 |
48 |
49 | >
50 | );
51 | };
52 |
53 | export default Layout;
54 |
--------------------------------------------------------------------------------
/Ch11_Code_Nextjs_eCommerce/Ch11_Code_Nextjs_eCommerce/adapting slug.js/Adapting slug.js.txt:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "next-i18next";
2 | import { serverSideTranslations } from "next-i18next/serverSideTranslations";
3 |
4 |
5 | const { t } = useTranslation("demobanner");
6 |
7 |
8 |
9 | const Home = () => {
10 | const { t } = useTranslation("home");
11 |
12 | return (
13 | <>
14 | ...
15 | >
16 | );
17 | };
18 |
19 |
20 |
21 | export async function getServerSideProps({ locale, params: { slug } }) {
22 | // getStaticPaths()
23 | const query = `*[_type == "product"] {
24 | slug {
25 | current
26 | }
27 | }
28 | `;
29 |
30 | const productPaths = await client.fetch(query);
31 |
32 | const paths = productPaths.map((product) => ({
33 | params: {
34 | slug: product.slug.current,
35 | },
36 | }));
37 |
38 | // getStaticProps()
39 | const query2 = `*[_type == "product" && slug.current == '${slug}'][0]`;
40 | const productsQuery = '*[_type == "product"]';
41 |
42 | const product = await client.fetch(query2);
43 | const products = await client.fetch(productsQuery);
44 |
45 | return {
46 | props: {
47 | paths,
48 | fallback: "blocking",
49 | products,
50 | product,
51 | ...(await serverSideTranslations(locale, [
52 | "demobanner",
53 | "navbar",
54 | "cart",
55 | "footer",
56 | "slug",
57 | "minicart",
58 | "emptycart",
59 | "shop",
60 | "product",
61 | "info",
62 | ])),
63 | // Will be passed to the page component as props
64 | },
65 | };
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/Ch11_Code_Nextjs_eCommerce/Ch11_Code_Nextjs_eCommerce/translating into english/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Home",
3 | "about": "About",
4 | "shop": "Shop",
5 | "contact": "Contact",
6 | "demo": "This is a demo store - no orders will be accepted or delivered",
7 | "luxury": "Luxury Macarons made by hand",
8 | "shop-now": "Shop now",
9 | "welcome": {
10 | "para1": "Welcome to Macaron Magic - the home of great-tasting, luxurious macarons, made by hand here in our workshop in the Peak District.",
11 | "para2": "We have carefully chosen a select range of flavors for your delight, ready for you to enjoy - just imagine...biting into each one, where it practically melts in your mouth...yum!",
12 | "para3": "To start, browse over to our shop where you will see the full range available - we'll be adding more over time. If you have any questions, please do let us know - our contact details are at the bottom of this page."
13 | },
14 | "perfect": {
15 | "for": "PERFECT FOR",
16 | "special": "SPECIAL OCCASIONS",
17 | "text": "Share the love and give every guest a little explosion of sweetness with our show stopping macaron towers. Perfect for weddings, anniversaries and parties. You could even add a touch of luxury to party bags and wedding favors with these perfect bite sized treats."
18 | },
19 | "newsletter": {
20 | "firstname": "First name",
21 | "lastname": "Last name",
22 | "email": "Email address"
23 | },
24 | "footer": {
25 | "delivery": "Delivery",
26 | "privacy": "Privacy",
27 | "terms": "Terms and Conditions of Sale",
28 | "contact": "Contact Us",
29 | "email": "Contact: hello@macaron-magic.com",
30 | "empty": "Your shopping bag is empty",
31 | "gotoshop": "Go to Shop"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding stripe/pages/api/stripe.js:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 |
3 | const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY);
4 |
5 | export default async function handler(req, res) {
6 | if (req.method === "POST") {
7 | try {
8 | const params = {
9 | submit_type: "pay",
10 | mode: "payment",
11 | payment_method_types: ["card"],
12 | billing_address_collection: "auto",
13 | shipping_options: [
14 | {
15 | shipping_rate: "shr_1MAwLmIG1cktkZ0RP4YPwGil",
16 | },
17 | ],
18 | line_items: req.body.map((item) => {
19 | const img = item.image[0].asset._ref;
20 | const newImage = img
21 | .replace("image-", "https://cdn.sanity.io/images/g6xvtbtr/")
22 | .replace("-webp", ".webp");
23 |
24 | return {
25 | price_data: {
26 | currency: "gbp",
27 | product_data: {
28 | name: item.name,
29 | images: [newImage],
30 | },
31 | unit_amount: item.price * 100,
32 | },
33 | adjustable_quantity: {
34 | enabled: true,
35 | minimum: 1,
36 | },
37 | quantity: item.quantity,
38 | };
39 | }),
40 | success_url: `${req.headers.origin}/success`,
41 | cancel_url: `${req.headers.origin}/canceled`,
42 | };
43 |
44 | // Create Checkout Sessions from body params.
45 | const session = await stripe.checkout.sessions.create(params);
46 |
47 | res.status(200).json(session);
48 | } catch (err) {
49 | res.status(err.statusCode || 500).json(err.message);
50 | }
51 | } else {
52 | res.setHeader("Allow", "POST");
53 | res.status(405).end("Method Not Allowed");
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Ch9_Code_Nextjs_eCommerce/Ch9_Code_Nextjs_eCommerce/cypress/e2e/component/product.cy.js:
--------------------------------------------------------------------------------
1 | import Product from "../../src/components/Product.jsx";
2 |
3 | const mockProduct = {
4 | _type: "product",
5 | delivery:
6 | "We carefully package our macarons and use Royal Mail to post them to you under first class postage. We only deliver to the UK.",
7 | details:
8 | "Enjoy the taste of our indulgent Chocolate Orange Macarons. Bursting with 100% natural flavours, our perfectly proportioned treats are handcrafted in our kitchen and beautifully packaged for your enjoyment.",
9 | image: [
10 | {
11 | _key: "ba6786ee0e81",
12 | _type: "image",
13 | asset: {
14 | _ref: "image-812a8575cab31a81ea8352e913d173c9244151b7-456x456-jpg",
15 | },
16 | },
17 | ],
18 | ingredients:
19 | "Ground Almonds (contains nuts) , Icing Sugar, Free Range Egg Whites (contains Eggs), Sugar, Milk Chocolate, (Sugar, Cocoa Butter, High Fat Milk Powder, Cocoa Mass, Whole Milk Powder, Skimmed Milk Powder, Lactose (Milk), Emulsifier: Lecithins (Soya); Vanilla Extract), Double Cream (contains Milk), Orange Extract, Colour E110 may have an adverse effect on activity and attention in children Please note: product may contain allergens - if in doubt, please ask.",
20 | name: "Chocolate Orange",
21 | price: 18.5,
22 | sku: "MACM001",
23 | slug: {
24 | current: "chocolate-orange",
25 | },
26 | weight: "335g",
27 | };
28 |
29 | describe(" ", () => {
30 | it("should render and display expected content", () => {
31 | // Mount the DemoBanner component
32 | cy.mount( );
33 |
34 | // The component should contain an element of "footer"
35 | cy.get("div").should("be.visible");
36 |
37 | // Validate that the correct text is displayed in the Product card
38 | cy.get("p.product-name:nth-child(2)").should("have.text", mockProduct.name);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/Ch11_Code_Nextjs_eCommerce/Ch11_Code_Nextjs_eCommerce/changes for adding language support/snippets for adding language support.txt:
--------------------------------------------------------------------------------
1 | "peerDependencies": {
2 | "i18next": "^22.4.15",
3 | "react-i18next": "^12.2.0"
4 | },
5 |
6 |
7 | module.exports = {
8 | i18n: {
9 | defaultLocale: "en",
10 | locales: ["en", "nl-NL"],
11 | },
12 | react: { useSuspense: false },
13 | };
14 |
15 |
16 |
17 | const { i18n } = require("./next-i18next.config");
18 |
19 |
20 |
21 | reactStrictMode: true,
22 | sassOptions: {
23 | includePaths: ["styles"],
24 | },
25 | i18n, <-- ADD THIS LINE
26 | };
27 |
28 |
29 |
30 |
31 | import { appWithTranslation } from "next-i18next";
32 | import nextI18NextConfig from "../../next-i18next.config";
33 |
34 |
35 |
36 | export default appWithTranslation(MyApp, nextI18NextConfig);
37 |
38 |
39 |
40 | import { serverSideTranslations } from "next-i18next/serverSideTranslations";
41 |
42 |
43 |
44 |
45 | export async function getStaticProps({ locale }) {
46 | return {
47 | props: {
48 | ...(await serverSideTranslations(locale, ["common", "test"])),
49 | // Will be passed to the page component as props
50 | },
51 | };
52 | }
53 |
54 |
55 | import { useTranslation } from "next-i18next";
56 | import { serverSideTranslations } from "next-i18next/serverSideTranslations";
57 |
58 |
59 | /* Completed version of the test component code */
60 | const About = () => {
61 | const { t } = useTranslation("test");
62 |
63 | return (
64 |
65 |
About Macaron Magic
66 |
67 |
{t("about")}
68 |
69 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
70 | (...shortened for brevity)
71 |
72 |
73 | );
74 | };
75 |
76 |
77 |
78 | export async function getServerSideProps({ locale }) {
79 | return {
80 | props: {
81 | ...(await serverSideTranslations(locale, ["test"])),
82 | // Will be passed to the page component as props
83 | },
84 | };
85 | }
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Ch3_Code_Nextjs_eCommerce/Ch3_Code_Nextjs_eCommerce/creating individual page/Product.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 |
4 | import { urlFor } from "../../lib/client";
5 | import { ArticleJsonLd, NextSeo } from "next-seo";
6 |
7 | const Product = ({
8 | product: { image, name, slug, price, description, sku },
9 | }) => {
10 | return (
11 | <>
12 |
22 |
23 |
40 |
41 |
42 |
43 |
44 |
45 |
51 |
52 | {name}
53 |
54 |
55 |
{name}
56 |
57 | $
58 | {price.toLocaleString("en-US", {
59 | maximumFractionDigits: 2,
60 | minimumFractionDigits: 2,
61 | })}
62 |
63 |
64 |
65 |
66 | >
67 | );
68 | };
69 |
70 | export default Product;
71 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding a minicart/MiniCart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AiOutlineShopping } from "react-icons/ai";
3 | import toast from "react-hot-toast";
4 | import { useStateContext } from "../../context/StateContext";
5 | import { urlFor } from "../../lib/client";
6 | import getStripe from "../../lib/getStripe";
7 | import { eUSLocale } from "../../lib/utils";
8 | import EmptyCart from "./Cart/EmptyCart";
9 | import Link from "next/link";
10 |
11 | const MiniCart = () => {
12 | const { totalPrice, totalQuantities, cartItems } = useStateContext();
13 |
14 | const handleCheckout = async () => {
15 | const stripe = await getStripe();
16 |
17 | const response = await fetch("/api/stripe", {
18 | method: "POST",
19 | headers: {
20 | "Content-Type": "application/json",
21 | },
22 | body: JSON.stringify(cartItems),
23 | });
24 |
25 | if (response.statusCode === 500) return;
26 |
27 | const data = await response.json();
28 |
29 | toast.loading("Redirecting...");
30 |
31 | stripe.redirectToCheckout({ sessionId: data.id });
32 | };
33 |
34 | return (
35 |
36 |
37 | Your Cart contains {totalQuantities} item
38 | {totalQuantities > 1 || totalQuantities === 0 ? "s" : ""}
39 |
40 |
41 | {cartItems.length < 1 && (
42 |
43 |
44 |
45 | Go to Shop
46 |
47 |
48 |
49 | )}
50 |
51 |
52 | {cartItems.length >= 1 &&
53 | cartItems.map((item) => (
54 |
55 |
56 |
57 |
58 |
59 | {item.name}
60 |
61 | {item.quantity}
62 | x
63 | ${eUSLocale(item.price)}
64 |
65 |
66 |
67 | ))}
68 |
69 | {cartItems.length >= 1 && (
70 |
71 |
72 |
${eUSLocale(totalPrice)}
73 |
74 |
75 |
76 | Pay with Stripe
77 |
78 |
79 |
80 | )}
81 |
82 | );
83 | };
84 |
85 | export default MiniCart;
86 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/refactoring the cart/MiniCart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AiOutlineShopping } from "react-icons/ai";
3 | import toast from "react-hot-toast";
4 | import { useStateContext } from "../../context/StateContext";
5 | import { urlFor } from "../../lib/client";
6 | import getStripe from "../../lib/getStripe";
7 | import { eUSLocale } from "../../lib/utils";
8 | import EmptyCart from "./Cart/EmptyCart";
9 | import Link from "next/link";
10 |
11 | const MiniCart = () => {
12 | const { totalPrice, totalQuantities, cartItems } = useStateContext();
13 |
14 | const handleCheckout = async () => {
15 | const stripe = await getStripe();
16 |
17 | const response = await fetch("/api/stripe", {
18 | method: "POST",
19 | headers: {
20 | "Content-Type": "application/json",
21 | },
22 | body: JSON.stringify(cartItems),
23 | });
24 |
25 | if (response.statusCode === 500) return;
26 |
27 | const data = await response.json();
28 |
29 | toast.loading("Redirecting...");
30 |
31 | stripe.redirectToCheckout({ sessionId: data.id });
32 | };
33 |
34 | return (
35 |
36 |
37 | Your Cart contains {totalQuantities} item
38 | {totalQuantities > 1 || totalQuantities === 0 ? "s" : ""}
39 |
40 |
41 | {cartItems.length < 1 && (
42 |
43 |
44 |
45 | Go to Shop
46 |
47 |
48 |
49 | )}
50 |
51 |
52 | {cartItems.length >= 1 &&
53 | cartItems.map((item) => (
54 |
55 |
56 |
57 |
58 |
59 | {item.name}
60 |
61 | {item.quantity}
62 | x
63 | ${eUSLocale(item.price)}
64 |
65 |
66 |
67 | ))}
68 |
69 | {cartItems.length >= 1 && (
70 |
71 |
72 |
${eUSLocale(totalPrice)}
73 |
74 |
75 |
76 | Pay with Stripe
77 |
78 |
79 |
80 | )}
81 |
82 | );
83 | };
84 |
85 | export default MiniCart;
86 |
--------------------------------------------------------------------------------
/Ch7_Code_Nextjs_eCommerce/Ch7_Code_Nextjs_eCommerce/updated add to cart button/styles for button.txt:
--------------------------------------------------------------------------------
1 | /* UPDATED ADD TO CART BUTTON */
2 | .buttons {
3 | margin-top: 40px;
4 | }
5 | .buttons .add-to-cart,
6 | .buttons .buy-now {
7 | margin-top: 0;
8 | }
9 |
10 | @keyframes shift-left {
11 | 0% {
12 | transform: translateX(0);
13 | }
14 | 100% {
15 | transform: translateX(-40px);
16 | }
17 | }
18 |
19 | @keyframes shift-left-circle {
20 | 0% {
21 | transform: translateX(0);
22 | }
23 | 50% {
24 | transform: translateX(-40px);
25 | }
26 | 100% {
27 | transform: translateX(-40px);
28 | }
29 | }
30 |
31 | @keyframes shift-left-mask {
32 | 0% {
33 | height: 7px;
34 | transform: translateX(0) rotate(0);
35 | }
36 | 50% {
37 | transform: translateX(0) rotate(180deg);
38 | }
39 | 100% {
40 | transform: translateX(-40px) rotate(180deg);
41 | }
42 | }
43 |
44 | .btn-cart {
45 | display: block;
46 | width: 200px;
47 | border: none;
48 | margin: 0 auto;
49 | background: none;
50 | background-color: #ffffff;
51 | font-weight: 500;
52 | color: white;
53 | font-size: 14px;
54 | position: relative;
55 | cursor: pointer;
56 | height: 45px;
57 | border: 1px solid #f02d34;
58 | font-size: 18px;
59 | }
60 |
61 | .btn-cart:before {
62 | content: "";
63 | display: block;
64 | width: 12px;
65 | height: 12px;
66 | position: absolute;
67 | border: 2px solid #f02d34;
68 | transform: translateX(0);
69 | left: 94px;
70 | border-radius: 50%;
71 | top: 5px;
72 | box-sizing: border-box;
73 | }
74 |
75 | .btn-cart:after {
76 | content: "";
77 | position: absolute;
78 | top: 0;
79 | left: 0;
80 | width: 100%;
81 | height: 100%;
82 | background: #f02d34;
83 | transition: all 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
84 | }
85 |
86 | .btn-cart:focus {
87 | outline: none;
88 | }
89 |
90 | .btn-cart:focus:before {
91 | animation: shift-left-circle 800ms forwards;
92 | animation-delay: 1200ms;
93 | }
94 |
95 | .btn-cart:focus:after {
96 | width: 20px;
97 | height: 20px;
98 | top: 12px;
99 | left: 90px;
100 | animation: shift-left 400ms forwards;
101 | animation-delay: 1200ms;
102 | transition-delay: 400ms;
103 | }
104 |
105 | .btn-cart:focus > span:before {
106 | animation: shift-left-mask 800ms forwards;
107 | animation-delay: 800ms;
108 | height: 7px;
109 | }
110 |
111 | .btn-cart:focus > span:after {
112 | transform: translate(-30%, 0);
113 | transition-delay: 1600ms;
114 | opacity: 1;
115 | }
116 |
117 | .btn-cart:focus > span span {
118 | opacity: 0;
119 | transform: translateY(20px);
120 | }
121 |
122 | .btn-cart > span {
123 | position: relative;
124 | display: block;
125 | }
126 |
127 | .btn-cart > span:before {
128 | content: "";
129 | display: block;
130 | position: absolute;
131 | width: 12px;
132 | height: 20px;
133 | background: white;
134 | top: 5px;
135 | left: 94px;
136 | animation-timing-function: linear;
137 | transform: translateX(0) rotate(0deg);
138 | transform-origin: center bottom;
139 | }
140 |
141 | .btn-cart > span:after {
142 | content: "Added";
143 | color: green;
144 | position: absolute;
145 | z-index: 3;
146 | left: 50%;
147 | opacity: 0;
148 | transition: all 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
149 | transform: translate(-30%, 20px);
150 | transition-delay: 0;
151 | }
152 |
153 | .btn-cart span span {
154 | display: inline-block;
155 | position: relative;
156 | z-index: 2;
157 | transition: all 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
158 | transform: translateY(0px);
159 | }
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding context/StateContext.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect } from "react";
2 | import { toast } from "react-hot-toast";
3 |
4 | const Context = createContext();
5 |
6 | export const StateContext = ({ children }) => {
7 | const [showCart, setShowCart] = useState(false);
8 | const [cartItems, setCartItems] = useState([]);
9 | const [totalPrice, setTotalPrice] = useState(0);
10 | const [totalQuantities, setTotalQuantities] = useState(0);
11 | const [qty, setQty] = useState(1);
12 |
13 | let foundProduct;
14 | let index;
15 |
16 | const onAdd = (product, quantity) => {
17 | const checkProductInCart = cartItems.find(
18 | (item) => item._id === product._id
19 | );
20 |
21 | setTotalPrice(
22 | (prevTotalPrice) => prevTotalPrice + product.price * quantity
23 | );
24 | setTotalQuantities((prevTotalQuantities) => prevTotalQuantities + quantity);
25 |
26 | if (checkProductInCart) {
27 | const updatedCartItems = cartItems.map((cartProduct) => {
28 | if (cartProduct._id === product._id)
29 | return {
30 | ...cartProduct,
31 | quantity: cartProduct.quantity + quantity,
32 | };
33 | });
34 |
35 | setCartItems(updatedCartItems);
36 | } else {
37 | product.quantity = quantity;
38 |
39 | setCartItems([...cartItems, { ...product }]);
40 | }
41 |
42 | toast.success(`${qty} ${product.name} added to the cart.`);
43 | };
44 |
45 | const onRemove = (product) => {
46 | foundProduct = cartItems.find((item) => item._id === product._id);
47 | const newCartItems = cartItems.filter((item) => item._id !== product._id);
48 |
49 | setTotalPrice(
50 | (prevTotalPrice) =>
51 | prevTotalPrice - foundProduct.price * foundProduct.quantity
52 | );
53 | setTotalQuantities(
54 | (prevTotalQuantities) => prevTotalQuantities - foundProduct.quantity
55 | );
56 | setCartItems(newCartItems);
57 | };
58 |
59 | const toggleCartItemQuantity = (id, value) => {
60 | foundProduct = cartItems.find((item) => item._id === id);
61 | index = cartItems.findIndex((product) => product._id === id);
62 | const newCartItems = cartItems.filter((item) => item._id !== id);
63 |
64 | if (value === "inc") {
65 | setCartItems([
66 | ...newCartItems,
67 | { ...foundProduct, quantity: foundProduct.quantity + 1 },
68 | ]);
69 | setTotalPrice((prevTotalPrice) => prevTotalPrice + foundProduct.price);
70 | setTotalQuantities((prevTotalQuantities) => prevTotalQuantities + 1);
71 | } else if (value === "dec") {
72 | if (foundProduct.quantity > 1) {
73 | setCartItems([
74 | ...newCartItems,
75 | { ...foundProduct, quantity: foundProduct.quantity - 1 },
76 | ]);
77 | setTotalPrice((prevTotalPrice) => prevTotalPrice - foundProduct.price);
78 | setTotalQuantities((prevTotalQuantities) => prevTotalQuantities - 1);
79 | }
80 | }
81 | };
82 |
83 | const incQty = () => {
84 | setQty((prevQty) => prevQty + 1);
85 | };
86 |
87 | const decQty = () => {
88 | setQty((prevQty) => {
89 | if (prevQty - 1 < 1) return 1;
90 |
91 | return prevQty - 1;
92 | });
93 | };
94 |
95 | return (
96 |
114 | {children}
115 |
116 | );
117 | };
118 |
119 | export const useStateContext = () => useContext(Context);
120 |
--------------------------------------------------------------------------------
/Ch3_Code_Nextjs_eCommerce/Ch3_Code_Nextjs_eCommerce/creating individual page/product/[slug].js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | AiOutlineMinus,
4 | AiOutlinePlus,
5 | AiFillStar,
6 | AiOutlineStar,
7 | } from "react-icons/ai";
8 | import { Info } from "../../components";
9 |
10 | import { client, urlFor } from "../../lib/client";
11 | import { Product } from "../../components";
12 |
13 | const ProductDetails = ({ product, products }) => {
14 | const { image, name, details, price, sku, ingredients, weight, delivery } =
15 | product;
16 | const [index, setIndex] = useState(0);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 | {image?.map((item, i) => (
30 |
setIndex(i)}
37 | />
38 | ))}
39 |
40 |
41 |
42 |
43 |
{name}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
(20)
53 |
54 |
Details:
55 |
{details}
56 |
57 | $
58 | {price.toLocaleString("en-US", {
59 | maximumFractionDigits: 2,
60 | minimumFractionDigits: 2,
61 | })}
62 |
63 | per box of 12
64 |
65 |
Quantity:
66 |
67 |
68 |
69 |
70 | 1
71 |
72 |
73 |
74 |
75 |
76 |
SKU: {sku}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
You may also like
84 |
85 |
86 | {products.map((item) => (
87 |
88 | ))}
89 |
90 |
91 |
92 |
93 | );
94 | };
95 |
96 | export const getStaticPaths = async () => {
97 | const query = `*[_type == "product"] {
98 | slug {
99 | current
100 | }
101 | }
102 | `;
103 |
104 | const products = await client.fetch(query);
105 |
106 | const paths = products.map((product) => ({
107 | params: {
108 | slug: product.slug.current,
109 | },
110 | }));
111 |
112 | return {
113 | paths,
114 | fallback: "blocking",
115 | };
116 | };
117 |
118 | export const getStaticProps = async ({ params: { slug } }) => {
119 | const query = `*[_type == "product" && slug.current == '${slug}'][0]`;
120 | const productsQuery = '*[_type == "product"]';
121 |
122 | const product = await client.fetch(query);
123 | const products = await client.fetch(productsQuery);
124 |
125 | return {
126 | props: { products, product },
127 | };
128 | };
129 |
130 | export default ProductDetails;
131 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/adding stripe/Cart.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import Link from "next/link";
3 | import {
4 | AiOutlineMinus,
5 | AiOutlinePlus,
6 | AiOutlineLeft,
7 | AiOutlineShopping,
8 | } from "react-icons/ai";
9 | import { TiDeleteOutline } from "react-icons/ti";
10 | import { useStateContext } from "../../context/StateContext";
11 | import { urlFor } from "../../lib/client";
12 | import getStripe from "../../lib/getStripe";
13 | import toast from "react-hot-toast";
14 | import { eUSLocale } from "../../lib/utils";
15 | import EmptyCart from "./Cart/EmptyCart";
16 |
17 | const Cart = () => {
18 | const cartRef = useRef();
19 | const {
20 | totalPrice,
21 | totalQuantities,
22 | cartItems,
23 | setShowCart,
24 | toggleCartItemQuantity,
25 | onRemove,
26 | } = useStateContext();
27 |
28 | const handleCheckout = async () => {
29 | const stripe = await getStripe();
30 |
31 | const response = await fetch("/api/stripe", {
32 | method: "POST",
33 | headers: {
34 | "Content-Type": "application/json",
35 | },
36 | body: JSON.stringify(cartItems),
37 | });
38 |
39 | if (response.statusCode === 500) return;
40 |
41 | const data = await response.json();
42 |
43 | toast.loading("Redirecting...");
44 |
45 | stripe.redirectToCheckout({ sessionId: data.id });
46 | };
47 |
48 | return (
49 |
50 |
51 |
setShowCart(false)}
55 | >
56 |
57 | Your Cart
58 | ({totalQuantities} items)
59 |
60 |
61 | {cartItems.length < 1 && (
62 |
63 |
64 | setShowCart(false)}
67 | className="btn"
68 | >
69 | Continue Shopping
70 |
71 |
72 |
73 | )}
74 |
75 |
76 | {cartItems.length >= 1 &&
77 | cartItems.map((item) => (
78 |
79 |
onRemove(item)}
83 | >
84 |
85 |
86 |
90 |
91 |
92 | {item.name}
93 |
94 | {item.quantity} @ ${eUSLocale(item.price)}
95 |
96 |
97 |
98 | toggleCartItemQuantity(item._id, "dec")}
101 | >
102 |
103 |
104 | toggleCartItemQuantity(item._id, "inc")}
107 | >
108 |
109 |
110 |
111 |
112 |
113 | ))}
114 |
115 | {cartItems.length >= 1 && (
116 |
117 |
118 |
Subtotal:
119 | ${eUSLocale(totalPrice)}
120 |
121 |
122 |
123 | Pay with Stripe
124 |
125 |
126 |
127 | )}
128 |
129 |
130 | );
131 | };
132 |
133 | export default Cart;
134 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/constructing the cart/Cart.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import Link from "next/link";
3 | import {
4 | AiOutlineMinus,
5 | AiOutlinePlus,
6 | AiOutlineLeft,
7 | AiOutlineShopping,
8 | } from "react-icons/ai";
9 | import { TiDeleteOutline } from "react-icons/ti";
10 | import { useStateContext } from "../../context/StateContext";
11 | import { urlFor } from "../../lib/client";
12 | import getStripe from "../../lib/getStripe";
13 | import toast from "react-hot-toast";
14 | import { eUSLocale } from "../../lib/utils";
15 | import EmptyCart from "./Cart/EmptyCart";
16 |
17 | const Cart = () => {
18 | const cartRef = useRef();
19 | const {
20 | totalPrice,
21 | totalQuantities,
22 | cartItems,
23 | setShowCart,
24 | toggleCartItemQuantity,
25 | onRemove,
26 | } = useStateContext();
27 |
28 | const handleCheckout = async () => {
29 | const stripe = await getStripe();
30 |
31 | const response = await fetch("/api/stripe", {
32 | method: "POST",
33 | headers: {
34 | "Content-Type": "application/json",
35 | },
36 | body: JSON.stringify(cartItems),
37 | });
38 |
39 | if (response.statusCode === 500) return;
40 |
41 | const data = await response.json();
42 |
43 | toast.loading("Redirecting...");
44 |
45 | stripe.redirectToCheckout({ sessionId: data.id });
46 | };
47 |
48 | return (
49 |
50 |
51 |
setShowCart(false)}
55 | >
56 |
57 | Your Cart
58 | ({totalQuantities} items)
59 |
60 |
61 | {cartItems.length < 1 && (
62 |
63 |
64 | setShowCart(false)}
67 | className="btn"
68 | >
69 | Continue Shopping
70 |
71 |
72 |
73 | )}
74 |
75 |
76 | {cartItems.length >= 1 &&
77 | cartItems.map((item) => (
78 |
79 |
onRemove(item)}
83 | >
84 |
85 |
86 |
90 |
91 |
92 | {item.name}
93 |
94 | {item.quantity} @ ${eUSLocale(item.price)}
95 |
96 |
97 |
98 | toggleCartItemQuantity(item._id, "dec")}
101 | >
102 |
103 |
104 | toggleCartItemQuantity(item._id, "inc")}
107 | >
108 |
109 |
110 |
111 |
112 |
113 | ))}
114 |
115 | {cartItems.length >= 1 && (
116 |
117 |
118 |
Subtotal:
119 | ${eUSLocale(totalPrice)}
120 |
121 |
122 |
123 | Pay with Stripe
124 |
125 |
126 |
127 | )}
128 |
129 |
130 | );
131 | };
132 |
133 | export default Cart;
134 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/refactoring the cart/Cart.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import Link from "next/link";
3 | import {
4 | AiOutlineMinus,
5 | AiOutlinePlus,
6 | AiOutlineLeft,
7 | AiOutlineShopping,
8 | } from "react-icons/ai";
9 | import { TiDeleteOutline } from "react-icons/ti";
10 | import { useStateContext } from "../../context/StateContext";
11 | import { urlFor } from "../../lib/client";
12 | import getStripe from "../../lib/getStripe";
13 | import toast from "react-hot-toast";
14 | import { eUSLocale } from "../../lib/utils";
15 | import EmptyCart from "./Cart/EmptyCart";
16 |
17 | const Cart = () => {
18 | const cartRef = useRef();
19 | const {
20 | totalPrice,
21 | totalQuantities,
22 | cartItems,
23 | setShowCart,
24 | toggleCartItemQuantity,
25 | onRemove,
26 | } = useStateContext();
27 |
28 | const handleCheckout = async () => {
29 | const stripe = await getStripe();
30 |
31 | const response = await fetch("/api/stripe", {
32 | method: "POST",
33 | headers: {
34 | "Content-Type": "application/json",
35 | },
36 | body: JSON.stringify(cartItems),
37 | });
38 |
39 | if (response.statusCode === 500) return;
40 |
41 | const data = await response.json();
42 |
43 | toast.loading("Redirecting...");
44 |
45 | stripe.redirectToCheckout({ sessionId: data.id });
46 | };
47 |
48 | return (
49 |
50 |
51 |
setShowCart(false)}
55 | >
56 |
57 | Your Cart
58 | ({totalQuantities} items)
59 |
60 |
61 | {cartItems.length < 1 && (
62 |
63 |
64 | setShowCart(false)}
67 | className="btn"
68 | >
69 | Continue Shopping
70 |
71 |
72 |
73 | )}
74 |
75 |
76 | {cartItems.length >= 1 &&
77 | cartItems.map((item) => (
78 |
79 |
onRemove(item)}
83 | >
84 |
85 |
86 |
90 |
91 |
92 | {item.name}
93 |
94 | {item.quantity} @ ${eUSLocale(item.price)}
95 |
96 |
97 |
98 | toggleCartItemQuantity(item._id, "dec")}
101 | >
102 |
103 |
104 | toggleCartItemQuantity(item._id, "inc")}
107 | >
108 |
109 |
110 |
111 |
112 |
113 | ))}
114 |
115 | {cartItems.length >= 1 && (
116 |
117 |
118 |
Subtotal:
119 | ${eUSLocale(totalPrice)}
120 |
121 |
122 |
123 | Pay with Stripe
124 |
125 |
126 |
127 | )}
128 |
129 |
130 | );
131 | };
132 |
133 | export default Cart;
134 |
--------------------------------------------------------------------------------
/Ch4_Code_Nextjs_eCommerce/Ch4_Code_Nextjs_eCommerce/finishing the buttons/[slug].js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { AiOutlineMinus, AiOutlinePlus } from "react-icons/ai";
3 |
4 | import { client, urlFor } from "../../../lib/client";
5 | import { Product } from "../../components";
6 | import { useStateContext } from "../../../context/StateContext";
7 | import { Info } from "../../components";
8 | import { StarRating } from "../../components";
9 |
10 | const ProductDetails = ({ product, products }) => {
11 | const { image, name, details, price, sku, ingredients, weight, delivery } =
12 | product;
13 | const [index, setIndex] = useState(0);
14 | const { decQty, incQty, qty, onAdd, setShowCart } = useStateContext();
15 |
16 | const handleBuyNow = () => {
17 | onAdd(product, qty);
18 | setShowCart(true);
19 | };
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 | {image?.map((item, i) => (
33 |
setIndex(i)}
40 | />
41 | ))}
42 |
43 |
44 |
45 |
46 |
{name}
47 |
51 |
Details:
52 |
{details}
53 |
54 | $
55 | {price.toLocaleString("en-US", {
56 | maximumFractionDigits: 2,
57 | minimumFractionDigits: 2,
58 | })}
59 |
60 | per box of 12
61 |
62 |
Quantity:
63 |
64 |
65 |
66 |
67 | {qty}
68 |
69 |
70 |
71 |
72 |
73 |
SKU: {sku}
74 |
75 | onAdd(product, qty)}
79 | >
80 | Add to Cart
81 |
82 | {/* NEW BUTTON */}
83 | onAdd(product, qty)}
87 | >
88 |
89 | Add to My Bag
90 |
91 |
92 |
93 |
94 | Buy Now
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
You may also like
104 |
105 |
106 | {products.map((item) => (
107 |
108 | ))}
109 |
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export const getStaticPaths = async () => {
117 | const query = `*[_type == "product"] {
118 | slug {
119 | current
120 | }
121 | }
122 | `;
123 |
124 | const products = await client.fetch(query);
125 |
126 | const paths = products.map((product) => ({
127 | params: {
128 | slug: product.slug.current,
129 | },
130 | }));
131 |
132 | return {
133 | paths,
134 | fallback: "blocking",
135 | };
136 | };
137 |
138 | export const getStaticProps = async ({ params: { slug } }) => {
139 | const query = `*[_type == "product" && slug.current == '${slug}'][0]`;
140 | const productsQuery = '*[_type == "product"]';
141 |
142 | const product = await client.fetch(query);
143 | const products = await client.fetch(productsQuery);
144 |
145 | return {
146 | props: { products, product },
147 | };
148 | };
149 |
150 | export default ProductDetails;
151 |
--------------------------------------------------------------------------------
/Ch7_Code_Nextjs_eCommerce/Ch7_Code_Nextjs_eCommerce/correcting errors/favicon/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.14, written by Peter Selinger 2001-2017
9 |
10 |
12 |
17 |
19 |
21 |
22 |
24 |
25 |
27 |
29 |
32 |
37 |
39 |
41 |
44 |
46 |
47 |
49 |
51 |
54 |
56 |
58 |
60 |
62 |
64 |
65 |
67 |
69 |
71 |
73 |
74 |
82 |
84 |
86 |
88 |
90 |
92 |
93 |
95 |
96 |
98 |
100 |
101 |
102 |
104 |
106 |
107 |
108 |
109 |
111 |
113 |
115 |
118 |
120 |
121 |
122 |
127 |
134 |
136 |
140 |
145 |
148 |
150 |
152 |
154 |
156 |
158 |
160 |
161 |
169 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/Ch2_Code_Nextjs_eCommerce/Ch2_Code_Nextjs_eCommerce/components/PaymentIcons.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const PaymentIcons = () => {
4 | return (
5 |
6 |
Payment methods
7 |
8 |
9 |
18 | American Express
19 |
20 |
25 |
29 |
33 |
34 |
35 |
36 |
37 |
50 | Apple Pay
51 |
55 |
59 |
60 |
61 |
65 |
69 |
70 |
71 |
75 |
79 |
83 |
84 |
85 |
86 |
87 |
88 |
97 | Diners Club
98 |
102 |
106 |
110 |
111 |
112 |
113 |
123 | Discover
124 |
129 |
133 |
137 |
141 |
146 |
150 |
154 |
158 |
159 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
200 | Google Pay
201 |
206 |
210 |
214 |
218 |
222 |
226 |
230 |
231 |
232 |
233 |
242 | Maestro
243 |
247 |
251 |
252 |
253 |
257 |
258 |
259 |
260 |
269 | Mastercard
270 |
274 |
278 |
279 |
280 |
284 |
285 |
286 |
287 |
296 | Shop Pay
297 |
302 |
306 |
310 |
311 |
312 |
313 |
322 | Visa
323 |
327 |
331 |
335 |
336 |
337 |
338 |
339 | );
340 | };
341 |
342 | export default PaymentIcons;
343 |
--------------------------------------------------------------------------------
/Ch5_Code_Nextjs_eCommerce/Ch5_Code_Nextjs_eCommerce/applying styles/globals.css:
--------------------------------------------------------------------------------
1 | /* GLOBALS ----------------------------------------- */
2 | @import url("https://fonts.googleapis.com/css2?family=Oswald&display=swap");
3 |
4 | html,
5 | body,
6 | * {
7 | padding: 0;
8 | margin: 0;
9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
11 | box-sizing: border-box;
12 | }
13 | ::-webkit-scrollbar {
14 | width: 0px;
15 | }
16 |
17 | a {
18 | color: inherit;
19 | text-decoration: none;
20 | }
21 |
22 | .main-container {
23 | margin: auto;
24 | width: 100%;
25 | }
26 |
27 | .navbar {
28 | border-top: 1px solid red;
29 | padding-top: 3px;
30 | }
31 |
32 | .navbar a {
33 | font-size: 20px;
34 | margin-right: 20px;
35 | }
36 |
37 | .demo-banner-container {
38 | background-color: #000000;
39 | color: white;
40 | text-align: center;
41 | padding: 10px 0;
42 | }
43 |
44 | .btn {
45 | width: 100%;
46 | max-width: 400px;
47 | padding: 10px 12px;
48 | border-radius: 15px;
49 | border: none;
50 | font-size: 20px;
51 | margin-top: 10px;
52 | margin-top: 40px;
53 |
54 | background-color: #f02d34;
55 | color: #fff;
56 | cursor: pointer;
57 | transform: scale(1, 1);
58 | transition: transform 0.5s ease;
59 | }
60 | .btn:hover {
61 | transform: scale(1.1, 1.1);
62 | }
63 |
64 | /* Product Page and Cart */
65 | .quantity-desc {
66 | border: 1px solid gray;
67 | padding: 6px 0;
68 | display: flex;
69 | align-items: baseline;
70 | }
71 |
72 | .quantity-desc span {
73 | font-size: 16px;
74 | padding: 6px 12px;
75 | cursor: pointer;
76 | }
77 |
78 | .quantity-desc .minus {
79 | color: #f02d34;
80 | }
81 |
82 | .quantity-desc .num {
83 | border-left: 1px solid gray;
84 | border-right: 1px solid gray;
85 | font-size: 20px;
86 | }
87 |
88 | .quantity-desc .plus {
89 | color: rgb(49, 168, 49);
90 | }
91 |
92 | /* HOMEPAGE ---------------------------------------- */
93 |
94 | /* Main homepage logo */
95 | .frontlogo {
96 | background-position: center top;
97 | background-image: url("../../public/images/frontimage.jpg");
98 | width: 100%;
99 | height: 675px;
100 | }
101 |
102 | /* "Luxury Macarons made by hand" banner on front page */
103 | .banner {
104 | float: right;
105 | margin: 100px 350px;
106 | font-size: 36px;
107 | display: flex;
108 | flex-direction: column;
109 | text-align: center;
110 | }
111 |
112 | .banner > span:nth-child(2) {
113 | width: 200px;
114 | background-color: #cbb790;
115 | margin: 30px auto;
116 | padding: 10px;
117 | }
118 |
119 | .tagline {
120 | font-weight: 600;
121 | font-family: "Oswald", sans-serif;
122 | text-transform: uppercase;
123 | font-size: 60px;
124 | line-height: 1.1em;
125 | color: #ffffff;
126 | width: 500px;
127 | }
128 |
129 | /* Newsletter component - home page */
130 | .newsletter {
131 | background-color: #cbb790;
132 | max-width: 60%;
133 | margin: 0 auto;
134 | display: flex;
135 | }
136 |
137 | @keyframes fadeIn {
138 | 0% {
139 | opacity: 0;
140 | }
141 | 100% {
142 | opacity: 1;
143 | }
144 | }
145 |
146 | .newsletter-image {
147 | width: 593px;
148 | padding: 0px 20px;
149 | position: relative;
150 | top: -35px;
151 | text-align: center;
152 | animation: fadeIn 4s;
153 | }
154 |
155 | /* Main intro on home page */
156 | .intro {
157 | background-color: #cbb790;
158 | width: 50%;
159 | margin: 50px auto;
160 | padding: 20px;
161 | font-size: 18px;
162 | }
163 |
164 | .intro p {
165 | padding-bottom: 10px;
166 | }
167 |
168 | /* Perfect message component */
169 | .perfect-occasions {
170 | display: flex;
171 | margin: 100px 10%;
172 | }
173 |
174 | .perfect-message {
175 | background-color: #959e92ff;
176 | width: 593px;
177 | height: 250px;
178 | padding: 50px 40px;
179 | position: relative;
180 | left: -30px;
181 | top: 20px;
182 | text-align: center;
183 | }
184 |
185 | .perfect-message > p:nth-child(1) {
186 | font-weight: 300;
187 | font-size: 21px;
188 | letter-spacing: 1px;
189 | line-height: 23px;
190 | padding-bottom: 5px;
191 | text-transform: uppercase;
192 | }
193 |
194 | .perfect-message > p:nth-child(2) {
195 | font-size: 42px;
196 | font-weight: 500;
197 | line-height: 24px;
198 | text-transform: uppercase;
199 | }
200 |
201 | .perfect-message > p:nth-child(3) {
202 | padding-top: 30px;
203 | }
204 |
205 | /* FOOTER --------------------------------------- */
206 | /* Now in Footer.module.css */
207 |
208 | /* Payment Icons */
209 | .visually-hidden {
210 | border: 0px;
211 | clip: rect(0px, 0px, 0px, 0px);
212 | height: 1px;
213 | width: 1px;
214 | margin: -1px;
215 | padding: 0px;
216 | overflow: hidden;
217 | white-space: nowrap;
218 | position: absolute !important;
219 | }
220 |
221 | .payment-icons {
222 | display: flex;
223 | list-style: none;
224 | }
225 |
226 | .payment-icons .icon {
227 | width: 38px;
228 | height: 24px;
229 | fill: inherit;
230 | display: inline-block;
231 | vertical-align: middle;
232 | }
233 |
234 | /* NAVBAR --------------------------------------- */
235 | .navbar-container {
236 | display: flex;
237 | justify-content: space-between;
238 | margin: 24px 18px;
239 | position: relative;
240 | }
241 | .navbar-container > div {
242 | display: flex;
243 | flex-direction: column;
244 | margin: 0 auto;
245 | text-align: center;
246 | }
247 |
248 | .navbar-container > div > a {
249 | margin-right: -0.1rem;
250 | letter-spacing: 0.2rem;
251 | opacity: 1;
252 | font-size: 48px;
253 | }
254 |
255 | .cart-icon {
256 | font-size: 25px;
257 | color: gray;
258 | cursor: pointer;
259 | position: relative;
260 | transition: transform 0.4s ease;
261 | border: none;
262 | background-color: transparent;
263 | vertical-align: middle;
264 | }
265 | .cart-icon:hover {
266 | transform: scale(1.1, 1.1);
267 | }
268 | .cart-item-qty {
269 | position: absolute;
270 | right: -8px;
271 | font-size: 12px;
272 | color: #eee;
273 | background-color: #f02d34;
274 | width: 18px;
275 | height: 18px;
276 | border-radius: 50%;
277 | text-align: center;
278 | font-weight: 600;
279 | }
280 |
281 | /* EMAIL SIGN-UP -------------------------------- */
282 | .email-signup {
283 | display: flex;
284 | flex-direction: column;
285 | width: 640px;
286 | padding: 64px 50px;
287 | }
288 |
289 | .email-signup > span {
290 | font-size: 42px;
291 | text-transform: uppercase;
292 | font-family: "Oswald", sans-serif;
293 | margin-bottom: 25px;
294 | text-align: center;
295 | }
296 |
297 | .email-signup > input {
298 | padding: 14px 4%;
299 | border-radius: 3px;
300 | border: 0;
301 | margin-bottom: 16px;
302 | }
303 |
304 | .email-signup > button {
305 | padding: 0.8rem 1rem;
306 | font-size: 18px;
307 | }
308 |
309 | /* ABOUT ---------------------------------------- */
310 | /* .about-us {
311 | width: 70%;
312 | margin: 0 auto;
313 | }
314 |
315 | .about-us p:nth-child(1) {
316 | margin: 40px 0 0 0;
317 | font-family: "Oswald", sans-serif;
318 | font-size: 42px;
319 | }
320 |
321 | .about-us > p:nth-child(2) {
322 | margin: 20px 0 20px 0;
323 | } */
324 |
325 | /* SHOP ---------------------------------------- */
326 | .products-heading {
327 | text-align: center;
328 | margin: 40px 0px;
329 | color: #324d67;
330 | }
331 | .products-heading h2 {
332 | font-size: 40px;
333 | font-weight: 800;
334 | }
335 | .products-heading p {
336 | font-size: 16px;
337 | font-weight: 200;
338 | }
339 |
340 | /* MAY LIKE COMPONENT -------------------------- */
341 | .maylike-products-wrapper {
342 | margin: 60px auto 0 auto;
343 | width: 70%;
344 | }
345 |
346 | .maylike-products-wrapper h2 {
347 | margin: 30px 0 20px 20px;
348 | color: #324d67;
349 |
350 | font-size: 28px;
351 | }
352 |
353 | .maylike-products-container {
354 | display: flex;
355 | justify-content: center;
356 | gap: 15px;
357 | }
358 |
359 | /* CONTACT --------------------------------- */
360 | .contact-us {
361 | width: 70%;
362 | margin: 0 auto;
363 | text-align: center;
364 | }
365 |
366 | .contact-us > p:nth-child(1) {
367 | margin: 40px 0 0 0;
368 | font-family: "Oswald", sans-serif;
369 | font-size: 42px;
370 | }
371 |
372 | .contact-us > p:nth-child(2) {
373 | margin: 20px 0 20px 0;
374 | }
375 |
376 | .contact-us > form > p {
377 | margin: 0 0 40px 0;
378 | }
379 |
380 | .contact-us-form {
381 | display: flex;
382 | flex-direction: column;
383 | width: 500px;
384 | margin: 0 auto;
385 | }
386 |
387 | .contact-us-form label {
388 | display: flex;
389 | }
390 |
391 | .contact-field {
392 | margin-bottom: 20px;
393 | display: flex;
394 | }
395 |
396 | .contact-field input,
397 | .contact-field textarea {
398 | width: 100%;
399 | }
400 |
401 | .contact-field input,
402 | .contact-field textarea {
403 | padding: 7px 15px;
404 | }
405 |
406 | .contact-field span {
407 | padding: 0 3px;
408 | color: #ff0000;
409 | font-weight: bold;
410 | }
411 |
412 | .contact-submit {
413 | padding: 15px 0;
414 | }
415 |
416 | /* MARQUEE COMPONENT ------------------------------- */
417 | .marquee-text {
418 | font-size: 29px;
419 | font-weight: 600;
420 | margin: 60px 0px;
421 | color: #f02d34;
422 | }
423 | .marquee {
424 | position: relative;
425 | height: 400px;
426 | width: 100%;
427 | overflow-x: hidden;
428 | }
429 |
430 | .track {
431 | position: absolute;
432 | white-space: nowrap;
433 | will-change: transform;
434 | animation: marquee 15s linear infinite;
435 | width: 180%;
436 | }
437 | .track:hover {
438 | animation-play-state: paused;
439 | }
440 | @keyframes marquee {
441 | from {
442 | transform: translateX(0);
443 | }
444 | to {
445 | transform: translateX(-50%);
446 | }
447 | }
448 |
449 | /* PRODUCT PAGE ------------------------------------- */
450 | .products-container {
451 | display: flex;
452 | flex-wrap: wrap;
453 | justify-content: center;
454 | gap: 15px;
455 | margin-top: 20px;
456 | width: 100%;
457 | }
458 | .product-card {
459 | cursor: pointer;
460 | transform: scale(1, 1);
461 | transition: transform 0.5s ease;
462 | color: #324d67;
463 | }
464 |
465 | .product-card:hover {
466 | transform: scale(1.1, 1.1);
467 | }
468 |
469 | .product-image {
470 | border-radius: 15px;
471 | background-color: #ebebeb;
472 | transform: scale(1, 1);
473 | transition: transform 0.5s ease;
474 | }
475 |
476 | .product-name {
477 | font-weight: 500;
478 | }
479 | .product-price {
480 | font-weight: 800;
481 | margin-top: 6px;
482 | color: black;
483 | }
484 |
485 | .product-detail-container {
486 | display: flex;
487 | grid-gap: 40px;
488 | gap: 40px;
489 | margin: 60px auto 0 auto;
490 | color: #324d67;
491 | width: 70%;
492 | }
493 |
494 | .product-detail-image {
495 | border-radius: 15px;
496 | background-color: #ebebeb;
497 |
498 | width: 400px;
499 | height: 400px;
500 | cursor: pointer;
501 | transition: 0.3s ease-in-out;
502 | }
503 | .product-detail-image:hover {
504 | background-color: #f02d34;
505 | }
506 | .small-images-container {
507 | display: flex;
508 | gap: 10px;
509 | margin-top: 20px;
510 | }
511 | .small-image {
512 | border: 1px solid #aaaaaa;
513 | border-radius: 4px;
514 | background-color: #ebebeb;
515 | width: 70px;
516 | height: 70px;
517 | cursor: pointer;
518 | }
519 |
520 | .selected-image {
521 | background-color: #f02d34;
522 | }
523 |
524 | .reviews {
525 | color: #f02d34;
526 | margin-top: 10px;
527 | display: flex;
528 | gap: 5px;
529 | align-items: center;
530 | }
531 |
532 | .product-detail-desc h4 {
533 | margin-top: 10px;
534 | }
535 | .product-detail-desc p {
536 | margin-top: 10px;
537 | }
538 | .reviews p {
539 | color: #324d67;
540 | margin-top: 0px;
541 | }
542 | .product-detail-desc .price {
543 | font-weight: 700;
544 | font-size: 26px;
545 | margin-top: 30px;
546 | color: #f02d34;
547 | }
548 |
549 | .product-detail-desc .quantity {
550 | display: flex;
551 | gap: 20px;
552 | margin-top: 10px;
553 | align-items: center;
554 | }
555 | .product-detail-desc .buttons {
556 | display: flex;
557 | gap: 30px;
558 | }
559 | .buttons .add-to-cart {
560 | padding: 10px 20px;
561 | border: 1px solid #f02d34;
562 | margin-top: 40px;
563 | font-size: 18px;
564 | font-weight: 500;
565 | background-color: white;
566 | color: #f02d34;
567 | cursor: pointer;
568 | width: 200px;
569 | transform: scale(1, 1);
570 | transition: transform 0.5s ease;
571 | }
572 | .buttons .add-to-cart:hover {
573 | transform: scale(1.1, 1.1);
574 | }
575 | .buttons .buy-now {
576 | width: 200px;
577 | padding: 10px 20px;
578 | background-color: #f02d34;
579 | color: white;
580 | border: none;
581 | margin-top: 40px;
582 | font-size: 18px;
583 | font-weight: 500;
584 | cursor: pointer;
585 | transform: scale(1, 1);
586 | transition: transform 0.5s ease;
587 | }
588 | .buttons .buy-now:hover {
589 | transform: scale(1.1, 1.1);
590 | }
591 |
592 | .sku {
593 | color: #65809a;
594 | font-size: 18px;
595 | }
596 |
597 | /* SUCCESS-CANCEL COMPONENT ------------------- */
598 | .success-wrapper,
599 | .canceled-wrapper {
600 | background-color: white;
601 | min-height: 60vh;
602 | }
603 | .success,
604 | .canceled {
605 | width: 1000px;
606 | margin: auto;
607 | margin-top: 20px;
608 | background-color: #dcdcdc;
609 | padding: 50px;
610 | border-radius: 15px;
611 | display: flex;
612 | justify-content: center;
613 | align-items: center;
614 | flex-direction: column;
615 | }
616 |
617 | .success .icon {
618 | color: green;
619 | font-size: 40px;
620 | }
621 | .success h2 {
622 | text-transform: capitalize;
623 | margin-top: 15px 0px;
624 | font-weight: 900;
625 | font-size: 40px;
626 | color: #324d67;
627 | }
628 | .success .email-msg {
629 | font-size: 16px;
630 | font-weight: 600;
631 | text-align: center;
632 | }
633 |
634 | .canceled {
635 | cursor: pointer;
636 | }
637 |
638 | .canceled p {
639 | font-size: 20px;
640 | font-weight: 600;
641 | }
642 | .success .description {
643 | font-size: 16px;
644 | font-weight: 600;
645 | text-align: center;
646 | margin: 10px;
647 | margin-top: 30px;
648 | }
649 | .success .description .email {
650 | margin-left: 5px;
651 | color: #f02d34;
652 | }
653 |
654 | /* CART COMPONENT ---------------------------- */
655 | /* @keyframes move {
656 | 0% {
657 | transform: translateX(100%);
658 | }
659 | 100% {
660 | transform: translateX(0%);
661 | }
662 | } */
663 |
664 | /* .cart-container {
665 | height: 100vh;
666 | width: 600px;
667 | background-color: white;
668 | float: right;
669 | padding: 40px 10px;
670 | animation-name: move;
671 | animation-duration: 2s;
672 | animation-iteration-count: 1;
673 | animation-fill-mode: backwards;
674 | } */
675 |
676 | @keyframes move {
677 | 0% {
678 | transform: translateX(100%);
679 | }
680 | 100% {
681 | transform: translateX(0%);
682 | }
683 | }
684 |
685 | .cart-container {
686 | height: 100vh;
687 | width: 600px;
688 | background-color: white;
689 | float: right;
690 | padding: 40px 10px;
691 | animation-name: move;
692 | animation-duration: 2s;
693 | animation-iteration-count: 1;
694 | animation-fill-mode: backwards;
695 | border: 1px solid #000000;
696 | }
697 |
698 | .cart-wrapper {
699 | width: 100vw;
700 | background: rgba(0, 0, 0, 0.5);
701 | position: fixed;
702 | right: 0;
703 | top: 0;
704 | z-index: 100;
705 | /* will-change: transform; */
706 | transition: all 1s ease-in-out;
707 | }
708 |
709 | /* .cart-wrapper {
710 | width: 100vw;
711 | background: rgba(0, 0, 0, 0.5);
712 | position: fixed;
713 | right: 0;
714 | top: 0;
715 | z-index: 100;
716 | /* will-change: transform; */
717 | /* transition: all 1s ease-in-out;
718 | } */
719 |
720 | .mini-cart-container {
721 | align-items: center;
722 | }
723 |
724 | .mini-cart-container > div.product-container {
725 | margin: 0;
726 | padding: 0;
727 | max-width: 320px;
728 | }
729 |
730 | .mini-cart-container > div.product-container > div {
731 | flex-direction: row;
732 | }
733 |
734 | div.mini-cart-container > div.product-container > div {
735 | padding: 5px;
736 | }
737 |
738 | /* Mini cart and Cart page */
739 | span.item-desc {
740 | display: flex;
741 | flex-direction: column;
742 | }
743 |
744 | /* Mini cart */
745 | .multiply {
746 | padding: 0 3px;
747 | }
748 |
749 | .icon-container {
750 | margin-top: 20px;
751 | border-top: 1px solid #caa34d;
752 | padding: 5px 20px 0 20px;
753 | }
754 |
755 | .cart-heading {
756 | display: flex;
757 | align-items: center;
758 | font-size: 18px;
759 | font-weight: 500;
760 | cursor: pointer;
761 | gap: 2px;
762 | margin-left: 10px;
763 | border: none;
764 | background-color: transparent;
765 | }
766 |
767 | .cart-heading .heading {
768 | margin-left: 10px;
769 | }
770 |
771 | .cart-num-items {
772 | margin-left: 10px;
773 | color: #f02d34;
774 | }
775 |
776 | .empty-cart {
777 | margin: 20px 0 20px 0;
778 | display: flex;
779 | align-items: center;
780 | display: flex;
781 | flex-direction: column;
782 | margin-right: 0;
783 | }
784 |
785 | .empty-cart h3 {
786 | font-weight: 600;
787 | font-size: 20px;
788 | }
789 |
790 | .empty-cart svg {
791 | width: 36px;
792 | height: auto;
793 | margin: 0 auto;
794 | }
795 |
796 | .product-container {
797 | overflow: auto;
798 | max-height: 70vh;
799 | padding: 20px 10px;
800 | }
801 |
802 | .product {
803 | display: flex;
804 | align-items: center;
805 | grid-gap: 30px;
806 | gap: 10px;
807 | padding: 20px 20px 0 0;
808 | }
809 |
810 | .product .cart-product-image {
811 | height: 75px;
812 | background-color: #ebebeb;
813 | }
814 |
815 | .product .mini-cart-image {
816 | width: 50px;
817 | height: auto;
818 | }
819 |
820 | .item-desc {
821 | display: flex;
822 | justify-content: space-between;
823 | width: 350px;
824 | color: #324d67;
825 | }
826 |
827 | .item-desc div {
828 | display: flex;
829 | flex-direction: column;
830 | }
831 |
832 | .item-desc .bottom {
833 | margin-top: 20px;
834 | }
835 |
836 | .total {
837 | display: flex;
838 | justify-content: space-between;
839 | }
840 |
841 | .total h3 {
842 | font-size: 22px;
843 | }
844 |
845 | .remove-item {
846 | font-size: 24px;
847 | color: #f02d34;
848 | cursor: pointer;
849 | background: transparent;
850 | border: none;
851 | }
852 |
853 | .cart-bottom {
854 | bottom: 12px;
855 | right: 5px;
856 | width: 100%;
857 | padding: 30px 65px;
858 | }
859 |
860 | .btn-container {
861 | width: 400px;
862 | margin: auto;
863 | }
864 |
865 | .mini-cart-container {
866 | align-items: center;
867 | }
868 |
869 | .mini-cart-container > div.product-container {
870 | margin: 0;
871 | padding: 0;
872 | max-width: 320px;
873 | }
874 |
875 | .mini-cart-container > div.product-container > div {
876 | flex-direction: row;
877 | }
878 |
879 | div.mini-cart-container > div.product-container > div {
880 | padding: 5px;
881 | }
882 |
883 | /* Mini cart and Cart page */
884 | span.item-desc {
885 | display: flex;
886 | flex-direction: column;
887 | }
888 |
889 | /* Mini cart */
890 | .multiply {
891 | padding: 0 3px;
892 | }
893 |
894 | .icon-container {
895 | margin-top: 20px;
896 | border-top: 1px solid #caa34d;
897 | padding: 5px 20px 0 20px;
898 | }
899 |
900 | .cart-heading {
901 | display: flex;
902 | align-items: center;
903 | font-size: 18px;
904 | font-weight: 500;
905 | cursor: pointer;
906 | gap: 2px;
907 | margin-left: 10px;
908 | border: none;
909 | background-color: transparent;
910 | }
911 |
912 | .cart-heading .heading {
913 | margin-left: 10px;
914 | }
915 |
916 | .cart-num-items {
917 | margin-left: 10px;
918 | color: #f02d34;
919 | }
920 |
921 | .empty-cart {
922 | margin: 20px 0 20px 0;
923 | display: flex;
924 | align-items: center;
925 | display: flex;
926 | flex-direction: column;
927 | margin-right: 0;
928 | }
929 |
930 | .empty-cart h3 {
931 | font-weight: 600;
932 | font-size: 20px;
933 | }
934 |
935 | .empty-cart svg {
936 | width: 36px;
937 | height: auto;
938 | margin: 0 auto;
939 | }
940 |
941 | .product-container {
942 | overflow: auto;
943 | max-height: 70vh;
944 | padding: 20px 10px;
945 | }
946 |
947 | .product {
948 | display: flex;
949 | align-items: center;
950 | grid-gap: 30px;
951 | gap: 10px;
952 | padding: 20px 20px 0 0;
953 | }
954 |
955 | .product .cart-product-image {
956 | height: 75px;
957 | background-color: #ebebeb;
958 | }
959 |
960 | .product .mini-cart-image {
961 | width: 50px;
962 | height: auto;
963 | }
964 |
965 | .item-desc {
966 | display: flex;
967 | justify-content: space-between;
968 | width: 350px;
969 | color: #324d67;
970 | }
971 |
972 | .item-desc div {
973 | display: flex;
974 | flex-direction: column;
975 | }
976 |
977 | .item-desc .bottom {
978 | margin-top: 20px;
979 | }
980 |
981 | .total {
982 | display: flex;
983 | justify-content: space-between;
984 | }
985 |
986 | .total h3 {
987 | font-size: 22px;
988 | }
989 |
990 | .remove-item {
991 | font-size: 24px;
992 | color: #f02d34;
993 | cursor: pointer;
994 | background: transparent;
995 | border: none;
996 | }
997 |
998 | .cart-bottom {
999 | bottom: 12px;
1000 | right: 5px;
1001 | width: 100%;
1002 | padding: 30px 65px;
1003 | }
1004 |
1005 | .btn-container {
1006 | width: 400px;
1007 | margin: auto;
1008 | }
1009 |
1010 | /* TABS COMPONENT ------------------------------ */
1011 | .react-tabs {
1012 | -webkit-tap-highlight-color: transparent;
1013 | width: 70%;
1014 | margin: 0 auto;
1015 | border: 1px solid #aaa;
1016 | margin-top: 40px;
1017 | }
1018 |
1019 | .react-tabs__tab-list {
1020 | border-bottom: 1px solid #aaa;
1021 | padding: 0;
1022 | background-color: #f4f4f4;
1023 | }
1024 |
1025 | .react-tabs__tab {
1026 | display: inline-block;
1027 | border: 1px solid transparent;
1028 | border-bottom: none;
1029 | bottom: -1px;
1030 | position: relative;
1031 | list-style: none;
1032 | padding: 6px 12px;
1033 | cursor: pointer;
1034 | }
1035 |
1036 | .react-tabs__tab--selected {
1037 | background: #ffffff;
1038 | border-right-color: #aaaaaa;
1039 | color: #000000;
1040 | }
1041 |
1042 | .react-tabs__tab--disabled {
1043 | color: #6d6d6d;
1044 | cursor: default;
1045 | }
1046 |
1047 | .react-tabs__tab:focus {
1048 | outline: none;
1049 | }
1050 |
1051 | .react-tabs__tab:focus:after {
1052 | content: "";
1053 | position: absolute;
1054 | height: 5px;
1055 | left: -4px;
1056 | right: -4px;
1057 | bottom: -5px;
1058 | background: #ffffff;
1059 | }
1060 |
1061 | .react-tabs__tab-panel {
1062 | display: none;
1063 | }
1064 |
1065 | .react-tabs__tab-panel--selected {
1066 | display: block;
1067 | padding: 10px;
1068 | }
1069 |
1070 | .react-tabs__tab-panel--selected h2 {
1071 | margin-bottom: 20px;
1072 | font-family: "Oswald", sans-serif;
1073 | text-transform: uppercase;
1074 | }
1075 |
1076 | .react-tabs__tab-panel--selected span {
1077 | line-height: 2;
1078 | display: inline-flex;
1079 | width: 120px;
1080 | }
1081 |
1082 | .react-tabs__tab--selected:last-child {
1083 | border-left: 1px solid #aaa;
1084 | }
1085 |
1086 | table.additional-info > tbody > tr > th {
1087 | padding: 13px 57px 13px 9px;
1088 | max-width: 100%;
1089 | }
1090 |
1091 | /* UPDATED ADD TO CART BUTTON */
1092 | .buttons {
1093 | margin-top: 40px;
1094 | }
1095 | .buttons .add-to-cart,
1096 | .buttons .buy-now {
1097 | margin-top: 0;
1098 | }
1099 |
1100 | @keyframes shift-left {
1101 | 0% {
1102 | transform: translateX(0);
1103 | }
1104 | 100% {
1105 | transform: translateX(-40px);
1106 | }
1107 | }
1108 |
1109 | @keyframes shift-left-circle {
1110 | 0% {
1111 | transform: translateX(0);
1112 | }
1113 | 50% {
1114 | transform: translateX(-40px);
1115 | }
1116 | 100% {
1117 | transform: translateX(-40px);
1118 | }
1119 | }
1120 |
1121 | @keyframes shift-left-mask {
1122 | 0% {
1123 | height: 7px;
1124 | transform: translateX(0) rotate(0);
1125 | }
1126 | 50% {
1127 | transform: translateX(0) rotate(180deg);
1128 | }
1129 | 100% {
1130 | transform: translateX(-40px) rotate(180deg);
1131 | }
1132 | }
1133 |
1134 | .btn-cart {
1135 | display: block;
1136 | width: 200px;
1137 | border: none;
1138 | margin: 0 auto;
1139 | background: none;
1140 | background-color: #ffffff;
1141 | font-weight: 500;
1142 | color: white;
1143 | font-size: 14px;
1144 | position: relative;
1145 | cursor: pointer;
1146 | height: 45px;
1147 | border: 1px solid #f02d34;
1148 | font-size: 18px;
1149 | }
1150 |
1151 | .btn-cart:before {
1152 | content: "";
1153 | display: block;
1154 | width: 12px;
1155 | height: 12px;
1156 | position: absolute;
1157 | border: 2px solid #f02d34;
1158 | transform: translateX(0);
1159 | left: 94px;
1160 | border-radius: 50%;
1161 | top: 5px;
1162 | box-sizing: border-box;
1163 | }
1164 |
1165 | .btn-cart:after {
1166 | content: "";
1167 | position: absolute;
1168 | top: 0;
1169 | left: 0;
1170 | width: 100%;
1171 | height: 100%;
1172 | background: #f02d34;
1173 | transition: all 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
1174 | }
1175 |
1176 | .btn-cart:focus {
1177 | outline: none;
1178 | }
1179 |
1180 | .btn-cart:focus:before {
1181 | animation: shift-left-circle 800ms forwards;
1182 | animation-delay: 1200ms;
1183 | }
1184 |
1185 | .btn-cart:focus:after {
1186 | width: 20px;
1187 | height: 20px;
1188 | top: 12px;
1189 | left: 90px;
1190 | animation: shift-left 400ms forwards;
1191 | animation-delay: 1200ms;
1192 | transition-delay: 400ms;
1193 | }
1194 |
1195 | .btn-cart:focus > span:before {
1196 | animation: shift-left-mask 800ms forwards;
1197 | animation-delay: 800ms;
1198 | height: 7px;
1199 | }
1200 |
1201 | .btn-cart:focus > span:after {
1202 | transform: translate(-30%, 0);
1203 | transition-delay: 1600ms;
1204 | opacity: 1;
1205 | }
1206 |
1207 | .btn-cart:focus > span span {
1208 | opacity: 0;
1209 | transform: translateY(20px);
1210 | }
1211 |
1212 | .btn-cart > span {
1213 | position: relative;
1214 | display: block;
1215 | }
1216 |
1217 | .btn-cart > span:before {
1218 | content: "";
1219 | display: block;
1220 | position: absolute;
1221 | width: 12px;
1222 | height: 20px;
1223 | background: white;
1224 | top: 5px;
1225 | left: 94px;
1226 | animation-timing-function: linear;
1227 | transform: translateX(0) rotate(0deg);
1228 | transform-origin: center bottom;
1229 | }
1230 |
1231 | .btn-cart > span:after {
1232 | content: "Added";
1233 | color: green;
1234 | position: absolute;
1235 | z-index: 3;
1236 | left: 50%;
1237 | opacity: 0;
1238 | transition: all 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
1239 | transform: translate(-30%, 20px);
1240 | transition-delay: 0;
1241 | }
1242 |
1243 | .btn-cart span span {
1244 | display: inline-block;
1245 | position: relative;
1246 | z-index: 2;
1247 | transition: all 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
1248 | transform: translateY(0px);
1249 | }
1250 |
1251 | /* FLIP IMAGES */
1252 | .fliptile {
1253 | color: #fff;
1254 | position: relative;
1255 | overflow: hidden;
1256 | margin: 10px;
1257 | min-width: 220px;
1258 | max-width: 310px;
1259 | width: 100%;
1260 | color: #000000;
1261 | text-align: left;
1262 | font-size: 16px;
1263 | perspective: 50em;
1264 | }
1265 | .fliptile * {
1266 | box-sizing: padding-box;
1267 | transition: all 0.2s ease-out;
1268 | }
1269 |
1270 | .fliptile img {
1271 | max-width: 100%;
1272 | vertical-align: top;
1273 | }
1274 |
1275 | .fliptile figcaption {
1276 | top: 20px;
1277 | left: 20px;
1278 | right: 20px;
1279 | bottom: 20px;
1280 | padding: 20px;
1281 | position: absolute;
1282 | opacity: 0;
1283 | z-index: 1;
1284 | transform: translateY(40px);
1285 | }
1286 |
1287 | .fliptile h2 {
1288 | margin: 0 0 5px;
1289 | }
1290 |
1291 | .fliptile h2 {
1292 | font-weight: 600;
1293 | }
1294 |
1295 | .fliptile:after {
1296 | background-color: rgb(0, 0, 0, 0.1);
1297 | position: absolute;
1298 | content: "";
1299 | display: block;
1300 | top: 20px;
1301 | left: 20px;
1302 | right: 20px;
1303 | bottom: 20px;
1304 | transition: all 0.4s ease-in-out;
1305 | transform: rotateX(-90deg);
1306 | transform-origin: 50% 50%;
1307 | opacity: 0;
1308 | }
1309 |
1310 | .fliptile:hover figcaption,
1311 | .fliptile.hover figcaption {
1312 | transform: translateY(0%);
1313 | opacity: 1;
1314 | transition-delay: 0.2s;
1315 | }
1316 |
1317 | .fliptile:hover:after,
1318 | .fliptile.hover:after {
1319 | transform: rotateX(0);
1320 | opacity: 0.9;
1321 | }
1322 |
1323 | /* UPDATED STAR COUNT */
1324 | .star-rating button {
1325 | background-color: transparent;
1326 | border: none;
1327 | outline: none;
1328 | cursor: pointer;
1329 | }
1330 |
1331 | .star {
1332 | width: 16px;
1333 | height: 16px;
1334 | display: flex;
1335 | }
1336 |
1337 | .on {
1338 | color: #f02d34;
1339 | fill: #f02d34;
1340 | }
1341 |
1342 | .off {
1343 | color: #d3d3d3;
1344 | fill: #ffffff;
1345 | }
1346 |
1347 | .on > path {
1348 | color: #f02d34;
1349 | fill: #f02d34;
1350 | }
1351 |
--------------------------------------------------------------------------------