mirror of
https://github.com/vercel/commerce.git
synced 2025-06-28 01:11:24 +00:00
refactor: change layout profile and home page
This commit is contained in:
parent
19802716d4
commit
6775f7363b
@ -80,13 +80,13 @@ export default function CheckoutPage() {
|
||||
<div className="flew-row col-span-4 row-span-2 flex">
|
||||
<Accordion defaultExpandedKeys={['1']} className="text-white md:w-2/3">
|
||||
<AccordionItem key="1" title="Shipping Info" className="text-white">
|
||||
<ShippingForm className="p-4" handleChangeAction={handleChangeShipping} />
|
||||
<ShippingForm handleChangeAction={handleChangeShipping} />
|
||||
<Checkbox onValueChange={(v) => setSameBilling(v)} className="mt-2">
|
||||
Use same address for billing?
|
||||
</Checkbox>
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Billing Info" className="text-white">
|
||||
<ShippingForm className="p-4" handleChangeAction={handleChangeBilling} />
|
||||
<ShippingForm handleChangeAction={handleChangeBilling} />
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="Payment" className="text-white">
|
||||
<div className="flex flex-col justify-between overflow-hidden p-1">
|
||||
|
73
app/page.tsx
73
app/page.tsx
@ -1,18 +1,77 @@
|
||||
import { Carousel } from 'components/carousel';
|
||||
import { ThreeItemGrid } from 'components/grid/three-items';
|
||||
|
||||
export const metadata = {
|
||||
description: 'High-performance ecommerce store built with Next.js, Vercel, and Shopify.',
|
||||
openGraph: {
|
||||
type: 'website'
|
||||
}
|
||||
};
|
||||
import { Carousel } from 'components/carousel';
|
||||
import { ThreeItemGrid } from 'components/grid/three-items';
|
||||
import { Category } from 'lib/woocomerce/models/base';
|
||||
import { Product } from 'lib/woocomerce/models/product';
|
||||
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
||||
import { wordpress } from 'lib/wordpress/wordpress';
|
||||
import React from 'react';
|
||||
|
||||
export default async function HomePage() {
|
||||
const categories: Category[] = await woocommerce.get('products/categories');
|
||||
const productsByCategory: Record<string, Product[]> = {};
|
||||
await Promise.all(
|
||||
categories.map((category) =>
|
||||
woocommerce.get('products', { category: category.id.toString() }).then((products) => {
|
||||
productsByCategory[category.name] = products;
|
||||
})
|
||||
)
|
||||
);
|
||||
const posts = await wordpress.get('posts');
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThreeItemGrid />
|
||||
<Carousel />
|
||||
</>
|
||||
<section>
|
||||
{categories.map((category, index) => (
|
||||
<div key={category.id} className={index % 2 === 0 ? 'bg-blue-600 py-4' : 'bg-white py-4'}>
|
||||
<div className="mb-2 mt-6 flex items-center justify-between px-4">
|
||||
<span className={`${index % 2 === 0 ? 'text-white' : 'text-black'} text-2xl font-bold`}>
|
||||
{category.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-6 px-4">
|
||||
<span className={`${index % 2 === 0 ? 'text-white' : 'text-black'}`}>
|
||||
{category.description}
|
||||
</span>
|
||||
</div>
|
||||
<React.Fragment key={category.id}>
|
||||
{productsByCategory[category.name] && (
|
||||
<ThreeItemGrid products={productsByCategory[category.name] ?? []} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
{index === 1 && (
|
||||
<div className="my-6 flex flex-col px-4">
|
||||
<span className="mb-2 text-2xl font-bold">Top products</span>
|
||||
<Carousel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="my-6 flex flex-col px-4">
|
||||
<span className="mb-2 text-2xl font-bold">Latest posts</span>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post: any) => (
|
||||
<div
|
||||
key={post.id + '-post'}
|
||||
className="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black"
|
||||
>
|
||||
<img
|
||||
src={post.featured_image}
|
||||
alt={post.title.rendered}
|
||||
className="h-48 w-full object-cover"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-bold">{post.title.rendered}</h2>
|
||||
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
7
app/profile/area/page.tsx
Normal file
7
app/profile/area/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default async function PersonalArea() {
|
||||
return (
|
||||
<section className="mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
|
||||
<h1 className="text-2xl font-bold">Personal Area</h1>
|
||||
</section>
|
||||
);
|
||||
}
|
66
app/profile/layout.tsx
Normal file
66
app/profile/layout.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar } from '@nextui-org/react';
|
||||
import LogoutButton from 'components/button/logout';
|
||||
import { Customer } from 'lib/woocomerce/models/customer';
|
||||
import { Shipping } from 'lib/woocomerce/models/shipping';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function ProfileLayout({ children }: { children: React.ReactNode }) {
|
||||
const [customer, setCustomer] = useState<Customer | undefined>(undefined);
|
||||
const [shippingAddress, setShippingAddress] = useState<Shipping | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomer = async () => {
|
||||
const data = (await (
|
||||
await fetch('/api/customer', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
).json()) as Customer;
|
||||
setCustomer(data);
|
||||
};
|
||||
|
||||
fetchCustomer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="mx-auto mt-4 flex max-w-screen-2xl flex-row gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black md:w-1/3">
|
||||
<h2 className="mb-2 text-2xl font-bold">Profile</h2>
|
||||
{customer && (
|
||||
<div>
|
||||
<Avatar src={customer.avatar_url} alt="avatar" className="h-11 w-11" />
|
||||
<div>
|
||||
<span className="text-lg font-bold">{customer.first_name}</span>
|
||||
<span className="text-lg font-bold">{customer.last_name}</span>
|
||||
</div>
|
||||
<div className="flex-start mt-3 flex">
|
||||
<Link href={`/profile/area`}>
|
||||
<button type="button" className="w-full rounded-md py-3">
|
||||
Personal area
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-start mt-3 flex">
|
||||
<Link href={`/profile/orders`}>
|
||||
<button type="button" className="w-full rounded-md py-3">
|
||||
Orders
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black md:w-2/3">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -10,7 +10,7 @@ export default async function OrdersPage() {
|
||||
const orders = await woocommerce.get('orders');
|
||||
|
||||
return (
|
||||
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4">
|
||||
<section className="mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
|
||||
<h1 className="text-2xl font-bold">Orders</h1>
|
||||
{orders.map((order) => (
|
||||
<Link href={`/profile/orders/${order.id}`} key={order.id} className="flex flex-col">
|
||||
|
@ -1,106 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import LogoutButton from 'components/button/logout';
|
||||
import ShippingForm from 'components/shipping/form';
|
||||
import { Customer } from 'lib/woocomerce/models/customer';
|
||||
import { Shipping } from 'lib/woocomerce/models/shipping';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [customer, setCustomer] = useState<Customer | undefined>(undefined);
|
||||
const [shippingAddress, setShippingAddress] = useState<Shipping | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomer = async () => {
|
||||
const data = (await (
|
||||
await fetch('/api/customer', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
).json()) as Customer;
|
||||
setCustomer(data);
|
||||
};
|
||||
|
||||
fetchCustomer();
|
||||
}, []);
|
||||
|
||||
export default async function ProfilePage() {
|
||||
return (
|
||||
<section className="mx-auto mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
|
||||
<h1 className="text-2xl font-bold">Profile</h1>
|
||||
<div className="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black">
|
||||
<h2 className="text-2xl font-bold">Info</h2>
|
||||
{customer && (
|
||||
<div>
|
||||
<img src={customer.avatar_url} alt="avatar" className="h-11 w-11" />
|
||||
<div className="flex flex-col">
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={customer.first_name}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 p-3 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor="lastname"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Cognome
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastname"
|
||||
value={customer.last_name}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 p-3 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={customer.email}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 p-3 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-lg"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<ShippingForm title="Shipping Info" handleChangeAction={setShippingAddress} />
|
||||
|
||||
<div className="mt-4">
|
||||
<Link href={`/profile/orders`}>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-md bg-indigo-500 p-3 text-white"
|
||||
>
|
||||
Orders
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<section className="mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
|
||||
<h1 className="text-2xl font-bold">Personal Area</h1>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export default function LogoutButton() {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-md bg-indigo-500 p-3 text-white"
|
||||
className="rounded-md py-3"
|
||||
onClick={() => {
|
||||
signOut({ callbackUrl: '/' });
|
||||
}}
|
||||
|
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { NextUIProvider } from '@nextui-org/react';
|
||||
import { Cart } from 'lib/woocomerce/models/cart';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
@ -30,14 +31,16 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CartContext.Provider
|
||||
value={{
|
||||
cart,
|
||||
setNewCart
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
<NextUIProvider>
|
||||
<CartContext.Provider
|
||||
value={{
|
||||
cart,
|
||||
setNewCart
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
</NextUIProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import { Product } from 'lib/woocomerce/models/product';
|
||||
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function ThreeItemGridItem({
|
||||
@ -41,12 +40,7 @@ export function ThreeItemGridItem({
|
||||
);
|
||||
}
|
||||
|
||||
export async function ThreeItemGrid() {
|
||||
// Collections that start with `hidden-*` are hidden from the search page.
|
||||
const products: Product[] = await woocommerce.get('products');
|
||||
|
||||
const [firstProduct, secondProduct, thirdProduct] = products;
|
||||
|
||||
export async function ThreeItemGrid({ products }: { products: Product[] }) {
|
||||
return (
|
||||
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
|
||||
{products.map((product, index) => (
|
||||
|
37
components/layout/breadcrumb.tsx
Normal file
37
components/layout/breadcrumb.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
import { BreadcrumbItem, Breadcrumbs } from '@nextui-org/react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
export default function Bread() {
|
||||
const paths: { name: string; href: string }[] = [];
|
||||
const currentPath = usePathname();
|
||||
if (currentPath !== '/') {
|
||||
currentPath.split('/').map((path) => {
|
||||
if (path.length === 0) {
|
||||
paths.push({
|
||||
name: 'Home',
|
||||
href: '/'
|
||||
});
|
||||
} else {
|
||||
const pathName = path.charAt(0).toUpperCase() + path.slice(1);
|
||||
paths.push({
|
||||
name: pathName,
|
||||
href: `/${path}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{paths.length > 0 && (
|
||||
<Breadcrumbs className="p-4">
|
||||
{paths.map((path) => (
|
||||
<BreadcrumbItem key={path.name} href={path.href}>
|
||||
{path.name}
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
'use client';
|
||||
import { Avatar, Select, SelectItem } from '@nextui-org/react';
|
||||
import clsx from 'clsx';
|
||||
import { getCountries } from 'lib/utils';
|
||||
import { Shipping } from 'lib/woocomerce/models/shipping';
|
||||
import { useState } from 'react';
|
||||
import countriesJson from '../../types/countries.json';
|
||||
|
||||
export const countries = countriesJson as { country: string; flag_base64: string }[];
|
||||
|
||||
export default function ShippingForm({
|
||||
className,
|
||||
@ -16,6 +14,7 @@ export default function ShippingForm({
|
||||
title?: string;
|
||||
handleChangeAction?: (data: Shipping) => void;
|
||||
}) {
|
||||
const countries = getCountries();
|
||||
const initialState: Shipping = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
@ -38,7 +37,7 @@ export default function ShippingForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('flex flex-col gap-4', className)}>
|
||||
<div className={clsx('flex flex-col', className)}>
|
||||
{title && <h2 className="mt-2 text-2xl font-bold">{title}</h2>}
|
||||
<div className="mt-4">
|
||||
<label
|
||||
@ -123,12 +122,12 @@ export default function ShippingForm({
|
||||
>
|
||||
{countries.map((item) => (
|
||||
<SelectItem
|
||||
key={item.country}
|
||||
key={item.name}
|
||||
startContent={
|
||||
<Avatar alt={item.country + '-img'} className="h-6 w-6" src={item.flag_base64} />
|
||||
<Avatar alt={item.name + '-img'} className="h-6 w-6" src={item.icon} />
|
||||
}
|
||||
>
|
||||
{item.country}
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ReadonlyURLSearchParams } from 'next/navigation';
|
||||
import countries from '../types/countries.json';
|
||||
|
||||
export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => {
|
||||
const paramsString = params.toString();
|
||||
@ -9,3 +10,9 @@ export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyUR
|
||||
|
||||
export const ensureStartsWith = (stringToCheck: string, startsWith: string) =>
|
||||
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
|
||||
|
||||
export const getCountries = (): { name: string; icon: string }[] =>
|
||||
(countries as { country: string; flag_base64: string }[]).map(({ country, flag_base64 }) => ({
|
||||
name: country,
|
||||
icon: flag_base64
|
||||
}));
|
||||
|
@ -14,6 +14,7 @@ export type Category = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type Tag = {
|
||||
|
429
lib/wordpress/models/client.ts
Normal file
429
lib/wordpress/models/client.ts
Normal file
@ -0,0 +1,429 @@
|
||||
import axios, { AxiosRequestConfig, RawAxiosRequestHeaders } from 'axios';
|
||||
import crypto from 'node:crypto';
|
||||
import OAuth from 'oauth-1.0a';
|
||||
import Url from 'url-parse';
|
||||
import { DELETE, IWCRestApiOptions, WCRestApiEndpoint, WCRestApiMethod } from './clientOptions';
|
||||
|
||||
/**
|
||||
* Set the axiosConfig property to the axios config object.
|
||||
* Could reveive any axios |... config objects.
|
||||
* @param {AxiosRequestConfig} axiosConfig
|
||||
*/
|
||||
export type WCRestApiOptions = IWCRestApiOptions<AxiosRequestConfig>;
|
||||
|
||||
/**
|
||||
* Set all the possible query params for the WCCommerce REST API.
|
||||
*/
|
||||
export type WCRestApiParams = DELETE;
|
||||
|
||||
/**
|
||||
* Define the response types for each endpoint.
|
||||
*/
|
||||
type WCCommerceResponse<
|
||||
T extends WCRestApiEndpoint,
|
||||
P extends Partial<WCRestApiParams> = {}
|
||||
> = P['id'] extends number | string ? (T extends 'posts' ? any : any) : any;
|
||||
|
||||
/**
|
||||
* WCCommerce REST API wrapper
|
||||
*
|
||||
* @param {Object} opt
|
||||
*/
|
||||
export default class WCCommerceRestApi<T extends WCRestApiOptions> {
|
||||
protected _opt: T;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param {Object} opt
|
||||
*/
|
||||
constructor(opt: T) {
|
||||
this._opt = opt;
|
||||
|
||||
/**
|
||||
* If the class is not instantiated, return a new instance.
|
||||
* This is useful for the static methods.
|
||||
*/
|
||||
if (!(this instanceof WCCommerceRestApi)) {
|
||||
return new WCCommerceRestApi(opt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the url is defined.
|
||||
*/
|
||||
if (!this._opt.url || this._opt.url === '') {
|
||||
throw new OptionsException('url is required');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the consumerKey is defined.
|
||||
*/
|
||||
if (!this._opt.consumerKey || this._opt.consumerKey === '') {
|
||||
throw new OptionsException('consumerKey is required');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the consumerSecret is defined.
|
||||
*/
|
||||
if (!this._opt.consumerSecret || this._opt.consumerSecret === '') {
|
||||
throw new OptionsException('consumerSecret is required');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default options
|
||||
*/
|
||||
this._setDefaultsOptions(this._opt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default options
|
||||
*
|
||||
* @param {Object} opt
|
||||
*/
|
||||
_setDefaultsOptions(opt: T): void {
|
||||
this._opt.wpAPIPrefix = opt.wpAPIPrefix || 'wp-json';
|
||||
this._opt.version = opt.version || 'wp/v2';
|
||||
this._opt.isHttps = /^https/i.test(this._opt.url);
|
||||
this._opt.encoding = opt.encoding || 'utf-8';
|
||||
this._opt.queryStringAuth = opt.queryStringAuth || false;
|
||||
this._opt.classVersion = '0.0.2';
|
||||
}
|
||||
|
||||
login(username: string, password: string): Promise<any> {
|
||||
return this._request('POST', 'token', { username, password }, {}, 'jwt-auth/v1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse params to object.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Object} query
|
||||
* @return {Object} IWCRestApiQuery
|
||||
*/
|
||||
// _parseParamsObject<T>(params: Record<string, T>, query: Record<string, any>): IWCRestApiQuery {
|
||||
// for (const key in params) {
|
||||
// if (typeof params[key] === "object") {
|
||||
// // If the value is an object, loop through it and add it to the query object
|
||||
// for (const subKey in params[key]) {
|
||||
// query[key + "[" + subKey + "]"] = params[key][subKey];
|
||||
// }
|
||||
// } else {
|
||||
// query[key] = params[key]; // If the value is not an object, add it to the query object
|
||||
// }
|
||||
// }
|
||||
// return query; // Return the query object
|
||||
// }
|
||||
|
||||
/**
|
||||
* Normalize query string for oAuth 1.0a
|
||||
* Depends on the _parseParamsObject method
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
_normalizeQueryString(url: string, params: Partial<Record<string, any>>): string {
|
||||
/**
|
||||
* Exit if url and params are not defined
|
||||
*/
|
||||
if (url.indexOf('?') === -1 && Object.keys(params).length === 0) {
|
||||
return url;
|
||||
}
|
||||
const query = new Url(url, true).query; // Parse the query string returned by the url
|
||||
|
||||
const values = [];
|
||||
|
||||
let queryString = '';
|
||||
|
||||
// Include params object into URL.searchParams.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// const a = this._parseParamsObject(params, query);
|
||||
// console.log("A:", a);
|
||||
|
||||
/**
|
||||
* Loop through the params object and push the key and value into the values array
|
||||
* Example: values = ['key1=value1', 'key2=value2']
|
||||
*/
|
||||
for (const key in query) {
|
||||
values.push(key);
|
||||
}
|
||||
|
||||
values.sort(); // Sort the values array
|
||||
|
||||
for (const i in values) {
|
||||
/*
|
||||
* If the queryString is not empty, add an ampersand to the end of the string
|
||||
*/
|
||||
if (queryString.length) queryString += '&';
|
||||
|
||||
/**
|
||||
* Add the key and value to the queryString
|
||||
*/
|
||||
queryString +=
|
||||
encodeURIComponent(values[i] || '') +
|
||||
'=' +
|
||||
encodeURIComponent(<string | number | boolean>(query[values[i] as string] ?? ''));
|
||||
}
|
||||
/**
|
||||
* Replace %5B with [ and %5D with ]
|
||||
*/
|
||||
queryString = queryString.replace(/%5B/g, '[').replace(/%5D/g, ']');
|
||||
|
||||
/**
|
||||
* Return the url with the queryString
|
||||
*/
|
||||
const urlObject = url.split('?')[0] + '?' + queryString;
|
||||
|
||||
return urlObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL
|
||||
*
|
||||
* @param {String} endpoint
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
_getUrl(endpoint: string, params: Partial<Record<string, unknown>>, version?: string): string {
|
||||
const api = this._opt.wpAPIPrefix + '/'; // Add prefix to endpoint
|
||||
|
||||
let url = this._opt.url.slice(-1) === '/' ? this._opt.url : this._opt.url + '/';
|
||||
|
||||
url = url + api + (version ?? this._opt.version) + '/' + endpoint;
|
||||
// Add id param to url
|
||||
if (params.id) {
|
||||
url = url + '/' + params.id;
|
||||
delete params.id;
|
||||
}
|
||||
|
||||
const queryParams: string[] = [];
|
||||
for (const key in params) {
|
||||
queryParams.push(
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(params[key] as string | number | boolean)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (queryParams.length > 0) {
|
||||
url += '?' + queryParams.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* If port is defined, add it to the url
|
||||
*/
|
||||
if (this._opt.port) {
|
||||
const hostname = new Url(url).hostname;
|
||||
url = url.replace(hostname, hostname + ':' + this._opt.port);
|
||||
}
|
||||
|
||||
/**
|
||||
* If isHttps is true, normalize the query string
|
||||
*/
|
||||
// if (this._opt.isHttps) {
|
||||
// url = this._normalizeQueryString(url, params);
|
||||
// return url;
|
||||
// }
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Hmac was deprecated fot this version at 16.11.2022
|
||||
* Get OAuth 1.0a since it is mandatory for WCCommerce REST API
|
||||
* You must use OAuth 1.0a "one-legged" authentication to ensure REST API credentials cannot be intercepted by an attacker.
|
||||
* Reference: https://WCcommerce.github.io/WCcommerce-rest-api-docs/#authentication-over-http
|
||||
* @return {Object}
|
||||
*/
|
||||
_getOAuth(): OAuth {
|
||||
const data = {
|
||||
consumer: {
|
||||
key: this._opt.consumerKey,
|
||||
secret: this._opt.consumerSecret
|
||||
},
|
||||
signature_method: 'HMAC-SHA256',
|
||||
hash_function: (base: any, key: any) => {
|
||||
return crypto.createHmac('sha256', key).update(base).digest('base64');
|
||||
}
|
||||
};
|
||||
|
||||
return new OAuth(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Axios request
|
||||
* Mount the options to send to axios and send the request.
|
||||
*
|
||||
* @param {String} method
|
||||
* @param {String} endpoint
|
||||
* @param {Object} data
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
_request<T extends WCRestApiEndpoint, P extends Partial<WCRestApiParams>>(
|
||||
method: WCRestApiMethod,
|
||||
endpoint: T,
|
||||
data?: Record<string, unknown>,
|
||||
params: P = {} as P,
|
||||
version?: string
|
||||
): Promise<WCCommerceResponse<T, P>> {
|
||||
const url = this._getUrl(endpoint, params, version);
|
||||
const header: RawAxiosRequestHeaders = {
|
||||
Accept: 'application/json'
|
||||
};
|
||||
// only set "User-Agent" in node environment
|
||||
// the checking method is identical to upstream axios
|
||||
if (
|
||||
typeof process !== 'undefined' &&
|
||||
Object.prototype.toString.call(process) === '[object process]'
|
||||
) {
|
||||
header['User-Agent'] = 'WCCommerce REST API - TS Client/' + this._opt.classVersion;
|
||||
}
|
||||
|
||||
let options: AxiosRequestConfig = {
|
||||
url,
|
||||
method,
|
||||
responseEncoding: this._opt.encoding,
|
||||
timeout: this._opt.timeout,
|
||||
responseType: 'json',
|
||||
headers: { ...header },
|
||||
params: {},
|
||||
data: data ? JSON.stringify(data) : null
|
||||
};
|
||||
|
||||
/**
|
||||
* If isHttps is false, add the query string to the params object
|
||||
*/
|
||||
if (this._opt.isHttps) {
|
||||
if (this._opt.queryStringAuth) {
|
||||
options.params = {
|
||||
consumer_key: this._opt.consumerKey,
|
||||
consumer_secret: this._opt.consumerSecret
|
||||
};
|
||||
} else {
|
||||
options.auth = {
|
||||
username: this._opt.consumerKey,
|
||||
password: this._opt.consumerSecret
|
||||
};
|
||||
}
|
||||
|
||||
options.params = { ...options.params, ...params };
|
||||
} else {
|
||||
options.params = this._getOAuth().authorize({
|
||||
url,
|
||||
method
|
||||
});
|
||||
}
|
||||
|
||||
if (options.data) {
|
||||
options.headers = {
|
||||
...header,
|
||||
'Content-Type': `application/json; charset=${this._opt.encoding}`
|
||||
};
|
||||
}
|
||||
|
||||
// Allow set and override Axios options.
|
||||
options = { ...options, ...this._opt.axiosConfig };
|
||||
|
||||
return axios(options).then((response) => response.data as WCCommerceResponse<T, P>);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET requests
|
||||
*
|
||||
* @param {String} endpoint
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
get<T extends WCRestApiEndpoint, P extends Partial<WCRestApiParams>>(
|
||||
endpoint: T,
|
||||
params?: P
|
||||
): Promise<WCCommerceResponse<T, P>> {
|
||||
return this._request('GET', endpoint, undefined, params || ({} as P));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST requests
|
||||
*
|
||||
* @param {String} endpoint
|
||||
* @param {Object} data
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
post<T extends WCRestApiEndpoint>(
|
||||
endpoint: T,
|
||||
data: Record<string, unknown>,
|
||||
params?: Partial<WCRestApiParams>
|
||||
): Promise<WCCommerceResponse<T>> {
|
||||
return this._request('POST', endpoint, data, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT requests
|
||||
*
|
||||
* @param {String} endpoint
|
||||
* @param {Object} data
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
put<T extends WCRestApiEndpoint>(
|
||||
endpoint: T,
|
||||
data: Record<string, unknown>,
|
||||
params?: Partial<WCRestApiParams>
|
||||
): Promise<WCCommerceResponse<T>> {
|
||||
return this._request('PUT', endpoint, data, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE requests
|
||||
*
|
||||
* @param {String} endpoint
|
||||
* @param {Object} params
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
delete<T extends WCRestApiEndpoint>(
|
||||
endpoint: T,
|
||||
data: Pick<WCRestApiParams, 'force'>,
|
||||
params: Pick<WCRestApiParams, 'id'>
|
||||
): Promise<WCCommerceResponse<T, Pick<WCRestApiParams, 'id'>>> {
|
||||
return this._request('DELETE', endpoint, data, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* OPTIONS requests
|
||||
*
|
||||
* @param {String} endpoint
|
||||
* @param {Object} params
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
options<T extends WCRestApiEndpoint>(
|
||||
endpoint: T,
|
||||
params?: Partial<WCRestApiParams>
|
||||
): Promise<WCCommerceResponse<T>> {
|
||||
return this._request('OPTIONS', endpoint, {}, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options Exception.
|
||||
*/
|
||||
export class OptionsException {
|
||||
public name: 'Options Error';
|
||||
public message: string;
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {String} message
|
||||
*/
|
||||
constructor(message: string) {
|
||||
this.name = 'Options Error';
|
||||
this.message = message;
|
||||
}
|
||||
}
|
59
lib/wordpress/models/clientOptions.ts
Normal file
59
lib/wordpress/models/clientOptions.ts
Normal file
@ -0,0 +1,59 @@
|
||||
export declare type WCRestApiVersion = 'wp/v2';
|
||||
|
||||
export declare type WCRestApiEncoding = 'utf-8' | 'ascii';
|
||||
export declare type WCRestApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
|
||||
|
||||
export declare type WCRestApiEndpoint = 'posts' | string;
|
||||
|
||||
export declare type IWCRestApiQuery = Record<string, unknown>;
|
||||
|
||||
export type IWCCredentials = {
|
||||
/* Your API consumer key */
|
||||
consumerKey: string;
|
||||
/* Your API consumer secret */
|
||||
consumerSecret: string;
|
||||
};
|
||||
|
||||
export type WCCommerceRestApiTypeFunctions = {
|
||||
get: <T>(endpoint: string, params?: T) => Promise<any>;
|
||||
post: <T>(endpoint: string, params?: T) => Promise<any>;
|
||||
put: <T>(endpoint: string, params?: T) => Promise<any>;
|
||||
delete: <T>(endpoint: string, params?: T) => Promise<any>;
|
||||
};
|
||||
|
||||
export interface IWCRestApiOptions<T> extends IWCCredentials {
|
||||
/* Your Store URL, example: http://WC.dev/ */
|
||||
url: string;
|
||||
|
||||
/* Custom WP REST API URL prefix, used to support custom prefixes created with the `rest_url_prefix filter` */
|
||||
wpAPIPrefix?: string;
|
||||
|
||||
/* API version, default is `v3` */
|
||||
version?: WCRestApiVersion;
|
||||
|
||||
/* Encoding, default is 'utf-8' */
|
||||
encoding?: WCRestApiEncoding;
|
||||
|
||||
/* When `true` and using under HTTPS force Basic Authentication as query string, default is `false` */
|
||||
queryStringAuth?: boolean;
|
||||
|
||||
/* Provide support for URLs with ports, eg: `8080` */
|
||||
port?: number;
|
||||
|
||||
/* Provide support for custom timeout, eg: `5000` */
|
||||
timeout?: number;
|
||||
|
||||
/* Define the custom Axios config, also override this library options */
|
||||
axiosConfig?: T;
|
||||
|
||||
/* Version of this library */
|
||||
classVersion?: string;
|
||||
|
||||
/* Https or Http */
|
||||
isHttps?: boolean;
|
||||
}
|
||||
|
||||
export interface DELETE {
|
||||
id: number | string;
|
||||
force?: boolean | string;
|
||||
}
|
11
lib/wordpress/wordpress.ts
Normal file
11
lib/wordpress/wordpress.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import WCCommerceRestApi, { WCRestApiOptions } from './models/client';
|
||||
|
||||
const option: WCRestApiOptions = {
|
||||
url: process.env.WOOCOMMERCE_URL ?? 'http://wordpress.localhost',
|
||||
consumerKey: process.env.WOOCOMMERCE_CONSUMER_KEY ?? '',
|
||||
consumerSecret: process.env.WOOCOMMERCE_CONSUMER_SECRET ?? '',
|
||||
isHttps: false,
|
||||
version: 'wp/v2',
|
||||
queryStringAuth: false // Force Basic Authentication as query string true and using under
|
||||
};
|
||||
export const wordpress = new WCCommerceRestApi(option);
|
@ -2,12 +2,10 @@ import { getToken } from 'next-auth/jwt';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
// Lista delle pagine protette
|
||||
const protectedRoutes = ['/profile'];
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
|
||||
console.log('token', token);
|
||||
const isProtectedRoute = protectedRoutes.some((route) => req.nextUrl.pathname.startsWith(route));
|
||||
if (isProtectedRoute && !token) {
|
||||
const loginUrl = new URL('/login', req.url);
|
||||
@ -19,9 +17,13 @@ export async function middleware(req: NextRequest) {
|
||||
return NextResponse.redirect(profileUrl);
|
||||
}
|
||||
|
||||
if (req.nextUrl.pathname.endsWith('/product')) {
|
||||
return NextResponse.redirect(new URL('/', req.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/login', '/profile', '/profile/:path*']
|
||||
matcher: ['/login', '/profile', '/profile/:path*', '/product']
|
||||
};
|
||||
|
@ -17,6 +17,9 @@
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"paths": {
|
||||
"@": ["/"] // enables us to use @components/MyComponent
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
|
Loading…
x
Reference in New Issue
Block a user