Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adjust navigation #79

Merged
merged 4 commits into from
Nov 14, 2024
Merged
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
117 changes: 97 additions & 20 deletions apps/storefront/src/app/[locale]/(main)/_components/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,124 @@
"use client";

import { type Menu } from "@nimara/domain/objects/Menu";
import { useState } from "react";

import type { Menu } from "@nimara/domain/objects/Menu";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@nimara/ui/components/navigation-menu";
import { RichText } from "@nimara/ui/components/rich-text";

import { Link } from "@/i18n/routing";
import { generateLinkUrl } from "@/lib/helpers";
import { Link, useRouter } from "@/i18n/routing";
import { generateLinkUrl, getQueryParams } from "@/lib/cms";
import { paths } from "@/lib/paths";
import type { Maybe } from "@/lib/types";

export const Navigation = ({ menu }: { menu: Maybe<Menu> }) => {
const router = useRouter();

// Close menu manually
const [value, setValue] = useState("");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value sound too generic. Is it possible to have some more speciifc name? smthng like currentMenuItem ?


if (!menu || menu?.items?.length === 0) {
return null;
}

return (
<NavigationMenu className="mx-auto hidden pb-6 pt-3 md:flex">
<NavigationMenuList>
<NavigationMenu
onValueChange={setValue}
value={value}
className="mx-auto hidden max-w-screen-xl pb-6 pt-3 md:flex"
>
<NavigationMenuList className="gap-6">
{menu.items.map((item) => (
<NavigationMenuItem key={item.id}>
<NavigationMenuTrigger showIcon={!!item.children?.length}>
{item?.url && <Link href={item.url}>{item.name}</Link>}
<NavigationMenuTrigger
showIcon={!!item.children?.length}
onClick={() => {
const { queryKey, queryValue } = getQueryParams(item);

if (queryKey && queryValue) {
router.replace(
paths.search.asPath({
query: {
[queryKey]: queryValue,
},
}),
);
}
}}
>
{item.name}
</NavigationMenuTrigger>
{item.children?.length ? (
<NavigationMenuContent>
<div className="grid w-[400px] p-2">
{item.children?.map((child) => (
<NavigationMenuLink asChild key={child.id} className="p-2">
<Link href={generateLinkUrl(child, paths)}>
<div className="text-sm font-medium leading-none group-hover:underline">
{child.name ||
child.collection?.name ||
child.category?.name}
</div>
</Link>
</NavigationMenuLink>
))}
<div className="grid w-full grid-cols-6 p-6">
<div className="col-span-2 flex flex-col gap-3 pr-6">
{item.children?.length
? item.children
?.filter((child) => !child.collection)
.map((child) => (
<Link
key={child.id}
href={generateLinkUrl(child, paths)}
className="group block space-y-1 rounded-md p-3 hover:bg-accent"
onClick={() => setValue("")}
>
<div className="text-sm font-medium leading-none">
{child.name || child.category?.name}
</div>
<div className="text-sm leading-snug text-muted-foreground">
<RichText
className="py-1"
jsonStringData={child.category?.description}
/>
</div>
</Link>
))
: null}
</div>

<div className="col-span-4 grid grid-cols-3 gap-3">
{item.children?.length
? item.children
?.filter((child) => child.collection)
.slice(0, 3)
.map((child) => (
<Link
key={child.id}
href={generateLinkUrl(child, paths)}
className="group relative min-h-[270px] overflow-hidden rounded-lg bg-accent"
onClick={() => setValue("")}
>
<div
className="h-1/2 bg-cover bg-center"
style={{
backgroundImage: `url(${child.collection?.backgroundImage?.url})`,
}}
/>
<div className="flex h-1/2 flex-col justify-start bg-muted/50 p-6">
<div className="relative z-20 space-y-2">
<div className="text-lg font-medium leading-none group-hover:underline">
{child.name || child.collection?.name}
</div>
<div className="text-sm leading-snug text-muted-foreground">
<RichText
className="py-1"
jsonStringData={
child.collection?.description
}
/>
</div>
</div>
</div>
</Link>
))
: null}
</div>
</div>
</NavigationMenuContent>
) : null}
Expand Down
2 changes: 1 addition & 1 deletion apps/storefront/src/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getTranslations } from "next-intl/server";
import { ReactComponent as NimaraLogo } from "@/assets/nimara-logo.svg";
import { CACHE_TTL } from "@/config";
import { Link } from "@/i18n/routing";
import { generateLinkUrl } from "@/lib/helpers";
import { generateLinkUrl } from "@/lib/cms";
import { paths } from "@/lib/paths";
import { getCurrentRegion } from "@/regions/server";
import { cmsMenuService } from "@/services";
Expand Down
57 changes: 36 additions & 21 deletions apps/storefront/src/components/mobile-navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Menu } from "@nimara/domain/objects/Menu";

import { Link } from "@/i18n/routing";
import { generateLinkUrl, isInternalUrl } from "@/lib/helpers";
import { generateLinkUrl, isInternalUrl } from "@/lib/cms";
import { paths } from "@/lib/paths";
import type { Maybe } from "@/lib/types";

Expand All @@ -23,33 +23,48 @@ export const MobileNavigation = ({
return (
<ul className="grid py-4">
{menu.items.map((item) => (
<li key={item.id} className="p-2 text-stone-500" onClick={handleClick}>
<li key={item.id} className="p-2 text-stone-500">
{isInternalUrl(item.url) ? (
<Link href={generateLinkUrl(item, paths)}>
<Link href={generateLinkUrl(item, paths)} onClick={handleClick}>
{item.name || item.category?.name || item.collection?.name}
{item.children?.length ? (
<ul>
{item.children.map((child) => (
<li
key={child.id}
className="py-2 text-stone-900"
onClick={handleClick}
>
<Link href={generateLinkUrl(child, paths)}>
{child.name ||
child.collection?.name ||
child.category?.name}
</Link>
</li>
))}
</ul>
) : null}
</Link>
) : (
<a href={item.url as string} target="_blank">
<a
href={item.url as string}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
>
{item.name}
</a>
)}
{item.children?.length ? (
<ul className="mt-2 pl-6">
{item.children.map((child) => (
<li key={child.id} className="py-1 pl-2 text-stone-700">
{isInternalUrl(child.url) ? (
<Link
href={generateLinkUrl(child, paths)}
onClick={handleClick}
>
{child.name ||
child.collection?.name ||
child.category?.name}
</Link>
) : (
<a
href={child.url as string}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleMenuItemClick ? to be more specific

>
{child.name}
</a>
)}
</li>
))}
</ul>
) : null}
</li>
))}
</ul>
Expand Down
89 changes: 89 additions & 0 deletions apps/storefront/src/lib/cms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { MenuItem } from "@nimara/domain/objects/Menu";
import { loggingService } from "@nimara/infrastructure/logging/service";

export const getQueryParams = (
item: MenuItem,
): { queryKey: string | null; queryValue: string | null } => {
if (item.url) {
const url = new URL(item.url);
const queryKey = Array.from(url.searchParams.keys())[0] || null;
const queryValue = queryKey ? url.searchParams.get(queryKey) : null;

if (queryKey && queryValue) {
return { queryKey, queryValue };
}
}

if (item.category?.slug) {
return { queryKey: "category", queryValue: item.category.slug };
}

if (item.collection?.slug) {
return { queryKey: "collection", queryValue: item.collection.slug };
}

return { queryKey: null, queryValue: null };
};

interface Paths {
page: {
asPath: (params: { slug: string }) => string;
};
search: {
asPath: (params: { query: Record<string, string> }) => string;
};
}
//TO DO - handle validating internal url
export const generateLinkUrl = (item: MenuItem, paths: Paths): string => {
if (item.collection) {
return paths.search.asPath({
query: { collection: item.collection.slug },
});
}
if (item.category) {
return paths.search.asPath({
query: { category: item.category.slug },
});
}
if (item.page) {
return paths.page.asPath({ slug: item.page.slug });
}
if (item.url) {
return item.url;
}

return "#";
};

const internalUrls = [
"https://nimara-dev.vercel.app",
"https://nimara-stage.vercel.app",
"https://nimara-prod.vercel.app",
"http://localhost",
"https://localhost",
];

export const isInternalUrl = (url: string | null): boolean => {
if (url === null) {
return true;
}
if (url) {
try {
const parsedUrl = new URL(url);

return internalUrls.some((internalUrl) => {
const internalParsedUrl = new URL(internalUrl);

return parsedUrl.hostname === internalParsedUrl.hostname;
});
} catch (e) {
loggingService.error("Given URL is not internal", {
error: e,
});

return false;
}
}

return false;
};
65 changes: 0 additions & 65 deletions apps/storefront/src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { Attribute } from "@nimara/domain/objects/Attribute";
import type { MenuItem } from "@nimara/domain/objects/Menu";
import { loggingService } from "@nimara/infrastructure/logging/service";

export const getAttributes = (
attributes: Attribute[] | undefined,
Expand All @@ -21,66 +19,3 @@ export const getAttributes = (
{} as { [key: string]: Attribute },
);
};

interface Paths {
page: {
asPath: (params: { slug: string }) => string;
};
search: {
asPath: (params: { query: Record<string, string> }) => string;
};
}
//TO DO - handle validating internal url
export const generateLinkUrl = (item: MenuItem, paths: Paths): string => {
if (item.collection) {
return paths.search.asPath({
query: { collection: item.collection.slug },
});
}
if (item.category) {
return paths.search.asPath({
query: { category: item.category.slug },
});
}
if (item.page) {
return paths.page.asPath({ slug: item.page.slug });
}
if (item.url) {
return item.url;
}

return "#";
};

const internalUrls = [
"https://nimara-dev.vercel.app",
"https://nimara-stage.vercel.app",
"https://nimara-prod.vercel.app",
"http://localhost",
"https://localhost",
];

export const isInternalUrl = (url: string | null): boolean => {
if (url === null) {
return true;
}
if (url) {
try {
const parsedUrl = new URL(url);

return internalUrls.some((internalUrl) => {
const internalParsedUrl = new URL(internalUrl);

return parsedUrl.hostname === internalParsedUrl.hostname;
});
} catch (e) {
loggingService.error("Given URL is not internal", {
error: e,
});

return false;
}
}

return false;
};
Loading