feat: add profile and orders customer

This commit is contained in:
paolosantarsiero 2024-12-27 03:24:35 +01:00
parent ce96340833
commit 5880b80676
52 changed files with 2019 additions and 1034 deletions

View File

@ -7,7 +7,6 @@ export async function generateMetadata(props: {
}): Promise<Metadata> { }): Promise<Metadata> {
const params = await props.params; const params = await props.params;
return { return {
title: '', title: '',
description: '', description: '',

View File

@ -1,20 +1,20 @@
import { woocommerce } from "lib/woocomerce/woocommerce"; import { woocommerce } from 'lib/woocomerce/woocommerce';
import { NextAuthOptions, Session, User } from "next-auth"; import { NextAuthOptions, Session, User } from 'next-auth';
import { JWT } from "next-auth/jwt"; import { JWT } from 'next-auth/jwt';
import NextAuth from "next-auth/next"; import NextAuth from 'next-auth/next';
import CredentialsProvider from 'next-auth/providers/credentials'; import CredentialsProvider from 'next-auth/providers/credentials';
export const authOptions = { export const authOptions = {
secret: process.env.NEXTAUTH_SECRET, secret: process.env.NEXTAUTH_SECRET,
session: { session: {
strategy: "jwt", // Use JWT for session handling strategy: 'jwt' // Use JWT for session handling
}, },
providers: [ providers: [
CredentialsProvider({ CredentialsProvider({
name: 'woocommerce', name: 'woocommerce',
credentials: { credentials: {
username: { label: 'Username', type: 'text', placeholder: 'Username' }, username: { label: 'Username', type: 'text', placeholder: 'Username' },
password: { label: 'Password', type: 'password', placeholder: 'Password' }, password: { label: 'Password', type: 'password', placeholder: 'Password' }
}, },
async authorize(credentials, req) { async authorize(credentials, req) {
if (!credentials?.username || !credentials?.password) { if (!credentials?.username || !credentials?.password) {
@ -23,28 +23,28 @@ export const authOptions = {
const user = await woocommerce.login(credentials.username, credentials.password); const user = await woocommerce.login(credentials.username, credentials.password);
// If no error and we have user data, return it // If no error and we have user data, return it
if (user) { if (user) {
return user return user;
} }
// Return null if user data could not be retrieved // Return null if user data could not be retrieved
return null return null;
} }
}), })
], ],
callbacks: { callbacks: {
async jwt({ token, user }: { token: JWT, user: User }) { async jwt({ token, user }: { token: JWT; user: User }) {
if (user) { if (user) {
console.debug('Set token user', user); console.debug('Set token user', user);
token.user = user; token.user = user;
} }
return token; return token;
}, },
async session({ session, token }: {session: Session, token: JWT}) { async session({ session, token }: { session: Session; token: JWT }) {
console.debug('Set session token', token.user); console.debug('Set session token', token.user);
session.user = token.user; session.user = token.user;
return session; return session;
}, }
}, }
} satisfies NextAuthOptions; } satisfies NextAuthOptions;
const handler = NextAuth(authOptions) const handler = NextAuth(authOptions);
export { handler as GET, handler as POST }; export { handler as GET, handler as POST };

View File

@ -1,12 +1,10 @@
import { ThreeItemGridItem } from 'components/grid/three-items'; import { ThreeItemGridItem } from 'components/grid/three-items';
import { Product } from 'lib/woocomerce/models/product'; import { Product } from 'lib/woocomerce/models/product';
import { woocommerce } from 'lib/woocomerce/woocommerce'; import { woocommerce } from 'lib/woocomerce/woocommerce';
export default async function ProductPage(props: { params: Promise<{ name: string }> }) { export default async function ProductPage(props: { params: Promise<{ name: string }> }) {
const params = await props.params; const params = await props.params;
const products: Product[] = (await (woocommerce.get('products', { category: params.name }))); const products: Product[] = await woocommerce.get('products', { category: params.name });
return ( return (
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]"> <section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]">

View File

@ -1,4 +1,3 @@
import { CartProvider } from 'components/cart/cart-context'; import { CartProvider } from 'components/cart/cart-context';
import { Navbar } from 'components/layout/navbar'; import { Navbar } from 'components/layout/navbar';
import { NextAuthProvider } from 'components/next-session-provider'; import { NextAuthProvider } from 'components/next-session-provider';

71
app/login/page.tsx Normal file
View File

@ -0,0 +1,71 @@
'use client';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export default function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const { replace } = useRouter();
const handleLogin = async (event: React.FormEvent) => {
event.preventDefault();
try {
await signIn('credentials', { username, password, redirect: false });
replace('/');
} catch (error) {
console.error(error);
}
};
return (
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4">
<h1 className="text-2xl font-bold">Login</h1>
<div className="flex h-screen justify-center">
<form onSubmit={handleLogin}>
<div className="mt-4">
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Username
</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
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"
required
/>
</div>
<div className="mt-4">
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Password
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
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"
required
/>
</div>
<div className="mt-6">
<button
type="submit"
className="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-3 text-lg font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Login
</button>
</div>
</form>
</div>
</section>
);
}

View File

@ -15,7 +15,9 @@ export async function generateMetadata(props: {
params: Promise<{ name: string }>; params: Promise<{ name: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const params = await props.params; const params = await props.params;
const product: Product | undefined = (await (woocommerce.get('products', { slug: params.name })))?.[0]; const product: Product | undefined = (
await woocommerce.get('products', { slug: params.name })
)?.[0];
if (!product) return notFound(); if (!product) return notFound();
@ -31,13 +33,15 @@ export async function generateMetadata(props: {
index: indexable, index: indexable,
follow: indexable follow: indexable
} }
}, }
}; };
} }
export default async function ProductPage(props: { params: Promise<{ name: string }> }) { export default async function ProductPage(props: { params: Promise<{ name: string }> }) {
const params = await props.params; const params = await props.params;
const product: Product | undefined = (await (woocommerce.get('products', { slug: params.name })))?.[0]; const product: Product | undefined = (
await woocommerce.get('products', { slug: params.name })
)?.[0];
if (!product) return notFound(); if (!product) return notFound();
@ -49,9 +53,8 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
image: product.images?.[0]?.src, image: product.images?.[0]?.src,
offers: { offers: {
'@type': 'AggregateOffer', '@type': 'AggregateOffer',
availability: product.stock_quantity > 0 availability:
? 'https://schema.org/InStock' product.stock_quantity > 0 ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
: 'https://schema.org/OutOfStock',
priceCurrency: product.price, priceCurrency: product.price,
highPrice: product.max_price, highPrice: product.max_price,
lowPrice: product.min_price lowPrice: product.min_price

View File

@ -0,0 +1,85 @@
import { authOptions } from 'app/api/auth/[...nextauth]/route';
import Price from 'components/price';
import { woocommerce } from 'lib/woocomerce/woocommerce';
import { getServerSession } from 'next-auth';
import Image from 'next/image';
export default async function OrderPage(props: { params: Promise<{ id: number }> }) {
const params = await props.params;
const data = await getServerSession(authOptions);
try {
const order = await woocommerce.get('orders', { id: params.id });
console.log(order);
} catch (error) {
console.error(error);
}
const order = await woocommerce.get('orders', { id: params.id });
return (
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4">
<h1 className="text-2xl font-bold">Order</h1>
<div className="flex h-screen 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={order.order_key}
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>
{order.line_items.map((item, i) => (
<li
key={i}
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
>
<div className="relative flex w-full flex-row justify-between px-1 py-4">
<div className="flex flex-row">
<div className="relative h-16 w-16 overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<Image
className="h-full w-full object-cover"
width={64}
height={64}
alt={item.name ?? ''}
src={item.image?.src || ''}
/>
</div>
<div className="flex flex-1 flex-col text-base">
<span className="leading-tight">{item.name}</span>
</div>
</div>
<div className="flex h-16 flex-col justify-between">
<Price
className="flex justify-end space-y-2 text-right text-sm"
amount={(item.price ?? 0).toString()}
currencyCode={order.currency}
/>
</div>
</div>
</li>
))}
<div className="mt-4">
<label
htmlFor="total"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Total
</label>
<input
type="text"
id="total"
value={order.total}
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>
</section>
);
}

View File

@ -0,0 +1,85 @@
import { authOptions } from 'app/api/auth/[...nextauth]/route';
import Price from 'components/price';
import { woocommerce } from 'lib/woocomerce/woocommerce';
import { getServerSession } from 'next-auth';
import Image from 'next/image';
import Link from 'next/link';
export default async function OrdersPage() {
const data = await getServerSession(authOptions);
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">
<h1 className="text-2xl font-bold">Orders</h1>
{orders.map((order) => (
<Link
href={`/profile/orders/${order.id}`}
key={order.id}
className="flex h-screen 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={order.order_key}
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>
{order.line_items.map((item, i) => (
<li
key={i}
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
>
<div className="relative flex w-full flex-row justify-between px-1 py-4">
<div className="flex flex-row">
<div className="relative h-16 w-16 overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<Image
className="h-full w-full object-cover"
width={64}
height={64}
alt={item.name ?? ''}
src={item.image?.src || ''}
/>
</div>
<div className="flex flex-1 flex-col text-base">
<span className="leading-tight">{item.name}</span>
</div>
</div>
<div className="flex h-16 flex-col justify-between">
<Price
className="flex justify-end space-y-2 text-right text-sm"
amount={(item.price ?? 0).toString()}
currencyCode={order.currency}
/>
</div>
</div>
</li>
))}
<div className="mt-4">
<label
htmlFor="total"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Total
</label>
<input
type="text"
id="total"
value={order.total}
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>
</Link>
))}
</section>
);
}

156
app/profile/page.tsx Normal file
View File

@ -0,0 +1,156 @@
import { authOptions } from 'app/api/auth/[...nextauth]/route';
import LogoutButton from 'components/button/logout';
import { woocommerce } from 'lib/woocomerce/woocommerce';
import { getServerSession } from 'next-auth';
import Link from 'next/link';
import { notFound } from 'next/navigation';
export default async function LoginPage() {
const data = await getServerSession(authOptions);
if (!data?.user) {
return notFound();
}
const customer = await woocommerce.get('customers', { id: data.user.store_id });
return (
<section className="mx-auto mt-4 grid max-w-screen-2xl justify-center gap-4 px-4 pb-4">
<h1 className="text-2xl font-bold">Profile</h1>
<h2 className="text-2xl font-bold">Info</h2>
<img src={customer.avatar_url} alt="avatar" className="h-11 w-11" />
<div className="flex h-screen 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"
>
Name
</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
/>
<h2 className="mt-2 text-2xl font-bold">Shipping info</h2>
<div className="mt-4">
<label
htmlFor="address"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Address
</label>
<input
type="text"
id="address"
value={customer.shipping.address_1}
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="city"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
City
</label>
<input
type="text"
id="city"
value={customer.shipping.city}
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="state"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
State
</label>
<input
type="text"
id="state"
value={customer.shipping.state}
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="postcode"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postcode
</label>
<input
type="text"
id="postcode"
value={customer.shipping.postcode}
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="country"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Country
</label>
<input
type="text"
id="country"
value={customer.shipping.country}
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">
<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>
</section>
);
}

View File

@ -3,6 +3,5 @@ import OpengraphImage from 'components/opengraph-image';
export const runtime = 'edge'; export const runtime = 'edge';
export default async function Image({ params }: { params: { collection: string } }) { export default async function Image({ params }: { params: { collection: string } }) {
return await OpengraphImage({ title: '' }); return await OpengraphImage({ title: '' });
} }

View File

@ -7,8 +7,7 @@ export default function SearchLayout({ children }: { children: React.ReactNode }
return ( return (
<> <>
<div className="mx-auto flex max-w-screen-2xl flex-col gap-8 px-4 pb-4 text-black md:flex-row dark:text-white"> <div className="mx-auto flex max-w-screen-2xl flex-col gap-8 px-4 pb-4 text-black md:flex-row dark:text-white">
<div className="order-first w-full flex-none md:max-w-[125px]"> <div className="order-first w-full flex-none md:max-w-[125px]"></div>
</div>
<div className="order-last min-h-screen w-full md:order-none"> <div className="order-last min-h-screen w-full md:order-none">
<ChildrenWrapper>{children}</ChildrenWrapper> <ChildrenWrapper>{children}</ChildrenWrapper>
</div> </div>

View File

@ -15,7 +15,7 @@ export default async function SearchPage(props: {
const { sort, q: searchValue } = searchParams as { [key: string]: string }; const { sort, q: searchValue } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort; const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = (await (woocommerce.get('products', { search: searchValue, orderby: sortKey }))); const products = await woocommerce.get('products', { search: searchValue, orderby: sortKey });
const resultsText = products.length > 1 ? 'results' : 'result'; const resultsText = products.length > 1 ? 'results' : 'result';
return ( return (

View File

@ -1,4 +1,3 @@
import { validateEnvironmentVariables } from 'lib/utils'; import { validateEnvironmentVariables } from 'lib/utils';
import { MetadataRoute } from 'next'; import { MetadataRoute } from 'next';

View File

@ -0,0 +1,14 @@
'use client';
import { signOut } from 'next-auth/react';
export default function LogoutButton() {
return (
<button
type="button"
className="w-full rounded-md bg-indigo-500 p-3 text-white"
onClick={() => signOut({ callbackUrl: '/' })}
>
Logout
</button>
);
}

View File

@ -5,7 +5,7 @@ import { GridTileImage } from './grid/tile';
export async function Carousel() { export async function Carousel() {
// Collections that start with `hidden-*` are hidden from the search page. // Collections that start with `hidden-*` are hidden from the search page.
const products: Product[] = (await (woocommerce.get('products'))); const products: Product[] = await woocommerce.get('products');
if (!products?.length) return null; if (!products?.length) return null;
@ -20,7 +20,7 @@ export async function Carousel() {
key={`${product.id}${i}`} key={`${product.id}${i}`}
className="relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3" className="relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3"
> >
<Link href={`/product/${product.id}`} className="relative h-full w-full"> <Link href={`/product/${product.slug}`} className="relative h-full w-full">
<GridTileImage <GridTileImage
alt={product.name} alt={product.name}
label={{ label={{

View File

@ -5,17 +5,12 @@ import clsx from 'clsx';
import { Product } from 'lib/woocomerce/models/product'; import { Product } from 'lib/woocomerce/models/product';
import { useCart } from './cart-context'; import { useCart } from './cart-context';
function SubmitButton({ function SubmitButton({}: {}) {
}: {
}) {
const buttonClasses = const buttonClasses =
'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white'; 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
return ( return (
<button <button aria-label="Please select an option" className={clsx(buttonClasses)}>
aria-label="Please select an option"
className={clsx(buttonClasses)}
>
<div className="absolute left-0 ml-4"> <div className="absolute left-0 ml-4">
<PlusIcon className="h-5" /> <PlusIcon className="h-5" />
</div> </div>
@ -31,7 +26,12 @@ export function AddToCart({ product }: { product: Product }) {
<form <form
action={async () => { action={async () => {
try { try {
const cart = await (await fetch('/api/cart', {method: 'POST', body: JSON.stringify({ id: product.id, quantity: 1, variation: [] })},)).json(); const cart = await (
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ id: product.id, quantity: 1, variation: [] })
})
).json();
setNewCart(cart); setNewCart(cart);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -5,8 +5,12 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
type UpdateType = 'plus' | 'minus' | 'delete'; type UpdateType = 'plus' | 'minus' | 'delete';
type UpdatePayload = { key: string | number; quantity: number;}; type UpdatePayload = { key: string | number; quantity: number };
type AddPayload = { id: string | number; quantity: number; variation: { attribute: string; value: string }[] }; type AddPayload = {
id: string | number;
quantity: number;
variation: { attribute: string; value: string }[];
};
type RemovePayload = { key: string | number }; type RemovePayload = { key: string | number };
type CartContextType = { type CartContextType = {
@ -16,17 +20,11 @@ type CartContextType = {
const CartContext = createContext<CartContextType | undefined>(undefined); const CartContext = createContext<CartContextType | undefined>(undefined);
export function CartProvider({ export function CartProvider({ value, children }: { value: Cart; children: React.ReactNode }) {
value,
children,
}: {
value: Cart;
children: React.ReactNode;
}) {
const [cart, setCart] = useState<Cart | undefined>(value); const [cart, setCart] = useState<Cart | undefined>(value);
const setNewCart = (cart: Cart) => { const setNewCart = (cart: Cart) => {
setCart(cart); setCart(cart);
} };
useEffect(() => { useEffect(() => {
setCart(value); setCart(value);
@ -36,7 +34,7 @@ export function CartProvider({
<CartContext.Provider <CartContext.Provider
value={{ value={{
cart, cart,
setNewCart, setNewCart
}} }}
> >
{children} {children}

View File

@ -4,18 +4,16 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
import { CartItem } from 'lib/woocomerce/models/cart'; import { CartItem } from 'lib/woocomerce/models/cart';
import { useCart } from './cart-context'; import { useCart } from './cart-context';
export function DeleteItemButton({ export function DeleteItemButton({ item }: { item: CartItem }) {
item,
}: {
item: CartItem;
}) {
const { setNewCart } = useCart(); const { setNewCart } = useCart();
return ( return (
<form <form
action={async () => { action={async () => {
try { try {
const cart = await (await fetch('/api/cart', {method: 'DELETE', body: JSON.stringify({ key: item.key })})).json(); const cart = await (
await fetch('/api/cart', { method: 'DELETE', body: JSON.stringify({ key: item.key }) })
).json();
setNewCart(cart); setNewCart(cart);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -26,29 +26,24 @@ function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
); );
} }
export function EditItemQuantityButton({ export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) {
item,
type,
}: {
item: CartItem;
type: 'plus' | 'minus';
}) {
const { setNewCart } = useCart(); const { setNewCart } = useCart();
const payload = { const payload = {
key: item.key, key: item.key,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1, quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
}; };
return ( return (
<form <form
action={async () => { action={async () => {
try { try {
const cart = await (await fetch('/api/cart', {method: 'PUT', body: JSON.stringify(payload)})).json(); const cart = await (
await fetch('/api/cart', { method: 'PUT', body: JSON.stringify(payload) })
).json();
setNewCart(cart); setNewCart(cart);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}} }}
> >
<SubmitButton type={type} /> <SubmitButton type={type} />

View File

@ -15,7 +15,6 @@ import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button'; import { EditItemQuantityButton } from './edit-item-quantity-button';
import OpenCart from './open-cart'; import OpenCart from './open-cart';
export default function CartModal() { export default function CartModal() {
const { cart, setNewCart } = useCart(); const { cart, setNewCart } = useCart();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -37,7 +36,14 @@ export default function CartModal() {
return ( return (
<> <>
<button aria-label="Open cart" onClick={openCart}> <button aria-label="Open cart" onClick={openCart}>
<OpenCart quantity={cart?.items?.map((item) => item.quantity).reduce((a, b) => a + b, 0).toString() ?? '0'} /> <OpenCart
quantity={
cart?.items
?.map((item) => item.quantity)
.reduce((a, b) => a + b, 0)
.toString() ?? '0'
}
/>
</button> </button>
<Transition show={isOpen}> <Transition show={isOpen}>
<Dialog onClose={closeCart} className="relative z-50"> <Dialog onClose={closeCart} className="relative z-50">
@ -77,10 +83,9 @@ export default function CartModal() {
) : ( ) : (
<div className="flex h-full flex-col justify-between overflow-hidden p-1"> <div className="flex h-full flex-col justify-between overflow-hidden p-1">
<ul className="flex-grow overflow-auto py-4"> <ul className="flex-grow overflow-auto py-4">
{cart.items?.length && cart.items {cart.items?.length &&
.sort((a, b) => cart.items
a.name.localeCompare(b.name) .sort((a, b) => a.name.localeCompare(b.name))
)
.map((item, i) => { .map((item, i) => {
return ( return (
<li <li
@ -97,9 +102,7 @@ export default function CartModal() {
className="h-full w-full object-cover" className="h-full w-full object-cover"
width={64} width={64}
height={64} height={64}
alt={ alt={item.name}
item.name
}
src={item.images?.[0]?.src || ''} src={item.images?.[0]?.src || ''}
/> />
</div> </div>
@ -109,9 +112,7 @@ export default function CartModal() {
className="z-30 ml-2 flex flex-row space-x-4" className="z-30 ml-2 flex flex-row space-x-4"
> >
<div className="flex flex-1 flex-col text-base"> <div className="flex flex-1 flex-col text-base">
<span className="leading-tight"> <span className="leading-tight">{item.name}</span>
{item.name}
</span>
</div> </div>
</Link> </Link>
</div> </div>
@ -123,17 +124,11 @@ export default function CartModal() {
currencyCode={item.prices.currency_code} currencyCode={item.prices.currency_code}
/> />
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700"> <div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
<EditItemQuantityButton <EditItemQuantityButton item={item} type="minus" />
item={item}
type="minus"
/>
<p className="w-6 text-center"> <p className="w-6 text-center">
<span className="w-full text-sm">{item.quantity}</span> <span className="w-full text-sm">{item.quantity}</span>
</p> </p>
<EditItemQuantityButton <EditItemQuantityButton item={item} type="plus" />
item={item}
type="plus"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -43,8 +43,7 @@ export function ThreeItemGridItem({
export async function ThreeItemGrid() { export async function ThreeItemGrid() {
// Collections that start with `hidden-*` are hidden from the search page. // Collections that start with `hidden-*` are hidden from the search page.
const products: Product[] = (await woocommerce.get('products')); const products: Product[] = await woocommerce.get('products');
const [firstProduct, secondProduct, thirdProduct] = products; const [firstProduct, secondProduct, thirdProduct] = products;

View File

@ -0,0 +1,16 @@
import { UserCircleIcon } from '@heroicons/react/24/outline';
import { authOptions } from 'app/api/auth/[...nextauth]/route';
import { getServerSession } from 'next-auth';
import Link from 'next/link';
export default async function UserIcon() {
const isAuthenticated = (await getServerSession(authOptions))?.user?.token;
return (
<Link href={!isAuthenticated ? '/login' : '/profile'} className="ms-2" aria-label="login">
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white">
<UserCircleIcon className="h-4 transition-all ease-in-out hover:scale-110" />
</div>
</Link>
);
}

View File

@ -1,7 +1,6 @@
import Link from 'next/link';
import FooterMenu from 'components/layout/footer-menu'; import FooterMenu from 'components/layout/footer-menu';
import LogoSquare from 'components/logo-square'; import LogoSquare from 'components/logo-square';
import Link from 'next/link';
import { Suspense } from 'react'; import { Suspense } from 'react';
const { COMPANY_NAME, SITE_NAME } = process.env; const { COMPANY_NAME, SITE_NAME } = process.env;
@ -20,7 +19,7 @@ export default async function Footer() {
{ {
title: 'Shop', title: 'Shop',
path: '/shop' path: '/shop'
}, }
] as Menu[]; ] as Menu[];
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : ''); const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');

View File

@ -1,6 +1,5 @@
import CartModal from 'components/cart/modal'; import CartModal from 'components/cart/modal';
import LoginModal from 'components/login/modal'; import UserIcon from 'components/icons/UserIcon';
import LogoSquare from 'components/logo-square'; import LogoSquare from 'components/logo-square';
import { Category } from 'lib/woocomerce/models/base'; import { Category } from 'lib/woocomerce/models/base';
import { woocommerce } from 'lib/woocomerce/woocommerce'; import { woocommerce } from 'lib/woocomerce/woocommerce';
@ -18,7 +17,7 @@ type Menu = {
}; };
export async function Navbar() { export async function Navbar() {
const categories: Category[] = (await (woocommerce.get('products/categories'))); const categories: Category[] = await woocommerce.get('products/categories');
const menu = [ const menu = [
{ {
title: 'Home', title: 'Home',
@ -71,8 +70,8 @@ export async function Navbar() {
</Suspense> </Suspense>
</div> </div>
<div className="flex justify-end md:w-1/3"> <div className="flex justify-end md:w-1/3">
<LoginModal />
<CartModal /> <CartModal />
<UserIcon />
</div> </div>
</div> </div>
</nav> </nav>

View File

@ -10,7 +10,7 @@ export default function ProductGridItems({ products }: { products: Product[] })
<Grid.Item key={product.id} className="animate-fadeIn"> <Grid.Item key={product.id} className="animate-fadeIn">
<Link <Link
className="relative inline-block h-full w-full" className="relative inline-block h-full w-full"
href={`/product/${product.id}`} href={`/product/${product.slug}`}
prefetch={true} prefetch={true}
> >
<GridTileImage <GridTileImage

View File

@ -1,11 +1,9 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { Suspense } from 'react'; import { Suspense } from 'react';
import FilterList, { ListItem } from './filter';
import { getCollections } from 'lib/shopify';
import FilterList from './filter';
async function CollectionList() { async function CollectionList() {
const collections = await getCollections(); const collections: ListItem[] = [];
return <FilterList list={collections} title="Collections" />; return <FilterList list={collections} title="Collections" />;
} }

View File

@ -1,133 +0,0 @@
'use client';
import { Dialog, Transition } from '@headlessui/react';
import { UserCircleIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { useCart } from 'components/cart/cart-context';
import { signIn, signOut, useSession } from 'next-auth/react';
import { Fragment, useEffect, useState } from 'react';
export default function LoginModal() {
const [isOpen, setIsOpen] = useState(false);
const [isLogged, setIsLogged] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const openLogin = () => setIsOpen(true);
const closeLogin = () => setIsOpen(false);
const {setNewCart} = useCart();
const {data} = useSession();
useEffect(() => {
if (data?.user.token) {
setIsLogged(true);
}
}, [data]);
const handleLogin = async (event: React.FormEvent) => {
event.preventDefault();
try {
const res = await signIn('credentials', {username, password, redirect: false});
const cart = await (await fetch('/api/cart')).json();
setNewCart(cart);
closeLogin();
} catch (error) {
console.error(error);
}
};
return (
<>
<button className="me-2" aria-label="Open cart" onClick={openLogin}>
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white">
<UserCircleIcon className="h-4 transition-all ease-in-out hover:scale-110" />
</div>
</button>
<Transition show={isOpen}>
<Dialog onClose={closeLogin} className="relative z-50">
<Transition.Child
as={Fragment}
enter="transition-all ease-in-out duration-300"
enterFrom="opacity-0 backdrop-blur-none"
enterTo="opacity-100 backdrop-blur-[.5px]"
leave="transition-all ease-in-out duration-200"
leaveFrom="opacity-100 backdrop-blur-[.5px]"
leaveTo="opacity-0 backdrop-blur-none"
>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="transition-all ease-in-out duration-300"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition-all ease-in-out duration-200"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<Dialog.Panel className="fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-200 bg-white/80 p-6 text-black backdrop-blur-xl md:w-[390px] dark:border-neutral-700 dark:bg-black/80 dark:text-white">
<div className="flex items-center justify-between">
<p className="text-lg font-semibold">Login</p>
<button aria-label="Close cart" onClick={closeLogin}>
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white">
<XMarkIcon className="h-6 transition-all ease-in-out hover:scale-110" />
</div>
</button>
</div>
{!isLogged ? ( <form onSubmit={handleLogin}>
<div className="mt-4">
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Username
</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
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"
required
/>
</div>
<div className="mt-4">
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Password
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
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"
required
/>
</div>
<div className="mt-6">
<button
type="submit"
className="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-3 text-lg font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Login
</button>
</div>
</form>) : (
<div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
<p className="mt-6 text-center text-2xl font-bold">You are logged in.</p>
<button className="mt-6 flex" onClick={() => signOut()}>
Sign out
</button>
</div>
)}
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
</>
);
}

View File

@ -1,6 +1,6 @@
"use client"; 'use client';
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from 'next-auth/react';
type Props = { type Props = {
children?: React.ReactNode; children?: React.ReactNode;

View File

@ -5,7 +5,7 @@ import { GridTileImage } from 'components/grid/tile';
import { useProduct, useUpdateURL } from 'components/product/product-context'; import { useProduct, useUpdateURL } from 'components/product/product-context';
import Image from 'next/image'; import Image from 'next/image';
export function Gallery({ images }: { images: { id: number, src: string; altText: string }[] }) { export function Gallery({ images }: { images: { id: number; src: string; altText: string }[] }) {
const { state, updateImage } = useProduct(); const { state, updateImage } = useProduct();
const updateURL = useUpdateURL(); const updateURL = useUpdateURL();
const imageIndex = state.image ? parseInt(state.image) : 0; const imageIndex = state.image ? parseInt(state.image) : 0;
@ -60,7 +60,7 @@ export function Gallery({ images }: { images: { id: number, src: string; altText
</div> </div>
{images.length > 1 ? ( {images.length > 1 ? (
<ul className="my-12 flex items-center flex-wrap justify-center gap-2 overflow-auto py-1 lg:mb-0"> <ul className="my-12 flex flex-wrap items-center justify-center gap-2 overflow-auto py-1 lg:mb-0">
{images.map((image, index) => { {images.map((image, index) => {
const isActive = index === imageIndex; const isActive = index === imageIndex;

View File

@ -9,10 +9,7 @@ export function ProductDescription({ product }: { product: Product }) {
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700"> <div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
<h1 className="mb-2 text-5xl font-medium">{product.name}</h1> <h1 className="mb-2 text-5xl font-medium">{product.name}</h1>
<div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white"> <div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white">
<Price <Price amount={product.price} currencyCode="EUR" />
amount={product.price}
currencyCode='EUR'
/>
</div> </div>
</div> </div>
{product.description ? ( {product.description ? (

View File

@ -41,7 +41,6 @@ export interface CartItem {
extensions: Extensions; extensions: Extensions;
} }
export interface IngAddress { export interface IngAddress {
first_name: string; first_name: string;
last_name: string; last_name: string;
@ -74,8 +73,7 @@ export interface CouponTotals {
currency_suffix: string; currency_suffix: string;
} }
export interface Extensions { export interface Extensions {}
}
export interface Image { export interface Image {
id: number; id: number;
@ -205,8 +203,7 @@ export interface TaxLine {
rate: string; rate: string;
} }
export interface Extensions { export interface Extensions {}
}
export interface Image { export interface Image {
id: number; id: number;

View File

@ -3,8 +3,8 @@ import crypto from 'node:crypto';
import OAuth from 'oauth-1.0a'; import OAuth from 'oauth-1.0a';
import Url from 'url-parse'; import Url from 'url-parse';
import { DELETE, IWooRestApiOptions, WooRestApiEndpoint, WooRestApiMethod } from './clientOptions'; import { DELETE, IWooRestApiOptions, WooRestApiEndpoint, WooRestApiMethod } from './clientOptions';
import { CouponsParams } from './coupon'; import { Coupon, CouponsParams } from './coupon';
import { CustomersParams } from './customer'; import { Customer, CustomersParams } from './customer';
import { Order, OrdersMainParams } from './orders'; import { Order, OrdersMainParams } from './orders';
import { Product, ProductMainParams } from './product'; import { Product, ProductMainParams } from './product';
@ -27,10 +27,28 @@ export type WooRestApiParams = CouponsParams &
/** /**
* Define the response types for each endpoint. * Define the response types for each endpoint.
*/ */
type WooCommerceResponse<T extends WooRestApiEndpoint> = type WooCommerceResponse<
T extends 'products' ? Product[] : T extends WooRestApiEndpoint,
T extends 'orders' ? Order[] : P extends Partial<WooRestApiParams> = {}
any; > = P['id'] extends number | string // Verifica se `id` è definito e di tipo string
? T extends 'products'
? Product
: T extends 'customers'
? Customer
: T extends 'orders'
? Order
: T extends 'coupons'
? Coupon
: any
: T extends 'products'
? Product[]
: T extends 'customers'
? Customer[]
: T extends 'orders'
? Order[]
: T extends 'coupons'
? Coupon[]
: any;
/** /**
* WooCommerce REST API wrapper * WooCommerce REST API wrapper
@ -265,13 +283,13 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
* *
* @return {Object} * @return {Object}
*/ */
_request<T extends WooRestApiEndpoint>( _request<T extends WooRestApiEndpoint, P extends Partial<WooRestApiParams>>(
method: WooRestApiMethod, method: WooRestApiMethod,
endpoint: T, endpoint: T,
data?: Record<string, unknown>, data?: Record<string, unknown>,
params: Record<string, unknown> = {}, params: P = {} as P,
version?: string, version?: string
): Promise<WooCommerceResponse<T>> { ): Promise<WooCommerceResponse<T, P>> {
const url = this._getUrl(endpoint, params, version); const url = this._getUrl(endpoint, params, version);
const header: RawAxiosRequestHeaders = { const header: RawAxiosRequestHeaders = {
Accept: 'application/json' Accept: 'application/json'
@ -330,7 +348,7 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
// Allow set and override Axios options. // Allow set and override Axios options.
options = { ...options, ...this._opt.axiosConfig }; options = { ...options, ...this._opt.axiosConfig };
return axios(options).then((response) => response.data as WooCommerceResponse<T>); return axios(options).then((response) => response.data as WooCommerceResponse<T, P>);
} }
/** /**
@ -341,8 +359,11 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
* *
* @return {Object} * @return {Object}
*/ */
get<T extends WooRestApiEndpoint>(endpoint: T, params?: Partial<WooRestApiParams>): Promise<WooCommerceResponse<T>> { get<T extends WooRestApiEndpoint, P extends Partial<WooRestApiParams>>(
return this._request('GET', endpoint, undefined, params); endpoint: T,
params?: P
): Promise<WooCommerceResponse<T, P>> {
return this._request('GET', endpoint, undefined, params || ({} as P));
} }
/** /**
@ -392,7 +413,7 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
endpoint: T, endpoint: T,
data: Pick<WooRestApiParams, 'force'>, data: Pick<WooRestApiParams, 'force'>,
params: Pick<WooRestApiParams, 'id'> params: Pick<WooRestApiParams, 'id'>
): Promise<WooCommerceResponse<T>> { ): Promise<WooCommerceResponse<T, Pick<WooRestApiParams, 'id'>>> {
return this._request('DELETE', endpoint, data, params); return this._request('DELETE', endpoint, data, params);
} }

View File

@ -1,35 +1,30 @@
export declare type WooRestApiVersion = "wc/v3"; export declare type WooRestApiVersion = 'wc/v3';
// | "wc/v2" // | "wc/v2"
// | "wc/v1" // | "wc/v1"
// | "wc-api/v3" // | "wc-api/v3"
// | "wc-api/v2" // | "wc-api/v2"
// | "wc-api/v1"; // | "wc-api/v1";
export declare type WooRestApiEncoding = "utf-8" | "ascii"; export declare type WooRestApiEncoding = 'utf-8' | 'ascii';
export declare type WooRestApiMethod = export declare type WooRestApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "OPTIONS";
export declare type WooRestApiEndpoint = export declare type WooRestApiEndpoint =
| "coupons" | 'coupons'
| "customers" | 'customers'
| "orders" | 'orders'
| "products" | 'products'
| "products/attributes" | 'products/attributes'
| "products/categories" | 'products/categories'
| "products/shipping_classes" | 'products/shipping_classes'
| "products/tags" | 'products/tags'
| "products/reviews" | 'products/reviews'
| "system_status" | 'system_status'
| "reports" // TODO: add support for reports | 'reports' // TODO: add support for reports
| "settings" // TODO: add support for settings | 'settings' // TODO: add support for settings
| "webhooks" // TODO: add support for webhooks | 'webhooks' // TODO: add support for webhooks
| "shipping" // TODO: add support for shipping | 'shipping' // TODO: add support for shipping
| "shipping_methods" // TODO: add support for shipping_methods | 'shipping_methods' // TODO: add support for shipping_methods
| "taxes" // TODO: add support for taxes | 'taxes' // TODO: add support for taxes
| "payment_gateways" // TODO: add support for payment_gateways | 'payment_gateways' // TODO: add support for payment_gateways
| string; // I need to have next endpoint: "orders/<id>/notes" | string; // I need to have next endpoint: "orders/<id>/notes"
export declare type IWooRestApiQuery = Record<string, unknown>; export declare type IWooRestApiQuery = Record<string, unknown>;
@ -80,7 +75,6 @@ export interface IWooRestApiOptions<T> extends IWooCredentials {
isHttps?: boolean; isHttps?: boolean;
} }
export interface DELETE { export interface DELETE {
id: number | string; id: number | string;
force?: boolean | string; force?: boolean | string;

View File

@ -1,5 +1,5 @@
import { Meta_Data } from "./base"; import { Meta_Data } from './base';
import { Tax } from "./taxes"; import { Tax } from './taxes';
export type Line_Item = { export type Line_Item = {
id: number; id: number;

View File

@ -1,8 +1,7 @@
import { Meta_Data } from './base'; import { Image, Meta_Data } from './base';
import { Billing } from './billing'; import { Billing } from './billing';
import { Coupon_Lines } from './coupon'; import { Coupon_Lines } from './coupon';
import { Fee_Lines } from './fee'; import { Fee_Lines } from './fee';
import { Line_Item } from './item';
import { Order_Refund_Line_Item, Refund } from './refund'; import { Order_Refund_Line_Item, Refund } from './refund';
import { Shipping, Shipping_Line } from './shipping'; import { Shipping, Shipping_Line } from './shipping';
import { Tax_Line } from './taxes'; import { Tax_Line } from './taxes';
@ -43,7 +42,7 @@ export interface Order {
date_completed_gmt: string; date_completed_gmt: string;
cart_hash: string; cart_hash: string;
meta_data: Partial<Meta_Data>[]; meta_data: Partial<Meta_Data>[];
line_items: Partial<Line_Item>[]; line_items: Partial<OrderItem>[];
tax_lines: Partial<Tax_Line>[]; tax_lines: Partial<Tax_Line>[];
shipping_lines: Partial<Shipping_Line>[]; shipping_lines: Partial<Shipping_Line>[];
fee_lines: Partial<Fee_Lines>[]; fee_lines: Partial<Fee_Lines>[];
@ -51,6 +50,25 @@ export interface Order {
refunds: Partial<Refund>[]; refunds: Partial<Refund>[];
set_paid: boolean; set_paid: boolean;
} }
export type OrderItem = {
id: number;
name: string;
product_id: number;
variation_id: number;
quantity: number;
tax_class: string;
subtotal: string;
subtotal_tax: string;
total: string;
total_tax: string;
taxes: any[];
meta_data: Meta_Data[];
sku: string;
price: number;
image: Image;
};
export interface OrderNotes { export interface OrderNotes {
id: number; id: number;
author: string; author: string;

View File

@ -1,12 +1,4 @@
import { import { Attribute, Category, Default_Attribute, Dimension, Image, Meta_Data, Tag } from './base';
Attribute,
Category,
Default_Attribute,
Dimension,
Image,
Meta_Data,
Tag
} from './base';
export interface Product { export interface Product {
id: number; id: number;
@ -211,7 +203,6 @@ type ProductShippingClassesParams = Partial<ProductShippingClass>;
type ProductTagsParams = Partial<ProductTags>; type ProductTagsParams = Partial<ProductTags>;
type ProductReviewsParams = Partial<ProductReviews>; type ProductReviewsParams = Partial<ProductReviews>;
export type ProductMainParams = export type ProductMainParams =
| (ProductParams & ProductVariationsParams & ProductAttributesParams) | (ProductParams & ProductVariationsParams & ProductAttributesParams)
| ProductAttributesTermsParams | ProductAttributesTermsParams

View File

@ -1,4 +1,4 @@
import { Meta_Data } from "./base"; import { Meta_Data } from './base';
export type Refund = { export type Refund = {
id: number; id: number;

View File

@ -11,12 +11,12 @@ class WooCommerceStoreApiClient {
constructor(baseURL: string) { constructor(baseURL: string) {
const headers: RawAxiosRequestHeaders = { const headers: RawAxiosRequestHeaders = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': '*/*', Accept: '*/*'
}; };
this.client = axios.create({ this.client = axios.create({
baseURL, baseURL,
headers, headers
}); });
this.client.interceptors.response.use((response) => { this.client.interceptors.response.use((response) => {
@ -37,16 +37,22 @@ class WooCommerceStoreApiClient {
return this.client.get<Cart>('/cart', { params }).then((response) => response.data); return this.client.get<Cart>('/cart', { params }).then((response) => response.data);
} }
async addToCart(payload: { id: string | number; quantity: number; variation: { attribute: string; value: string }[] }): Promise<Cart> { async addToCart(payload: {
id: string | number;
quantity: number;
variation: { attribute: string; value: string }[];
}): Promise<Cart> {
return this.client.post<Cart>('/cart/add-item', payload).then((response) => response.data); return this.client.post<Cart>('/cart/add-item', payload).then((response) => response.data);
} }
async updateItem(payload: { key: string | number; quantity: number; }): Promise<Cart> { async updateItem(payload: { key: string | number; quantity: number }): Promise<Cart> {
return this.client.post<Cart>('/cart/update-item', payload).then((response) => response.data); return this.client.post<Cart>('/cart/update-item', payload).then((response) => response.data);
} }
async removeFromCart(payload: { key: string | number }): Promise<Cart> { async removeFromCart(payload: { key: string | number }): Promise<Cart> {
return this.client.post<Cart>(`/cart/remove-item?key=${payload.key}`).then((response) => response.data); return this.client
.post<Cart>(`/cart/remove-item?key=${payload.key}`)
.then((response) => response.data);
} }
} }

View File

@ -1,11 +1,13 @@
import WooCommerceRestApi, { WooRestApiOptions } from "./models/client"; import WooCommerceRestApi, { WooRestApiOptions } from './models/client';
const option: WooRestApiOptions = { const option: WooRestApiOptions = {
url: process.env.WOOCOMMERCE_URL ?? "http://wordpress.localhost", url: process.env.WOOCOMMERCE_URL ?? 'http://wordpress.localhost',
consumerKey: process.env.WOOCOMMERCE_CONSUMER_KEY ?? "ck_1fb0a3c9b50ae813c31c7effc086a809d8416d90", consumerKey:
consumerSecret: process.env.WOOCOMMERCE_CONSUMER_SECRET ?? "cs_ee4f1c9e061d07a7cb6025b69d414189a9157e20", process.env.WOOCOMMERCE_CONSUMER_KEY ?? 'ck_1fb0a3c9b50ae813c31c7effc086a809d8416d90',
consumerSecret:
process.env.WOOCOMMERCE_CONSUMER_SECRET ?? 'cs_ee4f1c9e061d07a7cb6025b69d414189a9157e20',
isHttps: false, isHttps: false,
version: "wc/v3", version: 'wc/v3',
queryStringAuth: false // Force Basic Authentication as query string true and using under queryStringAuth: false // Force Basic Authentication as query string true and using under
} };
export const woocommerce = new WooCommerceRestApi(option); export const woocommerce = new WooCommerceRestApi(option);

View File

@ -3,8 +3,8 @@ export default {
formats: ['image/avif', 'image/webp'], formats: ['image/avif', 'image/webp'],
remotePatterns: [ remotePatterns: [
{ {
protocol: "http", protocol: 'http',
hostname: "**", hostname: '**'
} }
] ]
} }

1138
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,13 @@
} }
] ]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "types/**/*.d.ts"], "include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"types/**/*.d.ts",
"app/profile/orders/[id]"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -4,14 +4,16 @@ import 'next-auth/jwt';
declare module 'next-auth' { declare module 'next-auth' {
interface Session { interface Session {
user: { user: {
store_id: number;
token: string; token: string;
user_email: string; user_email: string;
user_nicename: string; user_nicename: string;
user_display_name: string; user_display_name: string;
} };
} }
interface User { interface User {
store_id: number;
token: string; token: string;
user_email: string; user_email: string;
user_nicename: string; user_nicename: string;
@ -22,10 +24,11 @@ declare module 'next-auth' {
declare module 'next-auth/jwt' { declare module 'next-auth/jwt' {
interface JWT { interface JWT {
user: { user: {
store_id: number;
token: string; token: string;
user_email: string; user_email: string;
user_nicename: string; user_nicename: string;
user_display_name: string; user_display_name: string;
} };
} }
} }