chore: transfer repo

This commit is contained in:
Danijel
2026-01-19 20:21:14 +01:00
commit 7d2fb0c737
213 changed files with 18085 additions and 0 deletions

12
app/[page]/layout.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { Footer } from 'components/layout/footer';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<div className="w-full">
<div className="mx-8 max-w-2xl py-20 sm:mx-auto">{children}</div>
</div>
<Footer />
</>
);
}

View File

@@ -0,0 +1,11 @@
import { getPage } from 'lib/shopify';
import OpengraphImage from '../../components/opengraph-image';
export const runtime = 'edge';
export default async function Image({ params }: { params: { page: string } }) {
const page = await getPage(params.page);
const title = page.seo?.title || page.title;
return await OpengraphImage({ title });
}

45
app/[page]/page.tsx Normal file
View File

@@ -0,0 +1,45 @@
import type { Metadata } from 'next';
import Prose from 'components/prose';
import { getPage } from 'lib/shopify';
import { notFound } from 'next/navigation';
export async function generateMetadata(props: {
params: Promise<{ page: string }>;
}): Promise<Metadata> {
const params = await props.params;
const page = await getPage(params.page);
if (!page) return notFound();
return {
title: page.seo?.title || page.title,
description: page.seo?.description || page.bodySummary,
openGraph: {
publishedTime: page.createdAt,
modifiedTime: page.updatedAt,
type: 'article'
}
};
}
export default async function Page(props: { params: Promise<{ page: string }> }) {
const params = await props.params;
const page = await getPage(params.page);
if (!page) return notFound();
return (
<>
<h1 className="mb-8">{page.title}</h1>
<Prose className="mb-8" html={page.body} />
<p className="text-sm italic">
{`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date(page.updatedAt))}.`}
</p>
</>
);
}

11
app/about/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { AboutPageContent } from '@/components/about/AboutPageContent';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'About Us | Sent',
description: 'Learn more about Sent and our mission to create personalized gift boxes for every occasion.'
};
export default function AboutPage() {
return <AboutPageContent />;
}

21
app/api/products/route.ts Normal file
View File

@@ -0,0 +1,21 @@
import { getBoxProducts, getProducts } from 'lib/shopify';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
try {
// Get query parameters
const { searchParams } = new URL(request.url);
const type = searchParams.get('type');
if (type === 'boxes') {
const boxProducts = await getBoxProducts({});
return NextResponse.json({ products: boxProducts });
} else {
const allProducts = await getProducts({});
return NextResponse.json({ products: allProducts });
}
} catch (error) {
console.error('Error fetching products:', error);
return NextResponse.json({ error: 'Failed to fetch products' }, { status: 500 });
}
}

View File

@@ -0,0 +1,6 @@
import { revalidate } from 'lib/shopify';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest): Promise<NextResponse> {
return revalidate(req);
}

View File

@@ -0,0 +1,10 @@
import { BuildBoxCustomizePage } from "@/components/build-box/BuildBoxCustomizePage";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Choose Your Box | Sent",
description: "Select the perfect box design to complete your custom gift box."
};
export default function BuildBoxCustomizeRoute() {
return <BuildBoxCustomizePage />;
}

11
app/build-box/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { BuildBoxPage } from "@/components/build-box/BuildBoxPage";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Build Your Box | Sent",
description: "Create your custom gift box by selecting products you'd like to include."
};
export default function BuildBoxRoute() {
return <BuildBoxPage />;
}

10
app/cart/page.tsx Normal file
View File

@@ -0,0 +1,10 @@
import CartPage from 'components/cart/CartPage';
export const metadata = {
title: 'Cart',
description: 'View your shopping cart and proceed to checkout.',
};
export default function Cart() {
return <CartPage />;
}

19
app/error.tsx Normal file
View File

@@ -0,0 +1,19 @@
'use client';
export default function Error({ reset }: { reset: () => void }) {
return (
<div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
<h2 className="text-xl font-bold">Oh no!</h2>
<p className="my-2">
There was an issue with our storefront. This could be a temporary issue, please try your
action again.
</p>
<button
className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
onClick={() => reset()}
>
Try Again
</button>
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

28
app/fonts.css Normal file
View File

@@ -0,0 +1,28 @@
/* AlleNoire fonts */
/* Regular */
@font-face {
font-family: 'AlleNoire';
src: url('/fonts/allenoire-allenoire-regular-400.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* Poppins fonts */
/* Regular */
@font-face {
font-family: 'Poppins Regular';
src: url('/fonts/Poppins-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* SemiBold */
@font-face {
font-family: 'Poppins SemiBold';
src: url('/fonts/Poppins-SemiBold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}

153
app/globals.css Normal file
View File

@@ -0,0 +1,153 @@
@import './fonts.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom animation for fade-in effect */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Add custom styles below */
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Hide scrollbar but keep functionality */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
html {
font-family: 'AlleNoire', system-ui, sans-serif;
}
body {
@apply bg-background text-foreground;
font-family: 'AlleNoire', system-ui, sans-serif;
}
/* Font weight utility classes */
.font-extrabold {
font-family: 'AlleNoire', system-ui, sans-serif;
font-weight: 800;
}
.font-bold {
font-family: 'AlleNoire', system-ui, sans-serif;
font-weight: 700;
}
.font-semibold {
font-family: 'AlleNoire', system-ui, sans-serif;
font-weight: 600;
}
/* Set letter-spacing to 0 for all text elements */
h1, h2, h3, h4, h5, h6, p, span, div {
letter-spacing: 0;
}
}

92
app/layout.tsx Normal file
View File

@@ -0,0 +1,92 @@
import { CookieConsent } from '@/components/cookies';
import { Footer } from '@/components/layout/footer';
import Navbar from '@/components/layout/navbar';
import { ReduxProvider } from '@/lib/redux/provider';
import { cn } from '@/lib/utils';
import { CartProvider } from 'components/cart/cart-context';
import { getCart } from 'lib/shopify';
import { ensureStartsWith } from 'lib/utils';
import type { Metadata } from 'next';
import { cookies } from 'next/headers';
import { ReactNode } from 'react';
import { Toaster } from 'sonner';
import './globals.css';
// Add font loading check
if (typeof window !== 'undefined') {
document.fonts.ready.then(() => {
console.log('Fonts have loaded.');
document.fonts.forEach(font => {
console.log(`Font family: ${font.family}, Status: ${font.status}`);
});
});
}
const { TWITTER_CREATOR, TWITTER_SITE, SITE_NAME } = process.env;
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: 'http://localhost:3000';
const twitterCreator = TWITTER_CREATOR ? ensureStartsWith(TWITTER_CREATOR, '@') : undefined;
const twitterSite = TWITTER_SITE ? ensureStartsWith(TWITTER_SITE, 'https://') : undefined;
export const metadata: Metadata = {
metadataBase: new URL(baseUrl),
title: {
default: SITE_NAME!,
template: `%s | ${SITE_NAME}`
},
robots: {
follow: true,
index: true
},
...(twitterCreator &&
twitterSite && {
twitter: {
card: 'summary_large_image',
creator: twitterCreator,
site: twitterSite
}
})
};
export default async function RootLayout({ children }: { children: ReactNode }) {
const allCookies = await cookies();
const cartId = allCookies.get('cartId')?.value;
// Don't await the fetch, pass the Promise to the context provider
const cart = getCart(cartId);
// Get locale from cookie
const localeCookie = allCookies.get('NEXT_LOCALE')?.value;
const locale = localeCookie || 'hr';
return (
<html lang={locale}>
<head>
<meta name="theme-color" content="#ffffff" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"
integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g=="
crossOrigin="anonymous"
referrerPolicy="no-referrer"
/>
</head>
<body className={cn("min-h-screen bg-white antialiased")}>
<ReduxProvider>
<CartProvider cartPromise={cart}>
<CookieConsent>
<div className="flex flex-col min-h-screen">
<Navbar />
<main className="flex-grow">
{children}
<Toaster closeButton />
</main>
<Footer />
</div>
</CookieConsent>
</CartProvider>
</ReduxProvider>
</body>
</html>
);
}

8
app/opengraph-image.tsx Normal file
View File

@@ -0,0 +1,8 @@
import OpengraphImage from '../components/opengraph-image';
export const runtime = 'edge';
export default async function Image() {
// Default implementation for root page
return await OpengraphImage({ title: 'Sent - Personalizirani poklon paketi' });
}

20
app/page.tsx Normal file
View File

@@ -0,0 +1,20 @@
import NewHomePage from "@/components/home/NewHomePage";
// Statički metadata
export const metadata = {
title: 'Sent | Poklon paketi za svaku priliku',
description: 'Otkrij čar darivanja uz našu opciju "Složi kutiju". Biraj između pažljivo odabranih proizvoda i složi poklon koji će osvojiti na prvu.',
openGraph: {
type: 'website',
title: 'Sent | Poklon paketi za svaku priliku',
description: 'Otkrij čar darivanja uz našu opciju "Složi kutiju". Biraj između pažljivo odabranih proizvoda i složi poklon koji će osvojiti na prvu.'
}
};
export default function HomePage() {
return (
<>
<NewHomePage />
</>
);
}

View File

@@ -0,0 +1,64 @@
import { Heading, Text } from "@/components/ui/Typography";
import { container } from "@/lib/utils";
export default function PrivacyPolicyPage() {
return (
<div className={`${container} py-12`}>
<Heading level={1} className="mb-8 text-center">Privacy Policy</Heading>
<div className="max-w-3xl mx-auto">
<Text className="mb-6">
Last updated: {new Date().toLocaleDateString()}
</Text>
<Heading level={2} className="mb-4 mt-8">1. Introduction</Heading>
<Text className="mb-4">
Welcome to SENT. We respect your privacy and are committed to protecting your personal data.
This privacy policy will inform you about how we look after your personal data when you visit our website
and tell you about your privacy rights and how the law protects you.
</Text>
<Heading level={2} className="mb-4 mt-8">2. The Data We Collect About You</Heading>
<Text className="mb-4">
Personal data, or personal information, means any information about an individual from which that person can be identified.
It does not include data where the identity has been removed (anonymous data).
</Text>
<Text className="mb-4">
We may collect, use, store and transfer different kinds of personal data about you which we have grouped together as follows:
</Text>
<ul className="list-disc pl-6 mb-6 space-y-2">
<li>Identity Data includes first name, last name, username or similar identifier.</li>
<li>Contact Data includes billing address, delivery address, email address and telephone numbers.</li>
<li>Financial Data includes payment card details.</li>
<li>Transaction Data includes details about payments to and from you and other details of products you have purchased from us.</li>
<li>Technical Data includes internet protocol (IP) address, browser type and version, time zone setting and location, browser plug-in types and versions, operating system and platform, and other technology on the devices you use to access this website.</li>
</ul>
<Heading level={2} className="mb-4 mt-8">3. How We Use Your Personal Data</Heading>
<Text className="mb-4">
We will only use your personal data when the law allows us to. Most commonly, we will use your personal data in the following circumstances:
</Text>
<ul className="list-disc pl-6 mb-6 space-y-2">
<li>Where we need to perform the contract we are about to enter into or have entered into with you.</li>
<li>Where it is necessary for our legitimate interests (or those of a third party) and your interests and fundamental rights do not override those interests.</li>
<li>Where we need to comply with a legal obligation.</li>
</ul>
<Heading level={2} className="mb-4 mt-8">4. Data Security</Heading>
<Text className="mb-4">
We have put in place appropriate security measures to prevent your personal data from being accidentally lost, used or accessed in an unauthorized way, altered or disclosed.
In addition, we limit access to your personal data to those employees, agents, contractors and other third parties who have a business need to know.
They will only process your personal data on our instructions and they are subject to a duty of confidentiality.
</Text>
<Heading level={2} className="mb-4 mt-8">5. Contact Us</Heading>
<Text className="mb-6">
If you have any questions about this privacy policy or our privacy practices, please contact us at:
</Text>
<Text className="font-medium mb-1">Email: info@sentshop.com</Text>
<Text className="font-medium mb-1">Phone: +385 1 234 5678</Text>
<Text className="font-medium mb-6">Address: Ilica 123, 10000 Zagreb, Croatia</Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,122 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { BackButton } from '@/components/product/BackButton';
import { ProductDescription } from '@/components/product/ProductDescription';
import { container } from '@/lib/utils';
import { ProductProvider } from 'components/product/product-context';
import { ProductDetailsSection } from 'components/product/ProductDetailsSection';
import { ProductGallery } from 'components/product/ProductGallery';
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
import { getProduct } from 'lib/shopify';
import { Suspense } from 'react';
export async function generateMetadata(props: {
params: Promise<{ handle: string }>;
}): Promise<Metadata> {
const params = await props.params;
const product = await getProduct(params.handle);
if (!product) return notFound();
const { url, width, height, altText: alt } = product.featuredImage || {};
const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
return {
title: product.seo.title || product.title,
description: product.seo.description || product.description,
robots: {
index: indexable,
follow: indexable,
googleBot: {
index: indexable,
follow: indexable
}
},
openGraph: url
? {
images: [
{
url,
width,
height,
alt
}
]
}
: null
};
}
export default async function ProductPage(props: { params: Promise<{ handle: string }> }) {
const params = await props.params;
const product = await getProduct(params.handle);
if (!product) return notFound();
const productJsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.title,
description: product.description,
image: product.featuredImage.url,
offers: {
'@type': 'AggregateOffer',
availability: product.availableForSale
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
priceCurrency: product.priceRange.minVariantPrice.currencyCode,
highPrice: product.priceRange.maxVariantPrice.amount,
lowPrice: product.priceRange.minVariantPrice.amount
}
};
// If product has only one image, duplicate it to create the gallery effect
const galleryImages = product.images.length === 1
? Array(5).fill(product.images[0]).map(image => ({
url: image.url,
altText: image.altText,
width: image.width,
height: image.height
}))
: product.images;
return (
<ProductProvider>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(productJsonLd)
}}
/>
<div className="py-4">
<div className={container}>
<BackButton />
</div>
</div>
<div className={container}>
<div className="flex flex-col md:flex-row lg:gap-8">
<div className="w-full lg:w-3/5">
<Suspense
fallback={
<div className="relative aspect-square h-full max-h-[600px] w-full overflow-hidden" />
}
>
<ProductGallery
images={galleryImages.slice(0, 5)}
/>
</Suspense>
</div>
<div className="w-full lg:w-2/5 mt-8 lg:mt-0">
<Suspense fallback={null}>
<ProductDescription product={product} />
<ProductDetailsSection />
</Suspense>
</div>
</div>
</div>
</ProductProvider>
);
}

11
app/products/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { ProductsPage } from "@/components/products/ProductsPage";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Products | Sent",
description: "Explore our collection of ready-made gift boxes and baskets."
};
export default function ProductsRoute() {
return <ProductsPage />;
}

15
app/robots.ts Normal file
View File

@@ -0,0 +1,15 @@
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: 'http://localhost:3000';
export default function robots() {
return {
rules: [
{
userAgent: '*'
}
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl
};
}

View File

@@ -0,0 +1,11 @@
import { getCollection } from 'lib/shopify';
import OpengraphImage from '../../../components/opengraph-image';
export const runtime = 'edge';
export default async function Image({ params }: { params: { collection: string } }) {
const collection = await getCollection(params.collection);
const title = collection?.seo?.title || collection?.title;
return await OpengraphImage({ title });
}

View File

@@ -0,0 +1,45 @@
import { getCollection, getCollectionProducts } from 'lib/shopify';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import ProductGridItems from '@/components/layout/ProductGridItems';
import Grid from 'components/grid';
import { defaultSort, sorting } from 'lib/constants';
export async function generateMetadata(props: {
params: Promise<{ collection: string }>;
}): Promise<Metadata> {
const params = await props.params;
const collection = await getCollection(params.collection);
if (!collection) return notFound();
return {
title: collection.seo?.title || collection.title,
description:
collection.seo?.description || collection.description || `${collection.title} products`
};
}
export default async function CategoryPage(props: {
params: Promise<{ collection: string }>;
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const params = await props.params;
const { sort } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
return (
<section>
{products.length === 0 ? (
<p className="py-3 text-lg">{`No products found in this collection`}</p>
) : (
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<ProductGridItems products={products} />
</Grid>
)}
</section>
);
}

View File

@@ -0,0 +1,10 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { Fragment } from 'react';
// Ensure children are re-rendered when the search query changes
export default function ChildrenWrapper({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams();
return <Fragment key={searchParams.get('q')}>{children}</Fragment>;
}

24
app/search/layout.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { Footer } from 'components/layout/footer';
import Collections from 'components/layout/search/collections';
import FilterList from 'components/layout/search/filter';
import { sorting } from 'lib/constants';
import ChildrenWrapper from './children-wrapper';
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 md:flex-row">
<div className="order-first w-full flex-none md:max-w-[125px]">
<Collections />
</div>
<div className="order-last min-h-screen w-full md:order-none">
<ChildrenWrapper>{children}</ChildrenWrapper>
</div>
<div className="order-none flex-none md:order-last md:w-[125px]">
<FilterList list={sorting} title="Sort by" />
</div>
</div>
<Footer />
</>
);
}

18
app/search/loading.tsx Normal file
View File

@@ -0,0 +1,18 @@
import Grid from 'components/grid';
export default function Loading() {
return (
<>
<div className="mb-4 h-6" />
<Grid className="grid-cols-2 lg:grid-cols-3">
{Array(12)
.fill(0)
.map((_, index) => {
return (
<Grid.Item key={index} className="animate-pulse bg-neutral-100 dark:bg-neutral-800" />
);
})}
</Grid>
</>
);
}

38
app/search/page.tsx Normal file
View File

@@ -0,0 +1,38 @@
import ProductGridItems from '@/components/layout/ProductGridItems';
import Grid from 'components/grid';
import { defaultSort, sorting } from 'lib/constants';
import { getProducts } from 'lib/shopify';
export const metadata = {
title: 'Search',
description: 'Search for products in the store.'
};
export default async function SearchPage(props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const { sort, q: searchValue } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getProducts({ sortKey, reverse, query: searchValue });
const resultsText = products.length > 1 ? 'results' : 'result';
return (
<>
{searchValue ? (
<p className="mb-4">
{products.length === 0
? 'There are no products that match '
: `Showing ${products.length} ${resultsText} for `}
<span className="font-bold">&quot;{searchValue}&quot;</span>
</p>
) : null}
{products.length > 0 ? (
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<ProductGridItems products={products} />
</Grid>
) : null}
</>
);
}

54
app/sitemap.ts Normal file
View File

@@ -0,0 +1,54 @@
import { getCollections, getPages, getProducts } from 'lib/shopify';
import { validateEnvironmentVariables } from 'lib/utils';
import { MetadataRoute } from 'next';
type Route = {
url: string;
lastModified: string;
};
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: 'http://localhost:3000';
export const dynamic = 'force-dynamic';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
validateEnvironmentVariables();
const routesMap = [''].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date().toISOString()
}));
const collectionsPromise = getCollections().then((collections) =>
collections.map((collection) => ({
url: `${baseUrl}${collection.path}`,
lastModified: collection.updatedAt
}))
);
const productsPromise = getProducts({}).then((products) =>
products.map((product) => ({
url: `${baseUrl}/product/${product.handle}`,
lastModified: product.updatedAt
}))
);
const pagesPromise = getPages().then((pages) =>
pages.map((page) => ({
url: `${baseUrl}/${page.handle}`,
lastModified: page.updatedAt
}))
);
let fetchedRoutes: Route[] = [];
try {
fetchedRoutes = (await Promise.all([collectionsPromise, productsPromise, pagesPromise])).flat();
} catch (error) {
throw JSON.stringify(error, null, 2);
}
return [...routesMap, ...fetchedRoutes];
}

View File

@@ -0,0 +1,78 @@
import { Heading, Text } from "@/components/ui/Typography";
import { container } from "@/lib/utils";
export default function TermsOfServicePage() {
return (
<div className={`${container} py-12`}>
<Heading level={1} className="mb-8 text-center">Terms of Service</Heading>
<div className="max-w-3xl mx-auto">
<Text className="mb-6">
Last updated: {new Date().toLocaleDateString()}
</Text>
<Heading level={2} className="mb-4 mt-8">1. Agreement to Terms</Heading>
<Text className="mb-4">
By accessing our website at <span className="font-medium">sentshop.com</span>, you are agreeing to be bound by these terms of service,
all applicable laws and regulations, and agree that you are responsible for compliance with any
applicable local laws. If you do not agree with any of these terms, you are prohibited from
using or accessing this site.
</Text>
<Heading level={2} className="mb-4 mt-8">2. Use License</Heading>
<Text className="mb-4">
Permission is granted to temporarily download one copy of the materials (information or software)
on SENT's website for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title,
and under this license you may not:
</Text>
<ul className="list-disc pl-6 mb-6 space-y-2">
<li>Modify or copy the materials;</li>
<li>Use the materials for any commercial purpose, or for any public display (commercial or non-commercial);</li>
<li>Attempt to decompile or reverse engineer any software contained on SENT's website;</li>
<li>Remove any copyright or other proprietary notations from the materials; or</li>
<li>Transfer the materials to another person or "mirror" the materials on any other server.</li>
</ul>
<Text className="mb-4">
This license shall automatically terminate if you violate any of these restrictions and may be terminated by SENT at any time.
Upon terminating your viewing of these materials or upon the termination of this license,
you must destroy any downloaded materials in your possession whether in electronic or printed format.
</Text>
<Heading level={2} className="mb-4 mt-8">3. Disclaimer</Heading>
<Text className="mb-4">
The materials on SENT's website are provided on an 'as is' basis. SENT makes no warranties,
expressed or implied, and hereby disclaims and negates all other warranties including, without limitation,
implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property
or other violation of rights.
</Text>
<Text className="mb-4">
Further, SENT does not warrant or make any representations concerning the accuracy, likely results,
or reliability of the use of the materials on its website or otherwise relating to such materials or
on any sites linked to this site.
</Text>
<Heading level={2} className="mb-4 mt-8">4. Limitations</Heading>
<Text className="mb-4">
In no event shall SENT or its suppliers be liable for any damages (including, without limitation,
damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use
the materials on SENT's website, even if SENT or a SENT authorized representative has been notified orally or in writing
of the possibility of such damage.
</Text>
<Heading level={2} className="mb-4 mt-8">5. Governing Law</Heading>
<Text className="mb-4">
These terms and conditions are governed by and construed in accordance with the laws of Croatia
and you irrevocably submit to the exclusive jurisdiction of the courts in that location.
</Text>
<Heading level={2} className="mb-4 mt-8">6. Contact Us</Heading>
<Text className="mb-6">
If you have any questions about these Terms, please contact us at:
</Text>
<Text className="font-medium mb-1">Email: info@sentshop.com</Text>
<Text className="font-medium mb-1">Phone: +385 1 234 5678</Text>
<Text className="font-medium mb-6">Address: Ilica 123, 10000 Zagreb, Croatia</Text>
</div>
</div>
);
}

29
app/test-boxes/page.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { ProductGrid } from "@/components/products/ProductGrid";
import { Section } from "@/components/ui/Section";
import { Heading, Text } from "@/components/ui/Typography";
import { getBoxProducts } from "lib/shopify";
export default async function TestBoxesPage() {
const boxes = await getBoxProducts({
sortKey: 'CREATED_AT',
reverse: true
});
return (
<Section>
<div className="mb-6">
<Heading level={1}>Test Box Products</Heading>
<Text>Found {boxes.length} box products. This is a temporary test page.</Text>
</div>
{boxes.length === 0 ? (
<div className="p-12 text-center bg-gray-100 rounded-lg">
<Text size="lg" className="font-semibold">No box products found</Text>
<Text className="mt-2">Try adding products with 'box' in the title or with the 'box' tag.</Text>
</div>
) : (
<ProductGrid products={boxes} title="Box Products" />
)}
</Section>
);
}