├── .npmrc
├── public
└── favicon.ico
├── .vscode
├── extensions.json
└── launch.json
├── src
├── components
│ ├── ui
│ │ ├── Link.jsx
│ │ ├── Check.jsx
│ │ ├── SkeletonLoading.jsx
│ │ ├── Card.jsx
│ │ ├── Disclosure.jsx
│ │ ├── Select.jsx
│ │ ├── Modal.jsx
│ │ ├── Badge.jsx
│ │ ├── Input.jsx
│ │ └── Button.jsx
│ ├── icon
│ │ ├── Spinner.jsx
│ │ ├── Check.jsx
│ │ ├── ChevronDown.jsx
│ │ ├── ChevronRight.jsx
│ │ ├── Selector.jsx
│ │ ├── Search.jsx
│ │ ├── ShoppingBag.jsx
│ │ ├── User.jsx
│ │ ├── Refresh.jsx
│ │ ├── Trash.jsx
│ │ └── Truck.jsx
│ ├── login
│ │ ├── index.jsx
│ │ └── Login.jsx
│ ├── register
│ │ ├── index.jsx
│ │ └── Register.jsx
│ ├── checkout
│ │ ├── CustomerDetail.jsx
│ │ ├── Login.jsx
│ │ ├── Payment.jsx
│ │ ├── Register.jsx
│ │ ├── index.jsx
│ │ └── Shipping.jsx
│ ├── ItemCard.jsx
│ ├── Menu.jsx
│ ├── store.js
│ ├── profile
│ │ ├── Address.jsx
│ │ ├── Order.jsx
│ │ ├── Account.jsx
│ │ └── index.jsx
│ ├── product
│ │ ├── index.jsx
│ │ └── ProductCard.jsx
│ ├── OrderSummary.jsx
│ ├── MiniCart.jsx
│ ├── Footer.jsx
│ ├── collection
│ │ └── index.jsx
│ ├── Nav.jsx
│ ├── ElasticSearch.jsx
│ ├── cart
│ │ ├── index.jsx
│ │ └── OrderLine.jsx
│ └── home
│ │ └── index.jsx
├── pages
│ ├── checkout.astro
│ ├── cart.astro
│ ├── register.astro
│ ├── login.astro
│ ├── index.astro
│ ├── profile.astro
│ ├── product
│ │ └── [slug].astro
│ └── collection
│ │ └── [slug].astro
├── utils
│ ├── client
│ │ └── gqlClient.js
│ └── server
│ │ └── gqlClient.js
├── layouts
│ └── base.astro
├── api
│ ├── schema.js
│ ├── server.js
│ └── client.js
└── styles
│ └── global.css
├── tailwind.config.cjs
├── .gitignore
├── astro.config.mjs
├── tsconfig.json
├── package.json
├── tailwind-themes.js
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | # Expose Astro dependencies for `pnpm` users
2 | shamefully-hoist=true
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/turiguiliano88/astro-vendure-storefront/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/ui/Link.jsx:
--------------------------------------------------------------------------------
1 | export default function Link(props) {
2 | return {props.children};
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/checkout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from "../layouts/base.astro";
3 | import App from '../components/checkout';
4 | ---
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/pages/cart.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from "../layouts/base.astro";
3 | import App from '../components/cart'
4 | ---
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/icon/Spinner.jsx:
--------------------------------------------------------------------------------
1 | export default function SpinnerIcon(props) {
2 | return (
3 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | const themes = require("./tailwind-themes");
2 |
3 | module.exports = {
4 | content: ["./src/**/*.{astro,html,js,jsx,svelte,ts,tsx,vue}"],
5 | theme: themes.defaultTheme,
6 | plugins: [],
7 | };
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/login/index.jsx:
--------------------------------------------------------------------------------
1 | import Nav from "../Nav";
2 | import Login from "./Login";
3 | export default function App() {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/src/components/register/index.jsx:
--------------------------------------------------------------------------------
1 | import Nav from "../Nav";
2 | import Register from "./Register";
3 | export default function App() {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "astro/config";
2 | // import nodejs from "@astrojs/node";
3 | import react from "@astrojs/react";
4 | import tailwind from "@astrojs/tailwind";
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | integrations: [react(), tailwind()],
9 | // adapter: nodejs(),
10 | server: {
11 | port: 3001,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/icon/Check.jsx:
--------------------------------------------------------------------------------
1 | export default function Check(props) {
2 | return (
3 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ui/Check.jsx:
--------------------------------------------------------------------------------
1 | export default function Check(props) {
2 | return (
3 |
4 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/icon/ChevronDown.jsx:
--------------------------------------------------------------------------------
1 | export default function ChevronDown(props) {
2 | return (
3 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/icon/ChevronRight.jsx:
--------------------------------------------------------------------------------
1 | export default function ChevronRight(props) {
2 | return (
3 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ui/SkeletonLoading.jsx:
--------------------------------------------------------------------------------
1 | export default function SkeletonLoading({ rows }) {
2 | return (
3 |
4 | {[...Array(rows).keys()].map((index) => (
5 |
9 | ))}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/ui/Card.jsx:
--------------------------------------------------------------------------------
1 | export const Card = (props) => {
2 | return {props.children}
;
3 | };
4 |
5 | export const CardContent = (props) => {
6 | return {props.children}
;
7 | };
8 |
9 | export const CardTitle = (props) => {
10 | return (
11 |
12 | {props.children}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/ui/Disclosure.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export default function Disclosure({ title, content }) {
4 | const [open, setOpen] = useState(false);
5 |
6 | return (
7 |
8 |
{
10 | setOpen(!open);
11 | }}
12 | >
13 | {title}
14 |
15 |
{content}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/register.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from "../layouts/base.astro";
3 | import App from '../components/register'
4 | import { getActiveCustomer } from "../api/server";
5 |
6 | const cookie = Astro.request.headers.get('cookie');
7 | if (cookie) {
8 | const activeCustomer = (await getActiveCustomer(cookie)).activeCustomer;
9 | if (activeCustomer) return Astro.redirect('/profile');
10 | }
11 | ---
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/pages/login.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from "../layouts/base.astro";
3 | import App from '../components/login'
4 | import { getActiveCustomer } from "../api/server";
5 |
6 | // const cookie = Astro.request.headers.get('cookie');
7 | // if (cookie) {
8 | // const activeCustomer = (await getActiveCustomer(cookie)).activeCustomer;
9 | // if (activeCustomer) return Astro.redirect('/profile');
10 | // }
11 | ---
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/icon/Selector.jsx:
--------------------------------------------------------------------------------
1 | export default function Selector(props) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/icon/Search.jsx:
--------------------------------------------------------------------------------
1 | export default function Search(props) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from '../layouts/base.astro';
3 | import App from '../components/home'
4 | import { getProducts, getActiveCustomer, getActiveOrder, getCollectionsShort } from '../api/server';
5 |
6 | const data = await getProducts(10);
7 | const products = data.products;
8 |
9 | const collections = (await getCollectionsShort()).collections;
10 |
11 |
12 | ---
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/utils/client/gqlClient.js:
--------------------------------------------------------------------------------
1 | const gqlShopURL = import.meta.env.PUBLIC_SHOPAPI;
2 |
3 | export const createQuery = async ({ query, variables }) => {
4 | let headers = { "Content-Type": "application/json" };
5 | const response = await fetch(gqlShopURL, {
6 | method: "POST",
7 | headers,
8 | body: JSON.stringify({
9 | query,
10 | variables,
11 | }),
12 | credentials: "include",
13 | });
14 | const json = await response.json();
15 | return json.data;
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/icon/ShoppingBag.jsx:
--------------------------------------------------------------------------------
1 | export default function ShoppingBag(props) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/layouts/base.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import '../styles/global.css';
3 | import Footer from '../components/Footer'
4 |
5 | const { title } = Astro.props;
6 | ---
7 |
8 |
9 |
10 |
11 |
12 |
13 | {title}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/components/icon/User.jsx:
--------------------------------------------------------------------------------
1 | export default function User(props) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/checkout/CustomerDetail.jsx:
--------------------------------------------------------------------------------
1 | import Login from "./Login";
2 | import Register from "./Register";
3 | export default function CustomerDetail({ setOrder }) {
4 | return (
5 |
6 |
7 | Step 0: Register or Login
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable top-level await, and other modern ESM features.
4 | "target": "ESNext",
5 | "module": "ESNext",
6 | // Enable node-style module resolution, for things like npm package imports.
7 | "moduleResolution": "node",
8 | // Enable JSON imports.
9 | "resolveJsonModule": true,
10 | // Enable stricter transpilation for better output.
11 | "isolatedModules": true,
12 | // Add type definitions for our Vite runtime.
13 | "types": ["vite/client"]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/icon/Refresh.jsx:
--------------------------------------------------------------------------------
1 | export default function Refresh(props) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/server/gqlClient.js:
--------------------------------------------------------------------------------
1 | const gqlShopURL = import.meta.env.SERVER_SHOPAPI;
2 |
3 | export const createQuery = async ({ query, variables }, cookie) => {
4 | let headers = { "Content-Type": "application/json" };
5 | if (cookie) headers.Cookie = cookie;
6 | const response = await fetch(gqlShopURL, {
7 | method: "POST",
8 | headers,
9 | body: JSON.stringify({
10 | query,
11 | variables,
12 | }),
13 | credentials: "include",
14 | });
15 |
16 | const json = await response.json();
17 | return json.data;
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/icon/Trash.jsx:
--------------------------------------------------------------------------------
1 | export default function Trash(props) {
2 | return (
3 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/profile.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from "../layouts/base.astro";
3 | import App from '../components/profile';
4 | // import { getActiveCustomer, getActiveOrder } from "../api/server";
5 |
6 |
7 | // const cookie = Astro.request.headers.get('cookie');
8 | // let totalQuantity, customer;
9 | // if (cookie) {
10 | // totalQuantity = (await getActiveOrder(cookie)).activeOrder?.totalQuantity;
11 | // const activeCustomer = (await getActiveCustomer(cookie)).activeCustomer;
12 | // if (activeCustomer) customer = activeCustomer;
13 | // }
14 | // if (!cookie || !customer) {
15 | // return Astro.redirect('/login');
16 | // }
17 | ---
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/pages/product/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from '../../layouts/base.astro';
3 | import { getProducts } from '../../api/server';
4 | import App from '../../components/product';
5 |
6 | export async function getStaticPaths() {
7 | const data = await getProducts(100);
8 | return data.products.items.map(item => {
9 | return {
10 | params: {
11 | slug: item.slug
12 | },
13 | props: {
14 | product: item
15 | }
16 | }
17 | })
18 |
19 | }
20 |
21 | const { product } = Astro.props;
22 | ---
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/ItemCard.jsx:
--------------------------------------------------------------------------------
1 | export default function ItemCard({ name, price, img_url, path }) {
2 | return (
3 |
4 |
13 |
14 |
{name}
15 |
€{price}
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/icon/Truck.jsx:
--------------------------------------------------------------------------------
1 | export default function Truck(props) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ui/Select.jsx:
--------------------------------------------------------------------------------
1 | export default function Select(props) {
2 | return (
3 |
4 | {props.label &&
{props.label}
}
5 |
6 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Menu.jsx:
--------------------------------------------------------------------------------
1 | import ChevronRightIcon from "./icon/ChevronRight";
2 |
3 | export default function Menu(props) {
4 | return (
5 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/store.js:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 | import { getActiveCustomer, getActiveOrder } from "../api/client";
3 | import produce from "immer";
4 |
5 | export const useStore = create((set) => ({
6 | customer: null,
7 | order: null,
8 | orderQuantity: null,
9 | loading: false,
10 | fetchAll: async () => {
11 | set({ loading: true });
12 | const customer = (await getActiveCustomer()).activeCustomer;
13 | customer && set({ customer: customer });
14 | const order = (await getActiveOrder()).activeOrder;
15 | order && set({ order: order, orderQuantity: order.totalQuantity });
16 | set({ loading: false });
17 | },
18 | setCustomer: (data) => set({ customer: data }),
19 | setOrder: (data) => set({ order: data }),
20 | setOrderQuantity: (q) => set({ orderQuantity: q }),
21 | }));
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@example/basics",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro build",
9 | "preview": "astro preview"
10 | },
11 | "devDependencies": {
12 | "@astrojs/node": "^0.1.1",
13 | "@astrojs/react": "^0.1.1",
14 | "@astrojs/tailwind": "^0.2.1",
15 | "@astrojs/turbolinks": "^0.1.2",
16 | "astro": "^1.0.0-beta.20",
17 | "react": "^18.1.0",
18 | "react-dom": "^18.1.0"
19 | },
20 | "dependencies": {
21 | "@splidejs/react-splide": "^0.7.5",
22 | "astro-spa": "^1.3.9",
23 | "date-and-time": "^2.3.1",
24 | "immer": "^9.0.15",
25 | "react-router-dom": "^6.3.0",
26 | "recoil": "^0.7.3-alpha.2",
27 | "swr": "^1.3.0",
28 | "zustand": "^4.0.0-rc.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ui/Modal.jsx:
--------------------------------------------------------------------------------
1 | export const Modal = ({ enabled, children }) => {
2 | return (
3 |
8 |
9 |
10 | {children}
11 |
12 |
13 | );
14 | };
15 |
16 | export const ModalContent = (props) => {
17 | return {props.children}
;
18 | };
19 |
20 | export const ModalTitle = (props) => {
21 | return {props.children}
;
22 | };
23 |
24 | export const ModalAction = (props) => {
25 | return {props.children}
;
26 | };
27 |
--------------------------------------------------------------------------------
/src/api/schema.js:
--------------------------------------------------------------------------------
1 | export const OrderSchema = `
2 | id
3 | state
4 | createdAt
5 | code
6 | totalQuantity
7 | shipping
8 | subTotal
9 | total
10 | customer {
11 | id
12 | firstName
13 | lastName
14 | emailAddress
15 | }
16 | lines {
17 | id
18 | featuredAsset {
19 | preview
20 | }
21 | productVariant {
22 | name
23 | product {
24 | name
25 | slug
26 | }
27 | featuredAsset {
28 | preview
29 | }
30 | }
31 | quantity
32 | linePrice
33 | }
34 | `;
35 |
36 | export const CustomerSchema = `
37 | lastName
38 | firstName
39 | emailAddress
40 | phoneNumber
41 | addresses {
42 | fullName
43 | streetLine1
44 | streetLine2
45 | city
46 | province
47 | phoneNumber
48 | defaultShippingAddress
49 | }
50 | user {
51 | id
52 | }
53 | orders {
54 | items {
55 | ${OrderSchema}
56 | }
57 | }
58 | `;
59 |
--------------------------------------------------------------------------------
/src/components/profile/Address.jsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardTitle } from "../ui/Card";
2 | import Button from "../ui/Button";
3 | import Badge from "../ui/Badge";
4 |
5 | export default function ProfileAddress({ addresses }) {
6 | return (
7 |
8 |
9 | Shipping address
10 |
11 |
12 | {addresses?.map((item, index) => (
13 |
17 |
{item.fullName}
18 |
{item.streetLine1}
19 |
{`${item.city}, ${item.province}`}
20 |
{item.phoneNumber}
21 | {item.defaultShippingAddress && (
22 |
23 | default
24 |
25 | )}
26 |
27 | ))}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/product/index.jsx:
--------------------------------------------------------------------------------
1 | import Nav from "../Nav";
2 | import ProductCard from "./ProductCard";
3 | import { useState, useEffect } from "react";
4 | import { useStore } from "../store";
5 | export default function App({ product, showSearchBox }) {
6 | const [showMiniCart, setShowMiniCart] = useState(false);
7 | const customer = useStore((state) => state.customer);
8 | const orderQuantity = useStore((state) => state.orderQuantity);
9 | const fetchAll = useStore((state) => state.fetchAll);
10 |
11 | useEffect(() => {
12 | fetchAll();
13 | }, []);
14 |
15 | return (
16 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/ui/Badge.jsx:
--------------------------------------------------------------------------------
1 | export default function Badge(props) {
2 | let styleColor;
3 | switch (props.type) {
4 | case "neutral":
5 | styleColor = "bg-neutral-100 text-neutral-900 hover:ring-neutral-300";
6 | break;
7 | case "secondary":
8 | styleColor = "bg-secondary text-white hover:ring-secondary/40";
9 | break;
10 | case "transparent":
11 | styleColor = "bg-transparent hover:ring-transparent text-neutral-900";
12 | break;
13 | default:
14 | // styleColor = "bg-primary text-gray-900 hover:ring-gray-900";
15 | styleColor = "bg-primary text-white";
16 | break;
17 | }
18 |
19 | let styleSize;
20 | switch (props.size) {
21 | case "medium":
22 | styleSize = "py-xs px-md";
23 | break;
24 | case "large":
25 | styleSize = "py-sm px-lg";
26 | break;
27 | default:
28 | styleSize = "py-xxs px-sm";
29 | break;
30 | }
31 | return (
32 |
35 | {props.children}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/ui/Input.jsx:
--------------------------------------------------------------------------------
1 | export default function Input(props) {
2 | let backgroundStyle = props.disabled ? "bg-neutral-200" : "bg-transparent";
3 | backgroundStyle = props.backgroundStyle
4 | ? props.backgroundStyle
5 | : backgroundStyle;
6 | return (
7 |
8 | {props.label &&
{props.label}
}
9 |
12 |
24 | {props.icon}
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/OrderSummary.jsx:
--------------------------------------------------------------------------------
1 | import { Card, CardTitle, CardContent } from "./ui/Card";
2 | import Button from "./ui/Button";
3 |
4 | export default function OrderSummary({ order, showCheckoutButton }) {
5 | return (
6 |
7 |
8 | Order Summary
9 |
10 |
11 | Subtotal
12 | €{order && (order?.subTotal / 100).toFixed(2)}
13 |
14 |
15 | Shipping
16 | €{order && (order?.shipping / 100).toFixed(2)}
17 |
18 |
19 |
20 | Total
21 | €{order && (order?.total / 100).toFixed(2)}
22 |
23 | {showCheckoutButton && (
24 |
25 |
26 |
27 | )}
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/collection/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Base from "../../layouts/base.astro";
3 | import { getCollections } from "../../api/server";
4 | import App from '../../components/collection'
5 |
6 | export async function getStaticPaths() {
7 | const data = await getCollections();
8 |
9 | return data.collections.items.map(item => {
10 | let products = {};
11 | item.productVariants.items.map(item => {
12 | products[item.product.id] = item.product
13 | })
14 | return {
15 | params: {
16 | slug: item.slug
17 | },
18 | props: {
19 | products: Object.values(products),
20 | name: item.name
21 | }
22 | }
23 | })
24 |
25 | }
26 |
27 | // const cookie = Astro.request.headers.get('cookie');
28 | // let totalQuantity, customerName;
29 | // if (cookie) {
30 | // totalQuantity = (await getActiveOrder(cookie)).activeOrder?.totalQuantity;
31 | // const activeCustomer = (await getActiveCustomer(cookie)).activeCustomer;
32 | // if (activeCustomer) customerName = activeCustomer?.firstName + ' ' + activeCustomer?.lastName;
33 | // }
34 |
35 | const { name } = Astro.props;
36 | const { products } = Astro.props
37 | ---
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | /* * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | }
5 |
6 | :root {
7 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
8 | Apple Color Emoji, Segoe UI Emoji;
9 | font-size: 1rem;
10 | --user-font-scale: 1rem - 16px;
11 | font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);
12 | }
13 |
14 | body {
15 | padding: 4rem 2rem;
16 | width: 100%;
17 | min-height: 100vh;
18 | display: grid;
19 | justify-content: center;
20 | background: #f9fafb;
21 | color: #111827;
22 | }
23 |
24 | @media (prefers-color-scheme: dark) {
25 | body {
26 | background: #111827;
27 | color: #fff;
28 | }
29 | } */
30 |
31 | input[type="checkbox"]:checked {
32 | background-image: url('data:image/svg+xml,');
33 | border-color: transparent;
34 | background-color: currentColor;
35 | background-size: 100% 100%;
36 | background-position: center;
37 | background-repeat: no-repeat;
38 | }
39 |
40 | body {
41 | /* @apply bg-red-50; */
42 | @apply text-neutral-800;
43 | }
44 |
--------------------------------------------------------------------------------
/tailwind-themes.js:
--------------------------------------------------------------------------------
1 | const colors = require("tailwindcss/colors");
2 |
3 | module.exports = {
4 | defaultTheme: {
5 | spacing: {
6 | 0: 0,
7 | 1: "4px",
8 | 2: "8px",
9 | 3: "16px",
10 | 4: "24px",
11 | 5: "32px",
12 | 6: "48px",
13 | sm: "16px",
14 | md: "24px",
15 | lg: "32px",
16 | xl: "48px",
17 | },
18 | borderRadius: {
19 | sm: "10px",
20 | DEFAULT: "4px",
21 | md: "16px",
22 | lg: "32px",
23 | full: "9999px",
24 | },
25 | fontSize: {
26 | xs: ["10px", "12px"],
27 | sm: ["14px", "21px"],
28 | base: ["16px", "24px"],
29 | lg: ["18px", "21.6px"],
30 | xl: ["20px", "24px"],
31 | "2xl": ["24px", "28.8px"],
32 | "3xl": ["28px", "33.6px"],
33 | "4xl": ["32px", "38.4px"],
34 | "5xl": ["40px", "48px"],
35 | },
36 | extend: {
37 | colors: {
38 | primary: colors.red["600"],
39 | secondary: colors.green["400"],
40 | neutral: colors.neutral,
41 | },
42 | spacing: {
43 | sm: "16px",
44 | xs: "8px",
45 | xxs: "4px",
46 | 40: "160px",
47 | },
48 | flexShrink: {
49 | 2: 2,
50 | },
51 | },
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/MiniCart.jsx:
--------------------------------------------------------------------------------
1 | import Button from "./ui/Button";
2 | import ShoppingBagIcon from "./icon/ShoppingBag";
3 | export default function MiniCart({
4 | showMiniCart,
5 | setShowMiniCart,
6 | totalQuantity,
7 | }) {
8 | return (
9 |
10 |
18 |
25 |
26 |
27 | You have {totalQuantity} {totalQuantity > 1 ? "items" : "item"}.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
setShowMiniCart(false)}
41 | >
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | export default function Footer() {
2 | return (
3 |
4 |
5 |
6 | Shipping
7 | UPS
8 | DHL
9 |
10 |
11 | Payment
12 | Visa/Mastercard
13 | Paypal
14 | Klarna
15 |
16 |
17 | Social
18 | Facebook
19 | Instagram
20 |
21 |
27 |
28 |
29 |
30 | @Minh
31 |
32 | Made with ❤️ from Berlin
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ui/Button.jsx:
--------------------------------------------------------------------------------
1 | import SpinnerIcon from "../icon/Spinner";
2 | export default function Button(props) {
3 | let styleColor;
4 | switch (props.type) {
5 | case "neutral":
6 | styleColor = "bg-neutral-200 text-neutral-900 hover:ring-neutral-600";
7 | break;
8 | case "secondary":
9 | styleColor = "bg-secondary text-white hover:ring-secondary/40";
10 | break;
11 | case "transparent":
12 | styleColor =
13 | "bg-transparent hover:before:content-['→_'] hover:ring-transparent text-neutral-900 font-semibold";
14 | break;
15 | default:
16 | styleColor = "bg-primary text-white";
17 | break;
18 | }
19 |
20 | let styleSize;
21 | switch (props.size) {
22 | case "small":
23 | styleSize = "py-xxs px-sm";
24 | break;
25 | case "large":
26 | styleSize = "py-sm px-lg";
27 | break;
28 | default:
29 | styleSize = "py-xs px-md";
30 | break;
31 | }
32 |
33 | let loadingStyle = props.isLoading ? "" : "";
34 | return (
35 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/collection/index.jsx:
--------------------------------------------------------------------------------
1 | import Nav from "../Nav";
2 | import { Card, CardTitle, CardContent } from "../ui/Card";
3 | import ItemCard from "../ItemCard";
4 | import { useStore } from "../store";
5 | import { useEffect } from "react";
6 |
7 | export default function App({ name, products }) {
8 | const customer = useStore((state) => state.customer);
9 | const order = useStore((state) => state.order);
10 | const fetchAll = useStore((state) => state.fetchAll);
11 | useEffect(() => {
12 | fetchAll();
13 | }, []);
14 | return (
15 | <>
16 |
23 |
24 |
25 | {name}
26 |
27 |
28 | {products.map((item) => {
29 | return (
30 |
34 |
40 |
41 | );
42 | })}
43 |
44 |
45 |
46 |
47 | >
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/Nav.jsx:
--------------------------------------------------------------------------------
1 | import Button from "../components/ui/Button";
2 | import ElasticSearch from "./ElasticSearch";
3 | import MiniCart from "./MiniCart";
4 |
5 | export default function Nav({
6 | showMiniCart,
7 | setShowMiniCart,
8 | showSearchBox,
9 | customerName,
10 | totalQuantity,
11 | }) {
12 | return (
13 |
14 |
15 |
16 | Storefront
17 |
18 | {showSearchBox && (
19 |
20 |
21 |
22 | )}
23 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/checkout/Login.jsx:
--------------------------------------------------------------------------------
1 | import Input from "../ui/Input";
2 | import Button from "../ui/Button";
3 | import Check from "../ui/Check";
4 | import { useState } from "react";
5 | import { login, getActiveOrder } from "../../api/client";
6 |
7 | export default function Login({ setOrder }) {
8 | const [password, setPassword] = useState("");
9 | const [rememberMe, setRememberMe] = useState(false);
10 | const [email, setEmail] = useState("");
11 | return (
12 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/ElasticSearch.jsx:
--------------------------------------------------------------------------------
1 | import Input from "./ui/Input";
2 | import SearchIcon from "./icon/Search";
3 | import { useState, useEffect } from "react";
4 | import { search } from "../api/client";
5 |
6 | export default function ElasticSearch() {
7 | const [items, setItems] = useState([]);
8 | const [term, setTerm] = useState("");
9 |
10 | useEffect(() => {
11 | const timeOutId = setTimeout(async () => {
12 | if (term) {
13 | const data = await search(term);
14 | if (data.search.totalItems > 0) {
15 | setItems(data.search.items);
16 | } else {
17 | setItems([{ productName: "No product found." }]);
18 | }
19 | } else {
20 | setItems([]);
21 | }
22 | }, 300);
23 | return () => clearTimeout(timeOutId);
24 | }, [term]);
25 | return (
26 | <>
27 |
28 |
31 |
32 |
33 | }
34 | type="text"
35 | onChange={(event) => setTerm(event.target.value)}
36 | />
37 | {items.length > 0 && (
38 |
39 | {items.map((item, index) => (
40 |
49 | ))}
50 |
51 | )}
52 |
53 | >
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/cart/index.jsx:
--------------------------------------------------------------------------------
1 | import OrderLine from "./OrderLine";
2 | import OrderSummary from "../OrderSummary";
3 | import { Card, CardTitle, CardContent } from "../ui/Card";
4 | import Nav from "../Nav";
5 | import { useEffect } from "react";
6 | import { useStore } from "../store";
7 |
8 | export default function App({ showSearchBox }) {
9 | const customer = useStore((state) => state.customer);
10 | const order = useStore((state) => state.order);
11 | const fetchAll = useStore((state) => state.fetchAll);
12 | const setOrder = useStore((state) => state.setOrder);
13 | const loading = useStore((state) => state.loading);
14 |
15 | useEffect(() => {
16 | fetchAll();
17 | }, []);
18 | return (
19 |
20 |
27 |
28 |
29 |
30 |
31 | Cart
32 |
33 | {order?.lines &&
34 | order?.lines.map((line, index) => (
35 |
41 | ))}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/profile/Order.jsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardTitle } from "../ui/Card";
2 | import Disclosure from "../ui/Disclosure";
3 | import OrderLine from "../cart/OrderLine";
4 |
5 | export default function ProfileOrder({ orders }) {
6 | function parseDate(input) {
7 | return input.slice(0, 10);
8 | }
9 | return (
10 |
11 | Order
12 |
13 |
14 | {orders &&
15 | orders.map((order, index) => (
16 |
20 |
21 | Order code: {order.code}
22 |
23 |
24 | Items: {order.totalQuantity}
25 | Ordered at: {parseDate(order.createdAt)}
26 |
27 |
28 | }
29 | content={
30 |
31 | {order.lines.map((line, index) => (
32 |
33 |
34 |
35 | ))}
36 |
37 |
38 | Shipping:
39 | €{order.shipping}
40 |
41 |
42 | Total:
43 | €{order.total}
44 |
45 |
46 | }
47 | />
48 | ))}
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/checkout/Payment.jsx:
--------------------------------------------------------------------------------
1 | import Button from "../ui/Button";
2 | import { useState, useEffect } from "react";
3 | import {
4 | addPaymentToOrder,
5 | getEligiblePaymentMethods,
6 | transitionOrderToState,
7 | } from "../../api/client";
8 | export default function Payment({ setOrder }) {
9 | const [paymentMethod, setPaymentMethod] = useState({});
10 | const [paymentMethods, setPaymentMethods] = useState([]);
11 |
12 | useEffect(() => {
13 | async function fetchPaymentMethods() {
14 | const data = await getEligiblePaymentMethods();
15 | setPaymentMethods(data.eligiblePaymentMethods);
16 | }
17 | fetchPaymentMethods();
18 | }, []);
19 | return (
20 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/cart/OrderLine.jsx:
--------------------------------------------------------------------------------
1 | import Select from "../ui/Select";
2 | import { adjustOrderLine, removeOrderLine } from "../../api/client";
3 | import TrashIcon from "../icon/Trash";
4 |
5 | export default function OrderLine({ line, quantities, setOrder }) {
6 | return (
7 |
8 |
16 |
17 |
18 |
{line.productVariant.product.name}
19 |
{line.productVariant.name}
20 |
21 | {quantities ? (
22 |
23 |
49 | ) : (
50 | "x " + line.quantity
51 | )}
52 |
53 |
54 |
55 | {`€${line.linePrice / 100}`}
56 |
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/profile/Account.jsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardTitle } from "../ui/Card";
2 | import Input from "../ui/Input";
3 | import Button from "../ui/Button";
4 | import { updateCustomer } from "../../api/client";
5 | import { useState } from "react";
6 | import { useStore } from "../store";
7 |
8 | export default function ProfileAccount({ customer }) {
9 | const [firstName, setFirstName] = useState(customer.firstName);
10 | const [lastName, setLastName] = useState(customer.lastName);
11 | const [phoneNumber, setPhoneNumber] = useState(customer.phoneNumber);
12 | const setCustomer = useStore((state) => state.setCustomer);
13 | return (
14 |
15 | Account
16 |
17 |
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/home/index.jsx:
--------------------------------------------------------------------------------
1 | import Nav from "../Nav";
2 | import { Card, CardTitle, CardContent } from "../ui/Card";
3 | import ItemCard from "../ItemCard";
4 | import { useStore } from "../store";
5 | import { useEffect } from "react";
6 |
7 | export default function App({ collections, products }) {
8 | const customer = useStore((state) => state.customer);
9 | const order = useStore((state) => state.order);
10 | const fetchAll = useStore((state) => state.fetchAll);
11 | useEffect(() => {
12 | fetchAll();
13 | }, []);
14 | return (
15 | <>
16 |
23 |
24 |
25 |
26 | Collections
27 |
28 |
29 | {collections.items?.map((item) => {
30 | return (
31 |
37 | );
38 | })}
39 |
40 |
41 |
42 |
43 |
44 |
45 | Top products
46 |
47 |
48 | {products.items?.map((item) => {
49 | return (
50 |
54 |
60 |
61 | );
62 | })}
63 |
64 |
65 |
66 |
67 |
68 | >
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/login/Login.jsx:
--------------------------------------------------------------------------------
1 | import Button from "../ui/Button";
2 | import Input from "../ui/Input";
3 | import { Card, CardContent, CardTitle } from "../ui/Card";
4 | import Check from "../ui/Check";
5 | import { useState } from "react";
6 | import { login } from "../../api/client";
7 |
8 | export default function Login() {
9 | const [email, setEmail] = useState("");
10 | const [password, setPassword] = useState("");
11 | const [rememberMe, setRememberMe] = useState(false);
12 | const [isLoading, setIsLoading] = useState(false);
13 | const [errorMessage, setErrorMessage] = useState("");
14 |
15 | return (
16 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/profile/index.jsx:
--------------------------------------------------------------------------------
1 | import ProfileMenu from "../Menu";
2 | import ProfileOrder from "./Order";
3 | import ProfileAccount from "./Account";
4 | import ProfileAddress from "./Address";
5 | import Button from "../ui/Button";
6 | import { useState, useEffect } from "react";
7 | import Nav from "../Nav";
8 | import { useStore } from "../store";
9 | import { logout } from "../../api/client";
10 |
11 | export default function Profile() {
12 | const [activeTab, setActiveTab] = useState("Order");
13 | const [isLoading, setIsLoading] = useState(false);
14 |
15 | const customer = useStore((state) => state.customer);
16 | const order = useStore((state) => state.order);
17 | const fetchAll = useStore((state) => state.fetchAll);
18 |
19 | useEffect(() => {
20 | fetchAll();
21 | }, []);
22 |
23 | const options = ["Order", "Address", "Account"].map((item) => {
24 | return {
25 | name: item,
26 | value: item,
27 | onClick: () => {
28 | setActiveTab(item);
29 | },
30 | };
31 | });
32 | return (
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
60 |
61 |
62 |
63 | {activeTab === "Order" && (
64 |
65 | )}
66 | {activeTab === "Account" &&
}
67 | {activeTab === "Address" && (
68 |
69 | )}
70 |
71 |
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/product/ProductCard.jsx:
--------------------------------------------------------------------------------
1 | import Button from "../ui/Button";
2 | import Select from "../ui/Select";
3 | import { useState } from "react";
4 | import { addItemToOrder } from "../../api/client";
5 | import { useStore } from "../store";
6 |
7 | export default function ProductCard({ product, setShowMiniCart }) {
8 | const [currentVariant, setCurrentVariant] = useState(product.variants[0]);
9 | const [quantity, setQuantity] = useState(1);
10 | const [isLoading, setIsLoading] = useState(false);
11 | const setOrderQuantity = useStore((state) => state.setOrderQuantity);
12 |
13 | const addToBag = async () => {
14 | setIsLoading(true);
15 | const data = await addItemToOrder({
16 | productVariantId: currentVariant.id,
17 | quantity: Number(quantity),
18 | });
19 | // console.log("data ", data);
20 | setIsLoading(false);
21 | setOrderQuantity(data.addItemToOrder?.totalQuantity);
22 | setShowMiniCart(true);
23 | window.scroll(0, 0);
24 | };
25 | return (
26 |
27 |
28 |

29 |
30 |
31 |
{product.name}
32 |
33 | €{(currentVariant?.price / 100) * quantity}
34 |
35 |
38 |
39 | {product.variants.map((item, index) => {
40 | return (
41 | setCurrentVariant(item)}
49 | >
50 | {item.name}
51 |
52 | );
53 | })}
54 |
55 |
56 |
57 |
58 |
66 |
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/checkout/Register.jsx:
--------------------------------------------------------------------------------
1 | import Input from "../ui/Input";
2 | import Button from "../ui/Button";
3 | import { useState } from "react";
4 | import { setCustomerForOrder, registerCustomerAccount } from "../../api/client";
5 |
6 | export default function Register({ setOrder }) {
7 | const [firstName, setFirstName] = useState("");
8 | const [lastName, setLastName] = useState("");
9 | const [phoneNumber, setPhoneNumber] = useState("");
10 | const [password, setPassword] = useState("");
11 | const [emailAddress, setEmailAddress] = useState("");
12 | return (
13 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Astro Vendure storefront
2 |
3 | [Demo](https://astrossr.minh.berlin)
4 |
5 | ## Description
6 |
7 | This project attempts to deliever smooth, mobile-friendly e-commerce storefront experiences to customer/end user. It is powered by Vendure - headless commerce framework as backend and ~~leveraging SSR functionalities~~ and partial hydration on client side that Astro gives us.
8 |
9 | **Due to the instability and server's performance, I am switching to SSG for the moment. If you're interested in SSR version in the main branch, you can check out branch /ssr also in this repository**
10 |
11 | ## Features
12 |
13 | This project supports common ecommerce flow such as ordering and managing profile.
14 |
15 | - Order flow:
16 |
17 | 1. Add item to cart
18 | 2. Modify cart
19 | 3. Add shipping address
20 | 4. Authorize payment (dummy payment provider for now)
21 |
22 | - Profile management:
23 | - Orders
24 | - Addresses
25 | - Account detail
26 |
27 | ## 🚀 Project Structure
28 |
29 | Inside of your Astro project, you'll see the following folders and files:
30 |
31 | ### React components
32 |
33 | These are responsible for building UI components and interactivities on the client. As they are just React components, you can probably extract and use them anywhere else (in React project, or Nextjs project ...).
34 | It also follows design-guide carefully in order to create modern looking UI. By quickly modifying color primary / typography / spacing system in tailwind-themes.js you are good to go with your brand.
35 |
36 | ```
37 | ├── src/
38 | │ ├── components/
39 | │ │ ├── ui
40 | │ │ │ └── Button
41 | │ │ │ └── Input
42 | │ │ │ └── ...
43 | │ │ ├── icon
44 | │ │ │ └── ShoppingBag
45 | │ │ │ └── Search
46 | │ │ │ └── ...
47 | │ │ ├── cart
48 | │ │ │ └── index.jsx
49 | │ │ │ └── ...
50 | │ │ ├── checkout
51 | │ │ │ └── index.jsx
52 | │ │ │ └── ...
53 | │ │ ├── product
54 | │ │ │ └── index.jsx
55 | │ │ │ └── ...
56 | │ │ ├── profile
57 | │ │ │ └── index.jsx
58 | │ │ │ └── ...
59 | │ │ ├── login
60 | │ │ │ └── index.jsx
61 | │ │ │ └── ...
62 | │ │ ├── register
63 | │ │ │ └── index.jsx
64 | │ │ │ └── ...
65 | ```
66 |
67 | ### Astro components
68 |
69 | These are responsible for layout / non-state Astro component rendered at server.
70 |
71 | ```
72 | ├── src/
73 | │ ├── pages/
74 | │ │ └── index.astro
75 | │ │ └── login.astro
76 | │ │ └── register.astro
77 | │ │ └── cart.astro
78 | │ │ └── checkout.astro
79 | │ │ ├── product
80 | │ │ │ └── [slug].astro
81 | │ │ ├── collection
82 | │ │ │ └── [slug].astro
83 | │ ├── layouts/
84 | │ │ └── base.astro
85 | ```
86 |
87 | ### Api
88 |
89 | These are responsible for api logic serving only in server or public for client.
90 |
91 | ```
92 | ├── src/
93 | │ ├── api/
94 | │ │ └── client.js
95 | │ │ └── server.js
96 | ```
97 |
98 | ## Future improvements
99 |
100 | 1. Addresses management
101 | 2. Write test
102 | 3. ...
103 |
104 | ## Powered by
105 |
106 | - [Astro](https://astro.build)
107 | - [Vendure](https://www.vendure.io)
108 | - [React](https://reactjs.org)
109 | - [TailwindCSS](https://tailwindcss.com)
110 | - ...
111 |
112 | ## About me
113 |
114 | I am Minh from Berlin. Coding and desiging are my jobs 😁
115 |
116 | I hope you find this project useful!
117 |
118 | ```
119 |
120 | ```
121 |
--------------------------------------------------------------------------------
/src/components/checkout/index.jsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardTitle } from "../ui/Card";
2 | import Shipping from "./Shipping";
3 | import Payment from "./Payment";
4 | import CustomerDetail from "./CustomerDetail";
5 | import OrderSummary from "../OrderSummary";
6 | import Nav from "../Nav";
7 | import { useState, useEffect } from "react";
8 | import { useStore } from "../store";
9 | import SkeletonLoading from "../ui/SkeletonLoading";
10 |
11 | export default function App({}) {
12 | const customer = useStore((state) => state.customer);
13 | const order = useStore((state) => state.order);
14 | const fetchAll = useStore((state) => state.fetchAll);
15 | const setOrder = useStore((state) => state.setOrder);
16 | const setCustomer = useStore((state) => state.setCustomer);
17 | // const [isLoading, setIsLoading] = useState(false);
18 | const loading = useStore((state) => state.loading);
19 |
20 | useEffect(() => {
21 | fetchAll();
22 | // async function fetchData() {
23 | // setIsLoading(true);
24 | // const customer = (await getActiveCustomer()).activeCustomer;
25 | // customer && setCustomer(customer);
26 | // const order = (await getActiveOrder()).activeOrder;
27 | // order && setOrder(order);
28 | // setIsLoading(false);
29 | // }
30 | // fetchData();
31 | }, []);
32 | return (
33 |
34 |
38 |
39 | {loading || order === null ? (
40 |
41 |
42 |
43 | ) : (
44 |
45 | Checkout
46 |
47 |
48 |
49 | {order?.customer ? (
50 | <>
51 | {order?.state === "AddingItems" && (
52 |
53 | )}
54 | {order?.state === "ArrangingPayment" && (
55 |
56 | )}
57 | {order?.state === "PaymentSettled" && (
58 |
59 |
60 |
61 | Thank you for your order {order?.code} !
62 |
63 |
64 | You will soon receive an email as the order
65 | confirmation.
66 | {"\n"}
67 | Have a nice day!
68 |
69 |
70 |
71 | )}
72 | >
73 | ) : (
74 |
75 | )}
76 |
77 | {order?.state !== "PaymentSettled" && order?.customer && (
78 |
79 |
80 |
81 | )}
82 |
83 |
84 |
85 | )}
86 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/register/Register.jsx:
--------------------------------------------------------------------------------
1 | import Button from "../ui/Button";
2 | import Input from "../ui/Input";
3 | import { Card, CardContent, CardTitle } from "../ui/Card";
4 | import { registerCustomerAccount } from "../../api/client";
5 | import { useState } from "react";
6 |
7 | export default function SignUp() {
8 | const [firstName, setFirstName] = useState("");
9 | const [lastName, setLastName] = useState("");
10 | const [phoneNumber, setPhoneNumber] = useState("");
11 | const [password, setPassword] = useState("");
12 | const [emailAddress, setEmailAddress] = useState("");
13 | const [isLoading, setIsLoading] = useState(false);
14 | const [errorMessage, setErrorMessage] = useState("");
15 | return (
16 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/checkout/Shipping.jsx:
--------------------------------------------------------------------------------
1 | import Button from "../ui/Button";
2 | import Input from "../ui/Input";
3 | import { useState, useEffect } from "react";
4 | import {
5 | getEligibleShippingMethods,
6 | setOrderShippingMethod,
7 | setOrderShippingAddress,
8 | transitionOrderToState,
9 | } from "../../api/client";
10 |
11 | export default function Shipping({ setOrder }) {
12 | const [shippingMethod, setShippingMethod] = useState(0);
13 | const [shippingMethods, setShippingMethods] = useState([]);
14 | const [fullName, setFullName] = useState("");
15 | const [streetLine1, setStreetLine1] = useState("");
16 | const [streetLine2, setStreetLine2] = useState("");
17 | const [city, setCity] = useState("");
18 | const [province, setProvince] = useState("");
19 | const [phoneNumber, setPhoneNumber] = useState(0);
20 |
21 | useEffect(() => {
22 | async function fetchEligibleShippingMethods() {
23 | const data = await getEligibleShippingMethods();
24 | setShippingMethods(data.eligibleShippingMethods);
25 | }
26 | fetchEligibleShippingMethods();
27 | }, []);
28 | return (
29 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/src/api/server.js:
--------------------------------------------------------------------------------
1 | import { createQuery } from "../utils/server/gqlClient";
2 | import { OrderSchema, CustomerSchema } from "./schema";
3 |
4 | export async function addItemToOrder({ productVariantId, quantity }, cookie) {
5 | return await createQuery(
6 | {
7 | query: `
8 | mutation($productVariantId: ID!, $quantity: Int!){
9 | addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
10 | ... on Order {
11 | ${OrderSchema}
12 | }
13 | }
14 | }`,
15 | variables: {
16 | productVariantId,
17 | quantity,
18 | },
19 | },
20 | cookie
21 | );
22 | }
23 |
24 | export async function getActiveOrder(cookie) {
25 | return await createQuery(
26 | {
27 | query: `
28 | query {
29 | activeOrder {
30 | ${OrderSchema}
31 | }
32 | }`,
33 | },
34 | cookie
35 | );
36 | }
37 |
38 | export async function getCurrentUser(cookie) {
39 | return await createQuery(
40 | {
41 | query: `
42 |
43 | `,
44 | },
45 | cookie
46 | );
47 | }
48 |
49 | export async function getActiveCustomer(cookie) {
50 | return await createQuery(
51 | {
52 | query: `
53 | query {
54 | activeCustomer {
55 | ${CustomerSchema}
56 | }
57 | }`,
58 | },
59 | cookie
60 | );
61 | }
62 |
63 | export async function getMe(cookie) {
64 | return await createQuery(
65 | {
66 | query: `
67 | query {
68 | me {
69 | id
70 | }
71 | }`,
72 | },
73 | cookie
74 | );
75 | }
76 |
77 | export async function getProducts(take) {
78 | return await createQuery({
79 | query: `
80 | query {
81 | products(options: {
82 | take: ${take}
83 | }) {
84 | totalItems
85 | items {
86 | id
87 | slug
88 | name
89 | description
90 | assets {
91 | preview
92 | }
93 | featuredAsset {
94 | preview
95 | width
96 | height
97 | }
98 | variants {
99 | id
100 | name
101 | price
102 | }
103 | }
104 | }
105 | }
106 | `,
107 | });
108 | }
109 |
110 | export async function login({ username, password, rememberMe }) {
111 | console.log("argument", username, password, rememberMe);
112 | return await createQuery({
113 | query: `
114 | mutation($username: String!, $password: String!, $rememberMe: Boolean) {
115 | login(username: $username, password: $password, rememberMe: $rememberMe) {
116 | ... on CurrentUser {
117 | id
118 | }
119 | }
120 | }
121 | `,
122 | variables: {
123 | username,
124 | password,
125 | rememberMe,
126 | },
127 | });
128 | }
129 |
130 | export async function getEligibleShippingMethods() {
131 | return await createQuery({
132 | query: `
133 | query {
134 | eligibleShippingMethods {
135 | id
136 | name
137 | code
138 | description
139 | price
140 | priceWithTax
141 | }
142 | }`,
143 | });
144 | }
145 |
146 | export async function getEligiblePaymentMethods() {
147 | return await createQuery({
148 | query: `
149 | query {
150 | eligiblePaymentMethods {
151 | id
152 | name
153 | code
154 | description
155 | }
156 | }`,
157 | });
158 | }
159 |
160 | export async function setOrderShippingMethod(id, cookie) {
161 | return await createQuery(
162 | {
163 | query: `
164 | mutation {
165 | setOrderShippingMethod(shippingMethodId: ${id}) {
166 | ... on Order {
167 | id
168 | }
169 | }
170 | }`,
171 | },
172 | cookie
173 | );
174 | }
175 |
176 | export async function addPaymentToOrder(method, metadata) {
177 | return await createQuery({
178 | query: `
179 | mutation($metadata: JSON!, $method: String!) {
180 | addPaymentToOrder(input: {
181 | method: $method,
182 | metadata: $metadata
183 | }) {
184 | ... on Order {
185 | id
186 | }
187 | }
188 | }`,
189 | variables: {
190 | method,
191 | metadata,
192 | },
193 | });
194 | }
195 |
196 | export async function setOrderShippingAddress(
197 | fullName,
198 | streetLine1,
199 | streetLine2,
200 | city,
201 | province,
202 | phoneNumber,
203 | cookie
204 | ) {
205 | return await createQuery(
206 | {
207 | query: `
208 | mutation {
209 | setOrderShippingAddress(input: {
210 | fullName: "${fullName}",
211 | streetLine1: "${streetLine1}",
212 | streetLine2: "${streetLine2}",
213 | city: "${city}",
214 | province: "${province}",
215 | countryCode: "DE",
216 | phoneNumber: "${phoneNumber}"
217 | }) {
218 | ... on Order {
219 | ${OrderSchema}
220 | }
221 | }
222 | }`,
223 | },
224 | cookie
225 | );
226 | }
227 |
228 | export async function transitionOrderToState(state, cookie) {
229 | return await createQuery(
230 | {
231 | query: `
232 | mutation {
233 | transitionOrderToState(state: "${state}") {
234 | ... on OrderStateTransitionError {
235 | message
236 | }
237 | ... on Order {
238 | ${OrderSchema}
239 | }
240 | }
241 | }
242 | `,
243 | },
244 | cookie
245 | );
246 | }
247 |
248 | export async function getCollections() {
249 | return await createQuery({
250 | query: `
251 | query {
252 | collections {
253 | items {
254 | name
255 | slug
256 | featuredAsset {
257 | preview
258 | }
259 | productVariants {
260 | items {
261 | name
262 | product {
263 | id
264 | name
265 | slug
266 | featuredAsset {
267 | preview
268 | }
269 | variants {
270 | price
271 | }
272 | }
273 | }
274 | }
275 | }
276 | }
277 | }
278 | `,
279 | });
280 | }
281 |
282 | export async function getCollectionsShort() {
283 | return await createQuery({
284 | query: `
285 | query {
286 | collections {
287 | items {
288 | name
289 | slug
290 | featuredAsset {
291 | preview
292 | }
293 | }
294 | }
295 | }
296 | `,
297 | });
298 | }
299 |
--------------------------------------------------------------------------------
/src/api/client.js:
--------------------------------------------------------------------------------
1 | import { createQuery } from "../utils/client/gqlClient";
2 | import { OrderSchema, CustomerSchema } from "./schema";
3 |
4 | export async function addItemToOrder({ productVariantId, quantity }) {
5 | return await createQuery({
6 | query: `
7 | mutation($productVariantId: ID!, $quantity: Int!){
8 | addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
9 | ... on Order {
10 | ${OrderSchema}
11 | }
12 | }
13 | }`,
14 | variables: {
15 | productVariantId,
16 | quantity,
17 | },
18 | });
19 | }
20 |
21 | export async function getActiveOrder() {
22 | return await createQuery({
23 | query: `
24 | query {
25 | activeOrder {
26 | ${OrderSchema}
27 | }
28 | }`,
29 | });
30 | }
31 |
32 | export async function getCurrentUser(id) {
33 | return await createQuery({
34 | query: `
35 |
36 | `,
37 | });
38 | }
39 |
40 | export async function getActiveCustomer() {
41 | return await createQuery({
42 | query: `
43 | query {
44 | activeCustomer {
45 | ${CustomerSchema}
46 | }
47 | }`,
48 | });
49 | }
50 |
51 | export async function getMe() {
52 | return await createQuery({
53 | query: `
54 | query {
55 | me {
56 | id
57 | }
58 | }`,
59 | });
60 | }
61 |
62 | export async function getProducts() {
63 | return await createQuery({
64 | query: `
65 | query {
66 | products {
67 | totalItems
68 | items {
69 | id
70 | slug
71 | name
72 | description
73 | assets {
74 | preview
75 | }
76 | featuredAsset {
77 | preview
78 | width
79 | height
80 | }
81 | variants {
82 | id
83 | name
84 | price
85 | }
86 | }
87 | }
88 | }
89 | `,
90 | });
91 | }
92 |
93 | export async function login(username, password, rememberMe) {
94 | console.log("argument", username, password, rememberMe);
95 | return await createQuery({
96 | query: `
97 | mutation($username: String!, $password: String!, $rememberMe: Boolean) {
98 | login(username: $username, password: $password, rememberMe: $rememberMe) {
99 | ... on CurrentUser {
100 | id
101 | }
102 | ... on InvalidCredentialsError {
103 | message
104 | }
105 | }
106 | }
107 | `,
108 | variables: {
109 | username,
110 | password,
111 | rememberMe,
112 | },
113 | });
114 | }
115 |
116 | export async function getEligibleShippingMethods() {
117 | return await createQuery({
118 | query: `
119 | query {
120 | eligibleShippingMethods {
121 | id
122 | name
123 | code
124 | description
125 | price
126 | priceWithTax
127 | }
128 | }`,
129 | });
130 | }
131 |
132 | export async function getEligiblePaymentMethods() {
133 | return await createQuery({
134 | query: `
135 | query {
136 | eligiblePaymentMethods {
137 | id
138 | name
139 | code
140 | description
141 | }
142 | }`,
143 | });
144 | }
145 |
146 | export async function setOrderShippingMethod(id) {
147 | return await createQuery({
148 | query: `
149 | mutation {
150 | setOrderShippingMethod(shippingMethodId: ${id}) {
151 | ... on Order {
152 | id
153 | }
154 | }
155 | }`,
156 | });
157 | }
158 |
159 | export async function addPaymentToOrder(method, metadata) {
160 | return await createQuery({
161 | query: `
162 | mutation($metadata: JSON!, $method: String!) {
163 | addPaymentToOrder(input: {
164 | method: $method,
165 | metadata: $metadata
166 | }) {
167 | ... on Order {
168 | ${OrderSchema}
169 | }
170 | }
171 | }`,
172 | variables: {
173 | method,
174 | metadata,
175 | },
176 | });
177 | }
178 |
179 | export async function setOrderShippingAddress(
180 | fullName,
181 | streetLine1,
182 | streetLine2,
183 | city,
184 | province,
185 | phoneNumber
186 | ) {
187 | return await createQuery({
188 | query: `
189 | mutation {
190 | setOrderShippingAddress(input: {
191 | fullName: "${fullName}",
192 | streetLine1: "${streetLine1}",
193 | streetLine2: "${streetLine2}",
194 | city: "${city}",
195 | province: "${province}",
196 | countryCode: "DE",
197 | phoneNumber: "${phoneNumber}"
198 | }) {
199 | ... on Order {
200 | ${OrderSchema}
201 | }
202 | }
203 | }`,
204 | });
205 | }
206 |
207 | export async function transitionOrderToState(state) {
208 | return await createQuery({
209 | query: `
210 | mutation {
211 | transitionOrderToState(state: "${state}") {
212 | ... on OrderStateTransitionError {
213 | message
214 | }
215 | ... on Order {
216 | ${OrderSchema}
217 | }
218 | }
219 | }
220 | `,
221 | });
222 | }
223 |
224 | export async function search(term) {
225 | return await createQuery({
226 | query: `
227 | query {
228 | search(input: {
229 | term: "${term}",
230 | take: 10
231 | }) {
232 | totalItems
233 | items {
234 | productAsset {
235 | preview
236 | }
237 | productName
238 | slug
239 | }
240 | }
241 | }
242 | `,
243 | });
244 | }
245 |
246 | export async function updateCustomer(firstName, lastName, phoneNumber) {
247 | return await createQuery({
248 | query: `
249 | mutation {
250 | updateCustomer(input: {
251 | firstName: "${firstName}",
252 | lastName: "${lastName}",
253 | phoneNumber: "${phoneNumber}"
254 | }) {
255 | id
256 | firstName
257 | lastName
258 | phoneNumber
259 | }
260 | }
261 | `,
262 | });
263 | }
264 |
265 | export async function adjustOrderLine(orderLineId, quantity) {
266 | return await createQuery({
267 | query: `
268 | mutation {
269 | adjustOrderLine(orderLineId: ${orderLineId}, quantity: ${quantity}) {
270 | ... on Order {
271 | ${OrderSchema}
272 | }
273 | }
274 | }
275 | `,
276 | });
277 | }
278 |
279 | export async function removeOrderLine(orderLineId) {
280 | return await createQuery({
281 | query: `
282 | mutation {
283 | removeOrderLine(orderLineId: ${orderLineId}) {
284 | ... on Order {
285 | ${OrderSchema}
286 | }
287 | }
288 | }
289 | `,
290 | });
291 | }
292 |
293 | export async function setCustomerForOrder(
294 | firstName,
295 | lastName,
296 | phoneNumber,
297 | emailAddress
298 | ) {
299 | return await createQuery({
300 | query: `
301 | mutation {
302 | setCustomerForOrder(input: {
303 | firstName: "${firstName}",
304 | lastName: "${lastName}",
305 | phoneNumber: "${phoneNumber}",
306 | emailAddress: "${emailAddress}"
307 | }) {
308 | ... on Order {
309 | ${OrderSchema}
310 | }
311 | ... on EmailAddressConflictError {
312 | message
313 | errorCode
314 | }
315 | }
316 | }
317 | `,
318 | });
319 | }
320 |
321 | export async function registerCustomerAccount(
322 | firstName,
323 | lastName,
324 | phoneNumber,
325 | emailAddress,
326 | password
327 | ) {
328 | return await createQuery({
329 | query: `
330 | mutation {
331 | registerCustomerAccount(input: {
332 | firstName: "${firstName}",
333 | lastName: "${lastName}",
334 | phoneNumber: "${phoneNumber}",
335 | emailAddress: "${emailAddress}",
336 | password: "${password}"
337 | }) {
338 | ... on Success {
339 | success
340 | }
341 | ... on MissingPasswordError {
342 | errorCode
343 | message
344 | }
345 | ... on NativeAuthStrategyError {
346 | errorCode
347 | message
348 | }
349 | }
350 | }
351 | `,
352 | });
353 | }
354 |
355 | export async function logout() {
356 | return await createQuery({
357 | query: `
358 | mutation {
359 | logout {
360 | success
361 | }
362 | }
363 | `,
364 | });
365 | }
366 |
--------------------------------------------------------------------------------