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> {
const params = await props.params;
return {
title: '',
description: '',

View File

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

View File

@ -1,12 +1,10 @@
import { ThreeItemGridItem } from 'components/grid/three-items';
import { Product } from 'lib/woocomerce/models/product';
import { woocommerce } from 'lib/woocomerce/woocommerce';
export default async function ProductPage(props: { params: Promise<{ name: string }> }) {
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 (
<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 { Navbar } from 'components/layout/navbar';
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 }>;
}): Promise<Metadata> {
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();
@ -31,13 +33,15 @@ export async function generateMetadata(props: {
index: indexable,
follow: indexable
}
},
}
};
}
export default async function ProductPage(props: { params: Promise<{ name: string }> }) {
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();
@ -49,9 +53,8 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
image: product.images?.[0]?.src,
offers: {
'@type': 'AggregateOffer',
availability: product.stock_quantity > 0
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
availability:
product.stock_quantity > 0 ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
priceCurrency: product.price,
highPrice: product.max_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 default async function Image({ params }: { params: { collection: string } }) {
return await OpengraphImage({ title: '' });
}

View File

@ -7,8 +7,7 @@ export default function SearchLayout({ children }: { children: React.ReactNode }
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="order-first w-full flex-none md:max-w-[125px]">
</div>
<div className="order-first w-full flex-none md:max-w-[125px]"></div>
<div className="order-last min-h-screen w-full md:order-none">
<ChildrenWrapper>{children}</ChildrenWrapper>
</div>

View File

@ -15,7 +15,7 @@ export default async function SearchPage(props: {
const { sort, q: searchValue } = searchParams as { [key: string]: string };
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';
return (

View File

@ -1,4 +1,3 @@
import { validateEnvironmentVariables } from 'lib/utils';
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() {
// 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;
@ -20,7 +20,7 @@ export async function Carousel() {
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"
>
<Link href={`/product/${product.id}`} className="relative h-full w-full">
<Link href={`/product/${product.slug}`} className="relative h-full w-full">
<GridTileImage
alt={product.name}
label={{

View File

@ -5,17 +5,12 @@ import clsx from 'clsx';
import { Product } from 'lib/woocomerce/models/product';
import { useCart } from './cart-context';
function SubmitButton({
}: {
}) {
function SubmitButton({}: {}) {
const buttonClasses =
'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
return (
<button
aria-label="Please select an option"
className={clsx(buttonClasses)}
>
<button aria-label="Please select an option" className={clsx(buttonClasses)}>
<div className="absolute left-0 ml-4">
<PlusIcon className="h-5" />
</div>
@ -31,7 +26,12 @@ export function AddToCart({ product }: { product: Product }) {
<form
action={async () => {
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);
} catch (error) {
console.error(error);

View File

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

View File

@ -4,18 +4,16 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
import { CartItem } from 'lib/woocomerce/models/cart';
import { useCart } from './cart-context';
export function DeleteItemButton({
item,
}: {
item: CartItem;
}) {
export function DeleteItemButton({ item }: { item: CartItem }) {
const { setNewCart } = useCart();
return (
<form
action={async () => {
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);
} catch (error) {
console.error(error);

View File

@ -26,29 +26,24 @@ function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
);
}
export function EditItemQuantityButton({
item,
type,
}: {
item: CartItem;
type: 'plus' | 'minus';
}) {
export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) {
const { setNewCart } = useCart();
const payload = {
key: item.key,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
};
return (
<form
action={async () => {
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);
} catch (error) {
console.error(error);
}
}}
>
<SubmitButton type={type} />

View File

@ -15,7 +15,6 @@ import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button';
import OpenCart from './open-cart';
export default function CartModal() {
const { cart, setNewCart } = useCart();
const [isOpen, setIsOpen] = useState(false);
@ -37,7 +36,14 @@ export default function CartModal() {
return (
<>
<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>
<Transition show={isOpen}>
<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">
<ul className="flex-grow overflow-auto py-4">
{cart.items?.length && cart.items
.sort((a, b) =>
a.name.localeCompare(b.name)
)
{cart.items?.length &&
cart.items
.sort((a, b) => a.name.localeCompare(b.name))
.map((item, i) => {
return (
<li
@ -97,9 +102,7 @@ export default function CartModal() {
className="h-full w-full object-cover"
width={64}
height={64}
alt={
item.name
}
alt={item.name}
src={item.images?.[0]?.src || ''}
/>
</div>
@ -109,9 +112,7 @@ export default function CartModal() {
className="z-30 ml-2 flex flex-row space-x-4"
>
<div className="flex flex-1 flex-col text-base">
<span className="leading-tight">
{item.name}
</span>
<span className="leading-tight">{item.name}</span>
</div>
</Link>
</div>
@ -123,17 +124,11 @@ export default function CartModal() {
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">
<EditItemQuantityButton
item={item}
type="minus"
/>
<EditItemQuantityButton item={item} type="minus" />
<p className="w-6 text-center">
<span className="w-full text-sm">{item.quantity}</span>
</p>
<EditItemQuantityButton
item={item}
type="plus"
/>
<EditItemQuantityButton item={item} type="plus" />
</div>
</div>
</div>

View File

@ -43,8 +43,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 products: Product[] = await woocommerce.get('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 LogoSquare from 'components/logo-square';
import Link from 'next/link';
import { Suspense } from 'react';
const { COMPANY_NAME, SITE_NAME } = process.env;
@ -20,7 +19,7 @@ export default async function Footer() {
{
title: 'Shop',
path: '/shop'
},
}
] as Menu[];
const currentYear = new Date().getFullYear();
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');

View File

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

View File

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

View File

@ -1,11 +1,9 @@
import clsx from 'clsx';
import { Suspense } from 'react';
import { getCollections } from 'lib/shopify';
import FilterList from './filter';
import FilterList, { ListItem } from './filter';
async function CollectionList() {
const collections = await getCollections();
const collections: ListItem[] = [];
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 = {
children?: React.ReactNode;

View File

@ -5,7 +5,7 @@ import { GridTileImage } from 'components/grid/tile';
import { useProduct, useUpdateURL } from 'components/product/product-context';
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 updateURL = useUpdateURL();
const imageIndex = state.image ? parseInt(state.image) : 0;
@ -60,7 +60,7 @@ export function Gallery({ images }: { images: { id: number, src: string; altText
</div>
{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) => {
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">
<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">
<Price
amount={product.price}
currencyCode='EUR'
/>
<Price amount={product.price} currencyCode="EUR" />
</div>
</div>
{product.description ? (

View File

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

View File

@ -3,8 +3,8 @@ import crypto from 'node:crypto';
import OAuth from 'oauth-1.0a';
import Url from 'url-parse';
import { DELETE, IWooRestApiOptions, WooRestApiEndpoint, WooRestApiMethod } from './clientOptions';
import { CouponsParams } from './coupon';
import { CustomersParams } from './customer';
import { Coupon, CouponsParams } from './coupon';
import { Customer, CustomersParams } from './customer';
import { Order, OrdersMainParams } from './orders';
import { Product, ProductMainParams } from './product';
@ -27,10 +27,28 @@ export type WooRestApiParams = CouponsParams &
/**
* Define the response types for each endpoint.
*/
type WooCommerceResponse<T extends WooRestApiEndpoint> =
T extends 'products' ? Product[] :
T extends 'orders' ? Order[] :
any;
type WooCommerceResponse<
T extends WooRestApiEndpoint,
P extends Partial<WooRestApiParams> = {}
> = 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
@ -265,13 +283,13 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
*
* @return {Object}
*/
_request<T extends WooRestApiEndpoint>(
_request<T extends WooRestApiEndpoint, P extends Partial<WooRestApiParams>>(
method: WooRestApiMethod,
endpoint: T,
data?: Record<string, unknown>,
params: Record<string, unknown> = {},
version?: string,
): Promise<WooCommerceResponse<T>> {
params: P = {} as P,
version?: string
): Promise<WooCommerceResponse<T, P>> {
const url = this._getUrl(endpoint, params, version);
const header: RawAxiosRequestHeaders = {
Accept: 'application/json'
@ -330,7 +348,7 @@ export default class WooCommerceRestApi<T extends WooRestApiOptions> {
// Allow set and override Axios options.
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}
*/
get<T extends WooRestApiEndpoint>(endpoint: T, params?: Partial<WooRestApiParams>): Promise<WooCommerceResponse<T>> {
return this._request('GET', endpoint, undefined, params);
get<T extends WooRestApiEndpoint, P extends Partial<WooRestApiParams>>(
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,
data: Pick<WooRestApiParams, 'force'>,
params: Pick<WooRestApiParams, 'id'>
): Promise<WooCommerceResponse<T>> {
): Promise<WooCommerceResponse<T, Pick<WooRestApiParams, 'id'>>> {
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/v1"
// | "wc-api/v3"
// | "wc-api/v2"
// | "wc-api/v1";
export declare type WooRestApiEncoding = "utf-8" | "ascii";
export declare type WooRestApiMethod =
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "OPTIONS";
export declare type WooRestApiEncoding = 'utf-8' | 'ascii';
export declare type WooRestApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
export declare type WooRestApiEndpoint =
| "coupons"
| "customers"
| "orders"
| "products"
| "products/attributes"
| "products/categories"
| "products/shipping_classes"
| "products/tags"
| "products/reviews"
| "system_status"
| "reports" // TODO: add support for reports
| "settings" // TODO: add support for settings
| "webhooks" // TODO: add support for webhooks
| "shipping" // TODO: add support for shipping
| "shipping_methods" // TODO: add support for shipping_methods
| "taxes" // TODO: add support for taxes
| "payment_gateways" // TODO: add support for payment_gateways
| 'coupons'
| 'customers'
| 'orders'
| 'products'
| 'products/attributes'
| 'products/categories'
| 'products/shipping_classes'
| 'products/tags'
| 'products/reviews'
| 'system_status'
| 'reports' // TODO: add support for reports
| 'settings' // TODO: add support for settings
| 'webhooks' // TODO: add support for webhooks
| 'shipping' // TODO: add support for shipping
| 'shipping_methods' // TODO: add support for shipping_methods
| 'taxes' // TODO: add support for taxes
| 'payment_gateways' // TODO: add support for payment_gateways
| string; // I need to have next endpoint: "orders/<id>/notes"
export declare type IWooRestApiQuery = Record<string, unknown>;
@ -80,7 +75,6 @@ export interface IWooRestApiOptions<T> extends IWooCredentials {
isHttps?: boolean;
}
export interface DELETE {
id: number | string;
force?: boolean | string;

View File

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

View File

@ -1,8 +1,7 @@
import { Meta_Data } from './base';
import { Image, Meta_Data } from './base';
import { Billing } from './billing';
import { Coupon_Lines } from './coupon';
import { Fee_Lines } from './fee';
import { Line_Item } from './item';
import { Order_Refund_Line_Item, Refund } from './refund';
import { Shipping, Shipping_Line } from './shipping';
import { Tax_Line } from './taxes';
@ -43,7 +42,7 @@ export interface Order {
date_completed_gmt: string;
cart_hash: string;
meta_data: Partial<Meta_Data>[];
line_items: Partial<Line_Item>[];
line_items: Partial<OrderItem>[];
tax_lines: Partial<Tax_Line>[];
shipping_lines: Partial<Shipping_Line>[];
fee_lines: Partial<Fee_Lines>[];
@ -51,6 +50,25 @@ export interface Order {
refunds: Partial<Refund>[];
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 {
id: number;
author: string;

View File

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

View File

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

View File

@ -11,12 +11,12 @@ class WooCommerceStoreApiClient {
constructor(baseURL: string) {
const headers: RawAxiosRequestHeaders = {
'Content-Type': 'application/json',
'Accept': '*/*',
Accept: '*/*'
};
this.client = axios.create({
baseURL,
headers,
headers
});
this.client.interceptors.response.use((response) => {
@ -37,16 +37,22 @@ class WooCommerceStoreApiClient {
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);
}
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);
}
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 = {
url: process.env.WOOCOMMERCE_URL ?? "http://wordpress.localhost",
consumerKey: process.env.WOOCOMMERCE_CONSUMER_KEY ?? "ck_1fb0a3c9b50ae813c31c7effc086a809d8416d90",
consumerSecret: process.env.WOOCOMMERCE_CONSUMER_SECRET ?? "cs_ee4f1c9e061d07a7cb6025b69d414189a9157e20",
url: process.env.WOOCOMMERCE_URL ?? 'http://wordpress.localhost',
consumerKey:
process.env.WOOCOMMERCE_CONSUMER_KEY ?? 'ck_1fb0a3c9b50ae813c31c7effc086a809d8416d90',
consumerSecret:
process.env.WOOCOMMERCE_CONSUMER_SECRET ?? 'cs_ee4f1c9e061d07a7cb6025b69d414189a9157e20',
isHttps: false,
version: "wc/v3",
version: 'wc/v3',
queryStringAuth: false // Force Basic Authentication as query string true and using under
}
};
export const woocommerce = new WooCommerceRestApi(option);

View File

@ -3,8 +3,8 @@ export default {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{
protocol: "http",
hostname: "**",
protocol: 'http',
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"]
}

View File

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