diff --git a/360/layout/footer-menu.tsx b/360/layout/footer-menu.tsx
new file mode 100644
index 000000000..444406294
--- /dev/null
+++ b/360/layout/footer-menu.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import clsx from 'clsx';
+import { Menu } from 'lib/shopify/types';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { useEffect, useState } from 'react';
+
+const FooterMenuItem = ({ item }: { item: Menu }) => {
+ const pathname = usePathname();
+ const [active, setActive] = useState(pathname === item.path);
+
+ useEffect(() => {
+ setActive(pathname === item.path);
+ }, [pathname, item.path]);
+
+ return (
+
+
+ {item.title}
+
+
+ );
+};
+
+export default function FooterMenu({ menu }: { menu: Menu[] }) {
+ if (!menu.length) return null;
+
+ return (
+
+ );
+}
diff --git a/360/layout/footer.tsx b/360/layout/footer.tsx
new file mode 100644
index 000000000..704ff359b
--- /dev/null
+++ b/360/layout/footer.tsx
@@ -0,0 +1,67 @@
+import Link from 'next/link';
+
+import LogoSquare from 'components/logo-square';
+import { Suspense } from 'react';
+
+const { COMPANY_NAME, SITE_NAME } = process.env;
+
+export default async function Footer() {
+ const currentYear = new Date().getFullYear();
+ const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
+ const skeleton = 'w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700';
+ // const menu = await getMenu('next-js-frontend-footer-menu');
+ const copyrightName = COMPANY_NAME || SITE_NAME || '';
+
+ return (
+
+ );
+}
diff --git a/360/layout/navbar/index.tsx b/360/layout/navbar/index.tsx
new file mode 100644
index 000000000..2d56ebe2c
--- /dev/null
+++ b/360/layout/navbar/index.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import Cart from 'components/cart';
+import OpenCart from 'components/cart/open-cart';
+import LogoSquare from 'components/logo-square';
+import { Menu } from 'lib/shopify/types';
+import Link from 'next/link';
+import { Suspense } from 'react';
+import Search, { SearchSkeleton } from './search';
+const { SITE_NAME } = process.env;
+
+export default function Navbar() {
+ alert(123);
+ const menu = [];
+ return (
+
+ );
+}
diff --git a/360/layout/navbar/mobile-menu.tsx b/360/layout/navbar/mobile-menu.tsx
new file mode 100644
index 000000000..9091f93d3
--- /dev/null
+++ b/360/layout/navbar/mobile-menu.tsx
@@ -0,0 +1,100 @@
+'use client';
+
+import { Dialog, Transition } from '@headlessui/react';
+import Link from 'next/link';
+import { usePathname, useSearchParams } from 'next/navigation';
+import { Fragment, Suspense, useEffect, useState } from 'react';
+
+import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
+import { Menu } from 'lib/shopify/types';
+import Search, { SearchSkeleton } from './search';
+
+export default function MobileMenu({ menu }: { menu: Menu[] }) {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const [isOpen, setIsOpen] = useState(false);
+ const openMobileMenu = () => setIsOpen(true);
+ const closeMobileMenu = () => setIsOpen(false);
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth > 768) {
+ setIsOpen(false);
+ }
+ };
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, [isOpen]);
+
+ useEffect(() => {
+ setIsOpen(false);
+ }, [pathname, searchParams]);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/360/layout/navbar/search.tsx b/360/layout/navbar/search.tsx
new file mode 100644
index 000000000..551d781c2
--- /dev/null
+++ b/360/layout/navbar/search.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
+import { createUrl } from 'lib/utils';
+import { useRouter, useSearchParams } from 'next/navigation';
+
+export default function Search() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+
+ const val = e.target as HTMLFormElement;
+ const search = val.search as HTMLInputElement;
+ const newParams = new URLSearchParams(searchParams.toString());
+
+ if (search.value) {
+ newParams.set('q', search.value);
+ } else {
+ newParams.delete('q');
+ }
+
+ router.push(createUrl('/search', newParams));
+ }
+
+ return (
+
+ );
+}
+
+export function SearchSkeleton() {
+ return (
+
+ );
+}
diff --git a/360/layout/product-grid-items.tsx b/360/layout/product-grid-items.tsx
new file mode 100644
index 000000000..ea8a5ebf7
--- /dev/null
+++ b/360/layout/product-grid-items.tsx
@@ -0,0 +1,28 @@
+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) => (
+
+
+
+
+
+ ))}
+ >
+ );
+}
diff --git a/360/layout/search/collections.tsx b/360/layout/search/collections.tsx
new file mode 100644
index 000000000..c45833a39
--- /dev/null
+++ b/360/layout/search/collections.tsx
@@ -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 ;
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/360/layout/search/filter/dropdown.tsx b/360/layout/search/filter/dropdown.tsx
new file mode 100644
index 000000000..31daa25ce
--- /dev/null
+++ b/360/layout/search/filter/dropdown.tsx
@@ -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(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 (
+
+
{
+ 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"
+ >
+
{active}
+
+
+ {openSelect && (
+
{
+ 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) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/360/layout/search/filter/index.tsx b/360/layout/search/filter/index.tsx
new file mode 100644
index 000000000..11a7cd367
--- /dev/null
+++ b/360/layout/search/filter/index.tsx
@@ -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) => (
+
+ ))}
+ >
+ );
+}
+
+export default function FilterList({ list, title }: { list: ListItem[]; title?: string }) {
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/360/layout/search/filter/item.tsx b/360/layout/search/filter/item.tsx
new file mode 100644
index 000000000..3fce8e8a9
--- /dev/null
+++ b/360/layout/search/filter/item.tsx
@@ -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 (
+
+
+ {item.title}
+
+
+ );
+}
+
+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 (
+
+
+ {item.title}
+
+
+ );
+}
+
+export function FilterItem({ item }: { item: ListItem }) {
+ return 'path' in item ? : ;
+}