mirror of
https://github.com/vercel/commerce.git
synced 2025-06-28 01:11:24 +00:00
feat: add translations without routing
This commit is contained in:
parent
ae6ec92608
commit
57c718f388
@ -9,6 +9,7 @@ import Price from 'components/price';
|
|||||||
import { Billing } from 'lib/woocomerce/models/billing';
|
import { Billing } from 'lib/woocomerce/models/billing';
|
||||||
import { PaymentGateways } from 'lib/woocomerce/models/payment';
|
import { PaymentGateways } from 'lib/woocomerce/models/payment';
|
||||||
import { Shipping } from 'lib/woocomerce/models/shipping';
|
import { Shipping } from 'lib/woocomerce/models/shipping';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@ -26,6 +27,7 @@ const shippingSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function CheckoutPage() {
|
export default function CheckoutPage() {
|
||||||
|
const t = useTranslations('Checkout');
|
||||||
const { cart } = useCart();
|
const { cart } = useCart();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { checkout, setShipping, setBilling, setPayment } = useCheckout();
|
const { checkout, setShipping, setBilling, setPayment } = useCheckout();
|
||||||
@ -70,7 +72,7 @@ export default function CheckoutPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto grid h-full gap-4 px-4 pb-4">
|
<section className="mx-auto grid h-full gap-4 px-4 pb-4">
|
||||||
<p>Checkout</p>
|
<p>{t('title')}</p>
|
||||||
<form
|
<form
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
className="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black"
|
className="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-black"
|
||||||
@ -95,7 +97,7 @@ export default function CheckoutPage() {
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="mb-3 flex items-center justify-between pb-4 pt-4 dark:border-neutral-700">
|
<div className="mb-3 flex items-center justify-between pb-4 pt-4 dark:border-neutral-700">
|
||||||
<p>Total</p>
|
<p>{t('total')}</p>
|
||||||
<Price
|
<Price
|
||||||
className="text-right text-base text-black dark:text-white"
|
className="text-right text-base text-black dark:text-white"
|
||||||
amount={cart?.totals?.total_price}
|
amount={cart?.totals?.total_price}
|
||||||
@ -113,7 +115,7 @@ export default function CheckoutPage() {
|
|||||||
selectionMode="multiple"
|
selectionMode="multiple"
|
||||||
className="text-white sm:w-full md:w-2/3"
|
className="text-white sm:w-full md:w-2/3"
|
||||||
>
|
>
|
||||||
<AccordionItem key="1" title="Shipping Info" className="text-white">
|
<AccordionItem key="1" title={t('shipping')} className="text-white">
|
||||||
<ShippingForm
|
<ShippingForm
|
||||||
onChangeInput={(e) => {
|
onChangeInput={(e) => {
|
||||||
const updatedShipping = {
|
const updatedShipping = {
|
||||||
@ -126,10 +128,10 @@ export default function CheckoutPage() {
|
|||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
<Checkbox defaultSelected onValueChange={(v) => setSameBilling(v)} className="mt-2">
|
<Checkbox defaultSelected onValueChange={(v) => setSameBilling(v)} className="mt-2">
|
||||||
Hai bisogno di fatturazione?
|
{t('billingCheckbox')}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem key="2" title="Billing Info" className="text-white">
|
<AccordionItem key="2" title={t('billing')} className="text-white">
|
||||||
<ShippingForm
|
<ShippingForm
|
||||||
onChangeInput={(e) => {
|
onChangeInput={(e) => {
|
||||||
const updatedBilling = {
|
const updatedBilling = {
|
||||||
@ -141,7 +143,7 @@ export default function CheckoutPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem key="3" title="Payment" className="text-white">
|
<AccordionItem key="3" title={t('payment')} className="text-white">
|
||||||
<div className="flex flex-col justify-between overflow-hidden">
|
<div className="flex flex-col justify-between overflow-hidden">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
@ -6,6 +6,8 @@ import { NextAuthProvider } from 'components/next-session-provider';
|
|||||||
import { WelcomeToast } from 'components/welcome-toast';
|
import { WelcomeToast } from 'components/welcome-toast';
|
||||||
import { GeistSans } from 'geist/font/sans';
|
import { GeistSans } from 'geist/font/sans';
|
||||||
import { ensureStartsWith } from 'lib/utils';
|
import { ensureStartsWith } from 'lib/utils';
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { getLocale, getMessages } from 'next-intl/server';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
@ -38,22 +40,27 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({ children }: { children: ReactNode }) {
|
export default async function RootLayout({ children }: { children: ReactNode }) {
|
||||||
|
const locale = await getLocale();
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={GeistSans.variable}>
|
<html lang={locale} className={GeistSans.variable}>
|
||||||
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
||||||
<NextAuthProvider>
|
<NextIntlClientProvider messages={messages}>
|
||||||
<CartProvider>
|
<NextAuthProvider>
|
||||||
<CheckoutProvider>
|
<CartProvider>
|
||||||
<Navbar />
|
<CheckoutProvider>
|
||||||
<main>
|
<Navbar />
|
||||||
{children}
|
<main>
|
||||||
<Toaster closeButton />
|
{children}
|
||||||
<WelcomeToast />
|
<Toaster closeButton />
|
||||||
</main>
|
<WelcomeToast />
|
||||||
</CheckoutProvider>
|
</main>
|
||||||
</CartProvider>
|
</CheckoutProvider>
|
||||||
<Footer />
|
</CartProvider>
|
||||||
</NextAuthProvider>
|
<Footer />
|
||||||
|
</NextAuthProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
22
app/page.tsx
22
app/page.tsx
@ -11,8 +11,12 @@ import { Category } from 'lib/woocomerce/models/base';
|
|||||||
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';
|
||||||
import { wordpress } from 'lib/wordpress/wordpress';
|
import { wordpress } from 'lib/wordpress/wordpress';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
import Flowers from '../assets/images/fiori.png';
|
||||||
|
import ManWild from '../assets/images/man-wild.png';
|
||||||
|
|
||||||
async function Products({ category }: { category: Category }) {
|
async function Products({ category }: { category: Category }) {
|
||||||
const products: Product[] = await woocommerce.get('products', {
|
const products: Product[] = await woocommerce.get('products', {
|
||||||
@ -24,9 +28,19 @@ async function Products({ category }: { category: Category }) {
|
|||||||
|
|
||||||
async function ProductsByCategory() {
|
async function ProductsByCategory() {
|
||||||
const categories: Category[] = await woocommerce.get('products/categories');
|
const categories: Category[] = await woocommerce.get('products/categories');
|
||||||
|
const t = await getTranslations('HomePage');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div>
|
||||||
|
<Image alt="" src={Flowers} className="mb-4 h-[440px] w-full object-cover" />
|
||||||
|
<Link
|
||||||
|
href={''}
|
||||||
|
className="absolute right-20 top-80 text-2xl font-bold text-white underline"
|
||||||
|
>
|
||||||
|
{t('helpIA')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
{categories.map((category, index) => (
|
{categories.map((category, index) => (
|
||||||
<div key={category.id}>
|
<div key={category.id}>
|
||||||
<div className="mb-2 mt-6 flex items-center justify-between px-4">
|
<div className="mb-2 mt-6 flex items-center justify-between px-4">
|
||||||
@ -45,7 +59,10 @@ async function ProductsByCategory() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
{index === 1 && (
|
{index === 1 && (
|
||||||
<div className="my-6 flex flex-col px-4">
|
<div className="my-6 flex flex-col px-4">
|
||||||
<span className="mb-2 text-2xl font-bold">Top products</span>
|
<div className="-mx-4">
|
||||||
|
<Image alt="" src={ManWild} className="my-4 h-[440px] w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<span className="mb-2 text-2xl font-bold">{t('topProducts')}</span>
|
||||||
<Carousel />
|
<Carousel />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -57,10 +74,11 @@ async function ProductsByCategory() {
|
|||||||
|
|
||||||
async function LatestPosts() {
|
async function LatestPosts() {
|
||||||
const posts = await wordpress.get('posts?_embed');
|
const posts = await wordpress.get('posts?_embed');
|
||||||
|
const t = await getTranslations('HomePage');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-6 flex flex-col px-4">
|
<div className="my-6 flex flex-col px-4">
|
||||||
<span className="mb-2 text-2xl font-bold">Latest posts</span>
|
<span className="mb-2 text-2xl font-bold">{t('latestPosts')}</span>
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{posts.map((post: any) => (
|
{posts.map((post: any) => (
|
||||||
<div
|
<div
|
||||||
|
@ -11,6 +11,7 @@ import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
|||||||
import { Image } from 'lib/woocomerce/models/base';
|
import { Image } from 'lib/woocomerce/models/base';
|
||||||
import { Product, ProductVariations } from 'lib/woocomerce/models/product';
|
import { Product, ProductVariations } from 'lib/woocomerce/models/product';
|
||||||
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ export async function generateMetadata(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function RelatedProducts({ product }: { product: Product }) {
|
async function RelatedProducts({ product }: { product: Product }) {
|
||||||
|
const t = await getTranslations('ProductPage');
|
||||||
const relatedProducts = await Promise.all(
|
const relatedProducts = await Promise.all(
|
||||||
product.related_ids?.map(async (id) => woocommerce.get(`products/${id}`)) || []
|
product.related_ids?.map(async (id) => woocommerce.get(`products/${id}`)) || []
|
||||||
);
|
);
|
||||||
@ -49,7 +51,7 @@ async function RelatedProducts({ product }: { product: Product }) {
|
|||||||
<>
|
<>
|
||||||
{relatedProducts.length > 0 && (
|
{relatedProducts.length > 0 && (
|
||||||
<div className="mt-8 py-4">
|
<div className="mt-8 py-4">
|
||||||
<h3 className="text-2xl font-bold">Related Products</h3>
|
<h3 className="text-2xl font-bold">{t('relatedProducts')}</h3>
|
||||||
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{relatedProducts.map((relatedProduct) => {
|
{relatedProducts.map((relatedProduct) => {
|
||||||
return (
|
return (
|
||||||
@ -89,10 +91,6 @@ export default async function ProductPage(props: { params: Promise<{ name: strin
|
|||||||
|
|
||||||
if (!product) return notFound();
|
if (!product) return notFound();
|
||||||
|
|
||||||
const relatedProducts = await Promise.all(
|
|
||||||
product.related_ids?.map(async (id) => woocommerce.get(`products/${id}`)) || []
|
|
||||||
);
|
|
||||||
|
|
||||||
const productJsonLd = {
|
const productJsonLd = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'Product',
|
'@type': 'Product',
|
||||||
|
@ -2,16 +2,18 @@ import Price from 'components/price';
|
|||||||
import { authOptions } from 'lib/auth/config';
|
import { authOptions } from 'lib/auth/config';
|
||||||
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default async function OrdersPage() {
|
export default async function OrdersPage() {
|
||||||
|
const t = await getTranslations('ProfilePage');
|
||||||
const data = await getServerSession(authOptions);
|
const data = await getServerSession(authOptions);
|
||||||
const orders = await woocommerce.get('orders', { customer: data?.user?.store_id });
|
const orders = await woocommerce.get('orders', { customer: data?.user?.store_id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mt-4 grid w-full gap-4 px-4 pb-4">
|
<section className="mt-4 grid w-full gap-4 px-4 pb-4">
|
||||||
<h1 className="text-2xl font-bold">Orders</h1>
|
<h1 className="text-2xl font-bold">{t('orders')}</h1>
|
||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<div
|
<div
|
||||||
key={order.id}
|
key={order.id}
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
import { authOptions } from 'lib/auth/config';
|
import { authOptions } from 'lib/auth/config';
|
||||||
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
import { woocommerce } from 'lib/woocomerce/woocommerce';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
export default async function PersonalArea() {
|
export default async function PersonalArea() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
const t = await getTranslations('ProfilePage');
|
||||||
if (!session?.user?.store_id) {
|
if (!session?.user?.store_id) {
|
||||||
return { status: 401, body: { error: 'User not logged' } };
|
return { status: 401, body: { error: 'User not logged' } };
|
||||||
}
|
}
|
||||||
@ -14,7 +16,7 @@ export default async function PersonalArea() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
|
<section className="mt-4 grid max-w-screen-2xl gap-4 px-4 pb-4">
|
||||||
<h1 className="text-2xl font-bold">Personal Area</h1>
|
<h1 className="text-2xl font-bold">{t('area')}</h1>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
|
@ -5,10 +5,12 @@ import { Avatar } from '@nextui-org/react';
|
|||||||
import LogoutButton from 'components/button/logout';
|
import LogoutButton from 'components/button/logout';
|
||||||
import { Customer } from 'lib/woocomerce/models/customer';
|
import { Customer } from 'lib/woocomerce/models/customer';
|
||||||
import { Shipping } from 'lib/woocomerce/models/shipping';
|
import { Shipping } from 'lib/woocomerce/models/shipping';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function ProfileLayout({ user }: { user: React.ReactNode }) {
|
export default function ProfileLayout({ user }: { user: React.ReactNode }) {
|
||||||
|
const t = useTranslations('ProfilePage');
|
||||||
const [customer, setCustomer] = useState<Customer | undefined>(undefined);
|
const [customer, setCustomer] = useState<Customer | undefined>(undefined);
|
||||||
const [shippingAddress, setShippingAddress] = useState<Shipping | undefined>(undefined);
|
const [shippingAddress, setShippingAddress] = useState<Shipping | undefined>(undefined);
|
||||||
|
|
||||||
@ -35,14 +37,14 @@ export default function ProfileLayout({ user }: { user: React.ReactNode }) {
|
|||||||
<div>
|
<div>
|
||||||
<Avatar src={customer.avatar_url} alt="avatar" className="h-24 w-24" />
|
<Avatar src={customer.avatar_url} alt="avatar" className="h-24 w-24" />
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<span>Ciao </span>
|
<span> </span>
|
||||||
<span className="text-lg font-bold">{customer.first_name}</span>
|
<span className="text-lg font-bold">{customer.first_name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-start mt-2 flex">
|
<div className="flex-start mt-2 flex">
|
||||||
<Link href={`/profile`} className="hover:text-indigo-500">
|
<Link href={`/profile`} className="hover:text-indigo-500">
|
||||||
<button type="button" className="flex flex-row items-center rounded-md py-1">
|
<button type="button" className="flex flex-row items-center rounded-md py-1">
|
||||||
<UserCircleIcon className="me-2 h-4" />
|
<UserCircleIcon className="me-2 h-4" />
|
||||||
Personal area
|
{t('area')}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -50,7 +52,7 @@ export default function ProfileLayout({ user }: { user: React.ReactNode }) {
|
|||||||
<Link href={`/profile/orders`} className="hover:text-indigo-500">
|
<Link href={`/profile/orders`} className="hover:text-indigo-500">
|
||||||
<button type="button" className="flex flex-row items-center rounded-md py-1">
|
<button type="button" className="flex flex-row items-center rounded-md py-1">
|
||||||
<CubeIcon className="me-2 h-4" />
|
<CubeIcon className="me-2 h-4" />
|
||||||
Orders
|
{t('orders')}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -58,7 +60,7 @@ export default function ProfileLayout({ user }: { user: React.ReactNode }) {
|
|||||||
<Link href={`/profile/preferences`} className="hover:text-indigo-500">
|
<Link href={`/profile/preferences`} className="hover:text-indigo-500">
|
||||||
<button type="button" className="flex flex-row items-center rounded-md py-1">
|
<button type="button" className="flex flex-row items-center rounded-md py-1">
|
||||||
<Cog8ToothIcon className="me-2 h-4" />
|
<Cog8ToothIcon className="me-2 h-4" />
|
||||||
Preferences
|
{t('preferences')}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
BIN
assets/images/fiori.png
Normal file
BIN
assets/images/fiori.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 181 KiB |
BIN
assets/images/man-wild.png
Normal file
BIN
assets/images/man-wild.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 458 KiB |
@ -4,10 +4,12 @@ import { PlusIcon } from '@heroicons/react/24/outline';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useProduct } from 'components/product/product-context';
|
import { useProduct } from 'components/product/product-context';
|
||||||
import { Product, ProductVariations } from 'lib/woocomerce/models/product';
|
import { Product, ProductVariations } from 'lib/woocomerce/models/product';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useCart } from './cart-context';
|
import { useCart } from './cart-context';
|
||||||
|
|
||||||
function SubmitButton({ disabled = false }: { disabled: boolean }) {
|
function SubmitButton({ disabled = false }: { disabled: boolean }) {
|
||||||
|
const t = useTranslations('ProductPage');
|
||||||
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';
|
||||||
|
|
||||||
@ -20,7 +22,7 @@ function SubmitButton({ disabled = false }: { disabled: boolean }) {
|
|||||||
<div className="absolute left-0 ml-4">
|
<div className="absolute left-0 ml-4">
|
||||||
<PlusIcon className="h-5" />
|
<PlusIcon className="h-5" />
|
||||||
</div>
|
</div>
|
||||||
Add To Cart
|
{t('addToCart')}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { ShoppingCartIcon } from '@heroicons/react/24/outline';
|
|||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { Fragment, useEffect, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import { useFormStatus } from 'react-dom';
|
import { useFormStatus } from 'react-dom';
|
||||||
import { useCart } from './cart-context';
|
import { useCart } from './cart-context';
|
||||||
@ -13,6 +14,7 @@ import CloseCart from './close-cart';
|
|||||||
import OpenCart from './open-cart';
|
import OpenCart from './open-cart';
|
||||||
|
|
||||||
export default function CartModal() {
|
export default function CartModal() {
|
||||||
|
const t = useTranslations('Cart');
|
||||||
const { cart, setNewCart } = useCart();
|
const { cart, setNewCart } = useCart();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const openCart = () => setIsOpen(true);
|
const openCart = () => setIsOpen(true);
|
||||||
@ -65,7 +67,7 @@ export default function CartModal() {
|
|||||||
>
|
>
|
||||||
<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 dark:border-neutral-700 dark:bg-black/80 dark:text-white md:w-[390px]">
|
<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 dark:border-neutral-700 dark:bg-black/80 dark:text-white md:w-[390px]">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-lg font-semibold">My Cart</p>
|
<p className="text-lg font-semibold">{t('title')}</p>
|
||||||
<button aria-label="Close cart" onClick={closeCart}>
|
<button aria-label="Close cart" onClick={closeCart}>
|
||||||
<CloseCart />
|
<CloseCart />
|
||||||
</button>
|
</button>
|
||||||
@ -100,11 +102,11 @@ export default function CartModal() {
|
|||||||
</ul>
|
</ul>
|
||||||
<div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
||||||
<p>Shipping</p>
|
<p>{t('shipping')}</p>
|
||||||
<p className="text-right">Calculated at checkout</p>
|
<p className="text-right">Calculated at checkout</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
||||||
<p>Total</p>
|
<p>{t('total')}</p>
|
||||||
<Price
|
<Price
|
||||||
className="text-right text-base text-black dark:text-white"
|
className="text-right text-base text-black dark:text-white"
|
||||||
amount={cart.totals?.total_price}
|
amount={cart.totals?.total_price}
|
||||||
@ -127,6 +129,7 @@ export default function CartModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CheckoutButton() {
|
function CheckoutButton() {
|
||||||
|
const t = useTranslations('Cart');
|
||||||
const { pending } = useFormStatus();
|
const { pending } = useFormStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -135,7 +138,7 @@ function CheckoutButton() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
>
|
>
|
||||||
{pending ? <LoadingDots className="bg-white" /> : 'Proceed to Checkout'}
|
{pending ? <LoadingDots className="bg-white" /> : t('checkout')}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
|||||||
import { useProduct, useUpdateURL } from 'components/product/product-context';
|
import { useProduct, useUpdateURL } from 'components/product/product-context';
|
||||||
import { Attribute } from 'lib/woocomerce/models/base';
|
import { Attribute } from 'lib/woocomerce/models/base';
|
||||||
import { ProductVariations } from 'lib/woocomerce/models/product';
|
import { ProductVariations } from 'lib/woocomerce/models/product';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
type FilterVariation = {
|
type FilterVariation = {
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
@ -17,6 +18,7 @@ export function VariantSelector({
|
|||||||
options: Partial<Attribute>[];
|
options: Partial<Attribute>[];
|
||||||
variations: ProductVariations[];
|
variations: ProductVariations[];
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations('ProductPage.variants');
|
||||||
const { state, updateOption } = useProduct();
|
const { state, updateOption } = useProduct();
|
||||||
const updateURL = useUpdateURL();
|
const updateURL = useUpdateURL();
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ export function VariantSelector({
|
|||||||
return combinations.map((option) => (
|
return combinations.map((option) => (
|
||||||
<form key={option.name}>
|
<form key={option.name}>
|
||||||
<dl className="mb-8">
|
<dl className="mb-8">
|
||||||
<dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt>
|
<dt className="mb-4 text-sm uppercase tracking-wide">{t(option.name as any)}</dt>
|
||||||
<dd className="flex flex-wrap gap-3">
|
<dd className="flex flex-wrap gap-3">
|
||||||
{option?.values?.map((value) => {
|
{option?.values?.map((value) => {
|
||||||
const optionNameLowerCase = option?.name?.toLowerCase();
|
const optionNameLowerCase = option?.name?.toLowerCase();
|
||||||
|
8
global.d.ts
vendored
Normal file
8
global.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import it from './messages/it.json';
|
||||||
|
|
||||||
|
type Messages = typeof it;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// Use type safe message keys with `next-intl`
|
||||||
|
interface IntlMessages extends Messages {}
|
||||||
|
}
|
11
i18n/request.ts
Normal file
11
i18n/request.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { getRequestConfig } from 'next-intl/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
|
export default getRequestConfig(async () => {
|
||||||
|
const locale = ((await cookies()).get('locale')?.value as string) || 'it';
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`../messages/${locale}.json`)).default
|
||||||
|
};
|
||||||
|
});
|
42
messages/en.json
Normal file
42
messages/en.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"helpIA": "Buy with IA help",
|
||||||
|
"topProducts": "Top products",
|
||||||
|
"latestPosts": "Latest posts"
|
||||||
|
},
|
||||||
|
"ProductPage": {
|
||||||
|
"buy": "Buy",
|
||||||
|
"addToCart": "Add to cart",
|
||||||
|
"description": "Description",
|
||||||
|
"specifications": "Specifications",
|
||||||
|
"reviews": "Reviews",
|
||||||
|
"variants": {
|
||||||
|
"color": "Color",
|
||||||
|
"size": "Size"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ProfilePage": {
|
||||||
|
"area": "Personal Area",
|
||||||
|
"orders": "Orders",
|
||||||
|
"preferences": "Preferences",
|
||||||
|
"user": {
|
||||||
|
"hi": "Hi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Cart": {
|
||||||
|
"title": "Cart",
|
||||||
|
"empty": "Cart is empty",
|
||||||
|
"total": "Total",
|
||||||
|
"shipping": "Shipping",
|
||||||
|
"checkout": "Checkout"
|
||||||
|
},
|
||||||
|
"Checkout": {
|
||||||
|
"title": "Checkout",
|
||||||
|
"shipping": "Shipping",
|
||||||
|
"billing": "Billing",
|
||||||
|
"billingCheckbox": "Do you need an invoice?",
|
||||||
|
"payment": "Payment",
|
||||||
|
"confirm": "Confirm order",
|
||||||
|
"total": "Total"
|
||||||
|
}
|
||||||
|
}
|
43
messages/it.json
Normal file
43
messages/it.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"helpIA": "Guida all'acquisto con IA",
|
||||||
|
"topProducts": "Prodotti più venduti",
|
||||||
|
"latestPosts": "Ultimi articoli"
|
||||||
|
},
|
||||||
|
"ProductPage": {
|
||||||
|
"buy": "Acquista",
|
||||||
|
"addToCart": "Aggiungi al carrello",
|
||||||
|
"description": "Descrizione",
|
||||||
|
"specifications": "Specifiche",
|
||||||
|
"reviews": "Recensioni",
|
||||||
|
"relatedProducts": "Prodotti correlati",
|
||||||
|
"variants": {
|
||||||
|
"color": "Colore",
|
||||||
|
"size": "Dimensione"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ProfilePage": {
|
||||||
|
"area": "Area personale",
|
||||||
|
"orders": "Ordini",
|
||||||
|
"preferences": "Preferenze",
|
||||||
|
"user": {
|
||||||
|
"hi": "Ciao"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Cart": {
|
||||||
|
"title": "Carrello",
|
||||||
|
"empty": "Il carrello è vuoto",
|
||||||
|
"total": "Totale",
|
||||||
|
"shipping": "Spedizione",
|
||||||
|
"checkout": "Procedi all'acquisto"
|
||||||
|
},
|
||||||
|
"Checkout": {
|
||||||
|
"title": "Checkout",
|
||||||
|
"shipping": "Spedizione",
|
||||||
|
"billing": "Fatturazione",
|
||||||
|
"billingCheckbox": "Hai bisogno di una fattura?",
|
||||||
|
"payment": "Pagamento",
|
||||||
|
"confirm": "Conferma ordine",
|
||||||
|
"total": "Totale"
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,15 @@
|
|||||||
export default {
|
import { NextConfig } from 'next';
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
images: {
|
images: {
|
||||||
|
localPatterns: [
|
||||||
|
{
|
||||||
|
pathname: '/assets/images/**'
|
||||||
|
}
|
||||||
|
],
|
||||||
formats: ['image/avif', 'image/webp'],
|
formats: ['image/avif', 'image/webp'],
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
@ -9,3 +19,5 @@ export default {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default withNextIntl(nextConfig);
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"next": "15.0.4",
|
"next": "15.0.4",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
|
"next-intl": "^3.26.3",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
|
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@ -31,6 +31,9 @@ importers:
|
|||||||
next-auth:
|
next-auth:
|
||||||
specifier: ^4.24.11
|
specifier: ^4.24.11
|
||||||
version: 4.24.11(next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 4.24.11(next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
next-intl:
|
||||||
|
specifier: ^3.26.3
|
||||||
|
version: 3.26.3(next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||||
react:
|
react:
|
||||||
specifier: 19.0.0
|
specifier: 19.0.0
|
||||||
version: 19.0.0
|
version: 19.0.0
|
||||||
@ -2636,6 +2639,13 @@ packages:
|
|||||||
engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 }
|
engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 }
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
negotiator@1.0.0:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
|
||||||
|
}
|
||||||
|
engines: { node: '>= 0.6' }
|
||||||
|
|
||||||
next-auth@4.24.11:
|
next-auth@4.24.11:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -2653,6 +2663,15 @@ packages:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
next-intl@3.26.3:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-6Y97ODrDsEE1J8cXKMHwg1laLdtkN66QMIqG8BzH4zennJRUNTtM8UMtBDyhfmF6uiZ+xsbWLXmHUgmUymUsfQ==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
|
||||||
next@15.0.4:
|
next@15.0.4:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -3289,6 +3308,14 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
use-intl@3.26.3:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-yY0a2YseO17cKwHA9M6fcpiEJ2Uo81DEU0NOUxNTp6lJVNOuI6nULANPVVht6IFdrYFtlsMmMoc97+Eq9/Tnng==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
|
||||||
use-isomorphic-layout-effect@1.2.0:
|
use-isomorphic-layout-effect@1.2.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -5703,6 +5730,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.8: {}
|
nanoid@3.3.8: {}
|
||||||
|
|
||||||
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
next-auth@4.24.11(next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
next-auth@4.24.11(next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.0
|
'@babel/runtime': 7.26.0
|
||||||
@ -5718,6 +5747,14 @@ snapshots:
|
|||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
|
|
||||||
|
next-intl@3.26.3(next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/intl-localematcher': 0.5.9
|
||||||
|
negotiator: 1.0.0
|
||||||
|
next: 15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
react: 19.0.0
|
||||||
|
use-intl: 3.26.3(react@19.0.0)
|
||||||
|
|
||||||
next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
next@15.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 15.0.4
|
'@next/env': 15.0.4
|
||||||
@ -6065,6 +6102,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.0
|
'@types/react': 19.0.0
|
||||||
|
|
||||||
|
use-intl@3.26.3(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/fast-memoize': 2.2.5
|
||||||
|
intl-messageformat: 10.7.10
|
||||||
|
react: 19.0.0
|
||||||
|
|
||||||
use-isomorphic-layout-effect@1.2.0(@types/react@19.0.0)(react@19.0.0):
|
use-isomorphic-layout-effect@1.2.0(@types/react@19.0.0)(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@": ["/"] // enables us to use @components/MyComponent
|
"@": ["/"]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user