Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions app/(shop)/[...category]/filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,6 @@ export function Filters({ priceRange, sorting, paths, stockOptions, totalHits }:
<MenuItems
transition
className="absolute left-0 z-10 mt-2 w-40 origin-top-left rounded-md bg-light shadow-2xl ring-1 ring-dark/5 transition focus:outline-hidden data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
onChange={(e) => {
console.log(e);
}}
>
<div className="py-1">
{sortOptions.map((option) => (
Expand Down
59 changes: 32 additions & 27 deletions app/(shop)/account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getSession } from '@/core/auth.server';
import { getSession, logout } from '@/core/auth.server';
import LoginForm from '@/components/login-form';
import { createOrderFetcher, type Order } from '@crystallize/js-api-client';
import { crystallizeClient } from '@/core/crystallize-client.server';
import { Price } from '@/components/price';
import { logout } from '@/core/auth.server';
import { Image } from '@/components/image';
import { pipeline } from 'stream';

const formatDate = (incomingDate: string) => {
const date = new Date(incomingDate);
Expand All @@ -17,6 +17,31 @@ const formatDate = (incomingDate: string) => {
});
};

const CUSTOMER_QUERY = `#graphql
query GetCustomer($identifier: String!) {
customer(identifier: $identifier) {
... on Customer {
identifier
firstName
lastName
email
type
addresses {
city
country
postalCode
state
street
type
}
parents {
identifier
type
}
}
}
}`;

type OrdersPageProps = { searchParams: Promise<{ error?: string }> };

export default async function AccountPage(props: OrdersPageProps) {
Expand All @@ -31,30 +56,9 @@ export default async function AccountPage(props: OrdersPageProps) {
);
}

const customer = await crystallizeClient.nextPimApi(
`#graphql
query GetCustomer($identifier: String!) {
customer(identifier: $identifier) {
... on Customer {
identifier
firstName
lastName
email
addresses {
city
country
postalCode
state
street
type
}
}
}
}`,
{
identifier: session.user.email,
},
);
const customer = await crystallizeClient.nextPimApi(CUSTOMER_QUERY, {
identifier: session.user.email,
});

const orders = await createOrderFetcher(crystallizeClient).byCustomerIdentifier(
session.user.email,
Expand Down Expand Up @@ -85,6 +89,7 @@ export default async function AccountPage(props: OrdersPageProps) {
<h1 className="font-medium">Orders</h1>
{orders?.orders.map((item) => {
const order = item as Order & { reference: string; createdAt: string };
console.log('order', order);
return (
<details
key={order.id}
Expand Down Expand Up @@ -185,7 +190,7 @@ export default async function AccountPage(props: OrdersPageProps) {
type="submit"
className="px-6 py-2 mt-4 font-medium float-right rounded-lg bg-dark text-light hover:bg-dark/90 active:bg-dark/95"
>
Logut
Logout
</button>
</form>
</div>
Expand Down
1 change: 1 addition & 0 deletions app/(shop)/order/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Image } from '@/components/image';
import { Price } from '@/components/price';
import { crystallizeClient } from '@/core/crystallize-client.server';
import { createOrderFetcher, type Order } from '@crystallize/js-api-client';
import { pipeline } from 'stream';

const fetchData = async (orderId: string) => {
const response = await createOrderFetcher(crystallizeClient).byId(
Expand Down
8 changes: 4 additions & 4 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
@theme {
--color-*: initial;
--color-transparent: transparent;
--color-dark: #211a1d;
--color-dark: #0a2e3c;
--color-light: #ffffff;
--color-soft: #f6f4f3;
--color-accent: #79745d;
--color-muted: #dfdfdf;
--color-soft: #ececec;
--color-accent: #0091d3;
--color-muted: #ececec;
--color-vivid: #ee7674;
}

Expand Down
40 changes: 40 additions & 0 deletions assets/roxtec_logo_black_blue_rgb_fixed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 75 additions & 12 deletions components/ProductPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,29 @@ import { AddToCartButton } from '@/components/cart/add-to-cart-button';
import { ParagraphCollection } from '@/components/paragraph-collection';
import { SearchParams } from '@/app/(shop)/[...category]/types';
import downloadIcon from '@/assets/icon-download.svg';
import { getSession } from '@/core/auth.server';

import Image from 'next/image';
import classNames from 'classnames';
import { getPrice } from '@/utils/price';

// TODO: get these from the environment variables
const { CRYSTALLIZE_BASE_PRICE, CRYSTALLIZE_SELECTED_PRICE, CRYSTALLIZE_MARKETS_PRICE } = process.env;

type ProductsProps = {
searchParams: Promise<SearchParams>;
params: Promise<{ slug: string; category: string[] }>;
};

export const fetchProductData = async ({ path, isPreview = false }: { path: string; isPreview?: boolean }) => {
const session = await getSession();

const response = await apiRequest(FetchProductDocument, {
path,
publicationState: isPreview ? PublicationState.Draft : PublicationState.Published,
selectedPrice: CRYSTALLIZE_SELECTED_PRICE!,
basePrice: CRYSTALLIZE_BASE_PRICE!,
marketIdentifiers: session?.markets,
});
const { story, variants, brand, breadcrumbs, meta, ...product } = response.data.browse?.product?.hits?.[0] ?? {};

Expand All @@ -47,6 +58,11 @@ export default async function CategoryProduct(props: ProductsProps) {
const url = `/${params.category.join('/')}`;
const product = await fetchProductData({ path: url, isPreview: !!searchParams.preview });
const currentVariant = findSuitableVariant({ variants: product.variants, searchParams });
const currentVariantPrice = getPrice({
base: currentVariant?.basePrice,
selected: currentVariant?.selectedPrice,
market: currentVariant?.marketPrice ?? null,
});
const dimensions = currentVariant?.dimensions;
// TODO: this should be for how long the price will be valid
const TWO_DAYS_FROM_NOW = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000);
Expand All @@ -65,8 +81,8 @@ export default async function CategoryProduct(props: ProductsProps) {
'@type': 'Offer',
itemCondition: 'https://schema.org/NewCondition',
availability: 'https://schema.org/InStock',
price: variant?.defaultPrice.price ?? '',
priceCurrency: variant?.defaultPrice.currency ?? '',
price: currentVariantPrice.lowest ?? 0,
priceCurrency: currentVariantPrice.currency ?? 'EUR',
priceValidUntil: TWO_DAYS_FROM_NOW.toLocaleString(),
},
})) ?? [];
Expand Down Expand Up @@ -269,9 +285,30 @@ export default async function CategoryProduct(props: ProductsProps) {
</div>
)}
<div className="text-4xl flex items-center font-bold py-4 justify-between w-full">
<span>
<Price price={currentVariant?.priceVariants.default} />
</span>
<div className="flex flex-col">
{/* Lowest price */}
<span>
<Price
price={{
price: currentVariantPrice.lowest,
currency: currentVariantPrice.currency,
}}
/>
</span>
{/* Compared at price */}
<span
className={classNames('block text-sm line-through font-normal', {
hidden: !currentVariantPrice.hasBestPrice,
})}
>
<Price
price={{
price: currentVariantPrice.highest,
currency: currentVariantPrice.currency,
}}
/>
</span>
</div>
{!!currentVariant && !!currentVariant.sku && (
<AddToCartButton
input={{
Expand All @@ -283,9 +320,9 @@ export default async function CategoryProduct(props: ProductsProps) {
currentVariant?.images?.[0],
quantity: 1,
price: {
currency: currentVariant.defaultPrice?.currency || 'EUR',
gross: currentVariant.defaultPrice?.price || 0,
net: currentVariant.defaultPrice?.price || 0,
currency: currentVariantPrice.currency,
gross: currentVariantPrice.lowest,
net: currentVariantPrice.lowest,
taxAmount: 0,
discounts: [],
},
Expand All @@ -302,6 +339,15 @@ export default async function CategoryProduct(props: ProductsProps) {
})`}
>
{currentVariant?.matchingProducts?.variants?.map((product, index) => {
if (!product) {
return null;
}

const matchingProductPrice = getPrice({
base: product.basePrice,
selected: product.selectedPrice,
market: product.marketPrice,
});
return (
<div
key={`${product?.sku}-featured-${index}`}
Expand All @@ -316,7 +362,24 @@ export default async function CategoryProduct(props: ProductsProps) {
<Link href={product.product.path}>{product?.name}</Link>
)}
<span className="text-sm font-bold">
<Price price={product?.defaultPrice} />
<Price
price={{
price: matchingProductPrice.lowest,
currency: matchingProductPrice.currency,
}}
/>
</span>
<span
className={classNames('text-xs line-through', {
hidden: !matchingProductPrice.hasBestPrice,
})}
>
<Price
price={{
price: matchingProductPrice.highest,
currency: matchingProductPrice.currency,
}}
/>
</span>
</div>
</div>
Expand All @@ -331,9 +394,9 @@ export default async function CategoryProduct(props: ProductsProps) {
image: product.firstImage?.variants?.[0],
quantity: 1,
price: {
currency: product.defaultPrice?.currency || 'EUR',
gross: product.defaultPrice?.price || 0,
net: currentVariant.defaultPrice.price || 0,
currency: matchingProductPrice.currency,
gross: matchingProductPrice.lowest,
net: matchingProductPrice.lowest,
taxAmount: 0,
discounts: [],
},
Expand Down
10 changes: 7 additions & 3 deletions components/ProductPage/query.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
query FetchProduct($path: String!, $publicationState: [PublicationState]) {
query FetchProduct($path: String!, $publicationState: [PublicationState], $basePrice: String!, $selectedPrice: String!, $marketIdentifiers: [String!]) {
browse {
product(path: $path, publicationState: $publicationState) {
hits {
Expand Down Expand Up @@ -94,7 +94,9 @@ fragment productVariant on ProductVariantForProduct {
sku
priceVariants
attributes
defaultPrice: priceVariants(key: "default")
basePrice: priceVariants(key: $basePrice)
selectedPrice: priceVariants(key: $selectedPrice)
marketPrice: b2bPriceFor(marketIdentifiers: $marketIdentifiers)
description {
extraDescription
}
Expand All @@ -105,7 +107,9 @@ fragment productVariant on ProductVariantForProduct {
product {
path
}
defaultPrice: priceVariants(key: "default")
basePrice: priceVariants(key: $basePrice)
selectedPrice: priceVariants(key: $selectedPrice)
marketPrice: b2bPriceFor(marketIdentifiers: $marketIdentifiers)
firstImage {
url
variants(maxWidth: 200, types: ["avif"]) {
Expand Down
8 changes: 6 additions & 2 deletions components/cart/cart-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,15 @@ export const CartItems = () => {
{item.price.discounts?.length > 0 && (
<>
<s className="text-sm text-dark/60">
<Price price={{ price: item.variant.price.gross}} />
<Price price={{ price: item.variant.compareAtPrice.gross }} />
</s>
<br />
<Badge className={'text-xs mr-2'}>
-{Math.round((item.price.discounts?.[0].percent + Number.EPSILON) * 100) / 100}%
-
{Math.round(
(item.price.discounts?.[0].percent + Number.EPSILON) * 100,
) / 100}
%
</Badge>
</>
)}
Expand Down
Loading