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

View File

@@ -0,0 +1,32 @@
import Grid from 'components/grid';
import { GridTileImage } from 'components/grid/tile';
import { Product } from 'lib/shopify/types';
import Link from 'next/link';
export default function ProductGridItems({ products }: { products: Product[] }) {
return (
<>
{products.map((product) => (
<Grid.Item key={product.handle} className="animate-fadeIn">
<Link
className="relative inline-block h-full w-full"
href={`/product/${product.handle}`}
prefetch={true}
>
<GridTileImage
alt={product.title}
label={{
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
/>
</Link>
</Grid.Item>
))}
</>
);
}

View File

@@ -0,0 +1,59 @@
'use client';
import Link from 'next/link';
import { useState } from 'react';
const categories = {
'Men': [
{ name: 'T-Shirts', href: '/collections/mens-t-shirts' },
{ name: 'Hoodies', href: '/collections/mens-hoodies' },
{ name: 'Pants', href: '/collections/mens-pants' },
{ name: 'Accessories', href: '/collections/mens-accessories' }
],
'Women': [
{ name: 'Dresses', href: '/collections/womens-dresses' },
{ name: 'Tops', href: '/collections/womens-tops' },
{ name: 'Bottoms', href: '/collections/womens-bottoms' },
{ name: 'Accessories', href: '/collections/womens-accessories' }
]
};
export default function Categories() {
const [activeSection, setActiveSection] = useState<string | null>(null);
return (
<div className="relative">
<div className="flex gap-6">
{Object.keys(categories).map((section) => (
<button
key={section}
className="relative py-4 text-sm font-medium text-gray-700 hover:text-gray-900"
onMouseEnter={() => setActiveSection(section)}
onMouseLeave={() => setActiveSection(null)}
>
{section}
</button>
))}
</div>
{/* Dropdown menu */}
{activeSection && (
<div
className="absolute left-0 top-full z-10 w-48 rounded-md bg-white py-2 shadow-lg ring-1 ring-black ring-opacity-5"
onMouseEnter={() => setActiveSection(activeSection)}
onMouseLeave={() => setActiveSection(null)}
>
{categories[activeSection as keyof typeof categories].map((category) => (
<Link
key={category.name}
href={category.href}
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
{category.name}
</Link>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,120 @@
"use client";
import { useCookieConsent } from "@/components/cookies";
import { useTranslation } from "@/lib/hooks/useTranslation";
import { container } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
export function Footer() {
const { t } = useTranslation();
const { openModal } = useCookieConsent();
// Definiraj linkove s prijevodima
const navLinks = [
{ name: t('footer.build_box'), href: "/build-box" },
{ name: t('footer.products'), href: "/products" },
{ name: t('footer.about'), href: "/about" }
];
const legalLinks = [
{ name: t('footer.privacy_policy'), href: "/privacy-policy" },
{ name: t('footer.terms_of_service'), href: "/terms-of-service" },
{ name: t('footer.cookies_settings'), href: "#", isButton: true }
];
const socialLinks = [
{ name: t('footer.social.facebook'), href: "https://facebook.com/sentshop", icon: "/assets/images/Facebook.png" },
{ name: t('footer.social.instagram'), href: "https://instagram.com/sentshop", icon: "/assets/images/Instagram.png" }
];
const handleCookiesSettings = () => {
openModal();
};
return (
<footer className="bg-gray-100">
<div className={container}>
{/* Main footer content */}
<div className="py-12">
<div className="flex flex-col md:flex-row md:justify-between md:items-center">
{/* Logo */}
<div className="flex justify-center md:justify-start mb-8 md:mb-0">
<Link href="/" aria-label="Logo">
<Image
src="/assets/images/logo.svg"
alt="SENT logo"
width={125}
height={35}
className="w-[125px] h-auto object-contain"
/>
</Link>
</div>
{/* Navigation */}
<nav className="flex flex-col items-center md:flex-row md:space-x-8 mb-8 md:mb-0">
{navLinks.map((link, index) => (
<Link
key={index}
href={link.href}
className="text-[16px] font-bold text-gray-700 hover:text-gray-900 mb-4 md:mb-0"
>
{link.name}
</Link>
))}
</nav>
{/* Social media icons */}
<div className="flex items-center justify-center md:justify-end space-x-6 mb-8 md:mb-0">
{socialLinks.map((social, index) => (
<Link
key={index}
href={social.href}
aria-label={social.name}
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 hover:text-gray-700"
>
<Image
src={social.icon}
alt={social.name}
width={20}
height={20}
/>
</Link>
))}
</div>
</div>
</div>
{/* Legal footer */}
<div className="border-t border-gray-200">
<div className="py-6">
<div className="flex flex-col md:flex-row justify-center items-center text-[14px] leading-[21px] text-gray-500 space-y-4 md:space-y-0 md:space-x-6">
<span>&copy; {new Date().getFullYear()} Sent. {t('footer.all_rights_reserved')}</span>
{legalLinks.map((link, index) => (
link.isButton ? (
<button
key={index}
onClick={handleCookiesSettings}
className="hover:text-gray-700 text-[14px] leading-[21px]"
>
{link.name}
</button>
) : (
<Link
key={index}
href={link.href}
className="hover:text-gray-700 text-[14px] leading-[21px]"
>
{link.name}
</Link>
)
))}
</div>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,252 @@
'use client';
import { LanguageSwitcher } from '@/components/i18n/LanguageSwitcher';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { container } from '@/lib/utils';
import { useCart } from 'components/cart/cart-context';
import { Menu, X } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function Navbar() {
const { t, locale } = useTranslation();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { cart } = useCart();
const pathname = usePathname();
// Create links with translations and proper prefix
const getNavLinks = () => {
const links = [
{ name: t('navbar.build_box'), path: '/build-box' },
{ name: t('navbar.products'), path: '/products' },
{ name: t('navbar.about'), path: '/about' }
];
// Add locale prefix for English paths
if (locale === 'en') {
return links.map(link => ({
...link,
href: `/en${link.path}`
}));
}
// Return paths as is for Croatian
return links.map(link => ({
...link,
href: link.path
}));
};
const navLinks = getNavLinks();
// Close mobile menu on resize to desktop
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768) {
setIsMenuOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Prevent scrolling when mobile menu is open
useEffect(() => {
if (isMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isMenuOpen]);
// Create home link with proper prefix
const homeLink = locale === 'en' ? '/en' : '/';
// Create cart link with proper prefix
const cartLink = locale === 'en' ? '/en/cart' : '/cart';
return (
<>
<header className="border-b border-gray-200 sticky top-0 z-40 bg-white">
<div className={`${container} flex h-16 items-center justify-between`}>
{/* Logo */}
<Link href={homeLink} className="flex items-center h-12">
<Image
src="/assets/images/logo.svg"
alt="Logo"
width={125}
height={35}
className="w-[125px] h-auto object-contain"
priority
/>
</Link>
{/* Desktop Nav and Cart */}
<div className="hidden md:flex items-center space-x-8">
{navLinks.map((link) => {
const isActive = pathname === link.href ||
(link.href !== homeLink && pathname.startsWith(link.href));
return (
<Link
key={link.name}
href={link.href}
className={`text-sm font-medium transition-colors ${
isActive
? 'text-primary hover:text-primary-dark'
: 'text-black hover:text-primary'
}`}
>
{link.name}
</Link>
);
})}
<LanguageSwitcher />
<Link
href={cartLink}
className="inline-flex h-10 items-center justify-center rounded-button bg-primary px-6 py-2 text-sm font-medium text-white relative hover:bg-primary-dark"
>
<span>{t('navbar.cart')}</span>
<Image
src="/assets/images/cart-icon.png"
alt="Cart"
width={20}
height={20}
className="ml-2"
/>
{cart?.totalQuantity ? (
<span className="absolute -right-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
{cart.totalQuantity}
</span>
) : null}
</Link>
</div>
{/* Mobile menu button and cart */}
<div className="md:hidden flex items-center space-x-4">
<Link
href={cartLink}
className="relative text-black"
aria-label="View cart"
>
<Image
src="/assets/images/cart-icon-black.png"
alt="Cart"
width={24}
height={24}
/>
{cart?.totalQuantity ? (
<span className="absolute -right-2 -top-3 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
{cart.totalQuantity}
</span>
) : null}
</Link>
<button
className="text-gray-500"
onClick={() => setIsMenuOpen(true)}
aria-label="Open menu"
>
<Menu className="h-6 w-6" />
</button>
</div>
{/* Mobile menu */}
{isMenuOpen && (
<div className="fixed inset-0 z-50 bg-white md:hidden">
{/* Mobile menu header - use same container and height as main navbar */}
<div className={`${container} flex h-16 items-center justify-between`}>
<Link
href={homeLink}
className="flex items-center h-12"
onClick={() => setIsMenuOpen(false)}
>
<Image
src="/assets/images/logo.svg"
alt="Logo"
width={125}
height={35}
className="w-[125px] h-auto object-contain"
priority
/>
</Link>
<button
className="text-gray-500"
onClick={() => setIsMenuOpen(false)}
aria-label="Close menu"
>
<X className="h-6 w-6" />
</button>
</div>
{/* Mobile menu links */}
<div className={`${container} flex-1 flex flex-col py-4`}>
<div className="space-y-4">
{navLinks.map((link) => {
const isActive = pathname === link.href ||
(link.href !== homeLink && pathname.startsWith(link.href));
return (
<Link
key={link.name}
href={link.href}
className={`block text-lg font-medium py-2 transition-colors ${
isActive
? 'text-primary'
: 'text-black hover:text-primary'
}`}
onClick={() => setIsMenuOpen(false)}
>
{link.name}
</Link>
);
})}
<div className="py-2">
<LanguageSwitcher />
</div>
<Link
href={cartLink}
className="mt-4 flex h-12 w-full items-center justify-center relative"
onClick={() => setIsMenuOpen(false)}
>
<Button
variant="primary"
size="lg"
className="w-full h-full flex items-center justify-center"
>
<span>{t('navbar.cart')}</span>
<Image
src="/assets/images/cart-icon.png"
alt="Cart"
width={20}
height={20}
className="ml-2"
/>
{cart?.totalQuantity ? (
<span className="absolute right-4 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
{cart.totalQuantity}
</span>
) : null}
</Button>
</Link>
</div>
</div>
</div>
)}
</div>
</header>
</>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import Form from 'next/form';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
return (
<Form action="/search" className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
<input
key={searchParams?.get('q')}
type="text"
name="q"
placeholder="Search for products..."
autoComplete="off"
defaultValue={searchParams?.get('q') || ''}
className="w-full px-4 py-2 border"
/>
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
<MagnifyingGlassIcon className="h-4" />
</div>
</Form>
);
}
export function SearchSkeleton() {
return (
<form className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
<input
placeholder="Search for products..."
className="w-full"
/>
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
<MagnifyingGlassIcon className="h-4" />
</div>
</form>
);
}

View File

@@ -0,0 +1,37 @@
import clsx from 'clsx';
import { Suspense } from 'react';
import { getCollections } from 'lib/shopify';
import FilterList from './filter';
async function CollectionList() {
const collections = await getCollections();
return <FilterList list={collections} title="Collections" />;
}
const skeleton = 'mb-3 h-4 w-5/6 animate-pulse rounded';
const activeAndTitles = 'bg-neutral-800 dark:bg-neutral-300';
const items = 'bg-neutral-400 dark:bg-neutral-700';
export default function Collections() {
return (
<Suspense
fallback={
<div className="col-span-2 hidden h-[400px] w-full flex-none py-4 lg:block">
<div className={clsx(skeleton, activeAndTitles)} />
<div className={clsx(skeleton, activeAndTitles)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
</div>
}
>
<CollectionList />
</Suspense>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import type { ListItem } from '.';
import { FilterItem } from './item';
export default function FilterItemDropdown({ list }: { list: ListItem[] }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [active, setActive] = useState('');
const [openSelect, setOpenSelect] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setOpenSelect(false);
}
};
window.addEventListener('click', handleClickOutside);
return () => window.removeEventListener('click', handleClickOutside);
}, []);
useEffect(() => {
list.forEach((listItem: ListItem) => {
if (
('path' in listItem && pathname === listItem.path) ||
('slug' in listItem && searchParams.get('sort') === listItem.slug)
) {
setActive(listItem.title);
}
});
}, [pathname, list, searchParams]);
return (
<div className="relative" ref={ref}>
<div
onClick={() => {
setOpenSelect(!openSelect);
}}
className="flex w-full items-center justify-between rounded border border-black/30 px-4 py-2 text-sm dark:border-white/30"
>
<div>{active}</div>
<ChevronDownIcon className="h-4" />
</div>
{openSelect && (
<div
onClick={() => {
setOpenSelect(false);
}}
className="absolute z-40 w-full rounded-b-md bg-white p-4 shadow-md dark:bg-black"
>
{list.map((item: ListItem, i) => (
<FilterItem key={i} item={item} />
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { SortFilterItem } from 'lib/constants';
import { Suspense } from 'react';
import FilterItemDropdown from './dropdown';
import { FilterItem } from './item';
export type ListItem = SortFilterItem | PathFilterItem;
export type PathFilterItem = { title: string; path: string };
function FilterItemList({ list }: { list: ListItem[] }) {
return (
<>
{list.map((item: ListItem, i) => (
<FilterItem key={i} item={item} />
))}
</>
);
}
export default function FilterList({ list, title }: { list: ListItem[]; title?: string }) {
return (
<>
<nav>
{title ? (
<h3 className="hidden text-xs text-neutral-500 md:block dark:text-neutral-400">
{title}
</h3>
) : null}
<ul className="hidden md:block">
<Suspense fallback={null}>
<FilterItemList list={list} />
</Suspense>
</ul>
<ul className="md:hidden">
<Suspense fallback={null}>
<FilterItemDropdown list={list} />
</Suspense>
</ul>
</nav>
</>
);
}

View File

@@ -0,0 +1,67 @@
'use client';
import clsx from 'clsx';
import type { SortFilterItem } from 'lib/constants';
import { createUrl } from 'lib/utils';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import type { ListItem, PathFilterItem } from '.';
function PathFilterItem({ item }: { item: PathFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = pathname === item.path;
const newParams = new URLSearchParams(searchParams.toString());
const DynamicTag = active ? 'p' : Link;
newParams.delete('q');
return (
<li className="mt-2 flex text-black dark:text-white" key={item.title}>
<DynamicTag
href={createUrl(item.path, newParams)}
className={clsx(
'w-full text-sm underline-offset-4 hover:underline dark:hover:text-neutral-100',
{
'underline underline-offset-4': active
}
)}
>
{item.title}
</DynamicTag>
</li>
);
}
function SortFilterItem({ item }: { item: SortFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = searchParams.get('sort') === item.slug;
const q = searchParams.get('q');
const href = createUrl(
pathname,
new URLSearchParams({
...(q && { q }),
...(item.slug && item.slug.length && { sort: item.slug })
})
);
const DynamicTag = active ? 'p' : Link;
return (
<li className="mt-2 flex text-sm text-black dark:text-white" key={item.title}>
<DynamicTag
prefetch={!active ? false : undefined}
href={href}
className={clsx('w-full hover:underline hover:underline-offset-4', {
'underline underline-offset-4': active
})}
>
{item.title}
</DynamicTag>
</li>
);
}
export function FilterItem({ item }: { item: ListItem }) {
return 'path' in item ? <PathFilterItem item={item} /> : <SortFilterItem item={item} />;
}