Skip to content

Commit

Permalink
feat(frontend): add menu component (#53)
Browse files Browse the repository at this point in the history
* feat(frontend): add menu component

* feat(frontend): add menu snapshot
  • Loading branch information
Dario-Au authored Dec 24, 2024
1 parent 558f0a2 commit eed3300
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 27 deletions.
3 changes: 3 additions & 0 deletions frontend/app/.server/locales/gcweb-en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"language": "English",
"app": {
"menu": "Menu"
},
"nav": {
"skip-to-content": "Skip to main content",
"skip-to-about": "Skip to About this site"
Expand Down
3 changes: 3 additions & 0 deletions frontend/app/.server/locales/gcweb-fr.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"language": "Français",
"app": {
"menu": "Menu"
},
"nav": {
"skip-to-content": "Passer au contenu principal",
"skip-to-about": "Passer à « À propos de ce site »"
Expand Down
69 changes: 69 additions & 0 deletions frontend/app/components/menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { ComponentProps} from "react";
import { useState } from "react"
import { useTranslation } from "react-i18next";
import { cn } from "~/utils/tailwind-utils";
import { InlineLink } from "./inline-link";

type MenuItemProps = ComponentProps<typeof InlineLink>

export function MenuItem({children, ...props}: MenuItemProps) {
return (
<InlineLink
role="menuitem"
id="menu-item"
className="hover:text-blue-950 active:text-white focus:text-blue-400 text-md text-white block px-4 py-2 text-md hover:bg-slate-300 focus:bg-slate-600 active:bg-slate-800 text-md"
{...props}
>
{children}
</InlineLink>
)
}

interface MenuProps {
className?: string;
children: React.ReactNode;
}

export function Menu({ className, children }: MenuProps) {
const { t } = useTranslation(['gcweb']);
const [open, setOpen] = useState(false);
const baseClassName = cn(`${open ? "bg-slate-900 text-white hover:bg-slate-800" : "bg-slate-700 text-white hover:bg-slate-600"} hover:underline text-lg inline-flex justify-center space-x-2 rounded-b-md border-b border-l border-r border-slate-700 px-4 py-2 ring-black ring-opacity-5`);

const onClick = () => {
setOpen((value) => !value)
}

return (
<div className="relative inline-block text-left">
<button
onClick={onClick}
className={cn(baseClassName, className)}
aria-haspopup={true}
aria-expanded={open}
>
<span>{t('gcweb:app.menu')}</span>
{open ? (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 my-auto">
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
)}
</button>
{open && (
<div className="origin-top-left absolute left-0 mt-2 w-64 rounded-md shadow-lg bg-slate-700 ring-1 ring-black ring-opacity-5"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
tabIndex={-1}
>
<div className="py-1" role="none">
{children}
</div>
</div>
)}
</div>
)
}
18 changes: 6 additions & 12 deletions frontend/app/routes/protected/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { Trans, useTranslation } from 'react-i18next';
import type { Route } from './+types/index';

import { requireAuth } from '~/.server/utils/auth-utils';
import { InlineLink } from '~/components/inline-link';
import { getFixedT } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { Menu, MenuItem } from '~/components/menu';

export const handle = {
i18nNamespace: [...parentHandle.i18nNamespace, 'protected'],
Expand All @@ -28,24 +28,18 @@ export default function Index() {

return (
<div className="mb-8">
<Menu>
<MenuItem to="/">{t('protected:index.home')}</MenuItem>
<MenuItem file="routes/protected/admin.tsx">{t('protected:index.admin-dashboard')}</MenuItem>
<MenuItem file="routes/public/index.tsx">{t('protected:index.public')}</MenuItem>
</Menu>
<p className="mt-8 text-lg">
<Trans
i18nKey="protected:index.resources"
components={{ mark: <mark /> }}
values={{ resource: t('protected:resource') }}
/>
</p>
<ul className="ml-8 mt-8 list-disc">
<li>
<InlineLink file="routes/protected/admin.tsx">{t('protected:index.admin-dashboard')}</InlineLink>
</li>
<li>
<InlineLink file="routes/public/index.tsx">{t('protected:index.public')}</InlineLink>
</li>
<li>
<InlineLink to="/">{t('protected:index.home')}</InlineLink>
</li>
</ul>
</div>
);
}
20 changes: 11 additions & 9 deletions frontend/app/routes/protected/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,21 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
decoding="async"
/>
</AppLink>
<LanguageSwitcher>{t('gcweb:language-switcher.alt-lang')}</LanguageSwitcher>
<div className="text-right">
<LanguageSwitcher>{t('gcweb:language-switcher.alt-lang')}</LanguageSwitcher>
{!!loaderData.name && (
<div className="mt-4 text-right">
<p className="font-bold">{loaderData.name.toString()}</p>
<p>
<InlineLink to="/auth/logout">Logout</InlineLink>
</p>
</div>
)}
</div>
</div>
</div>
</header>
<main className="container">
{!!loaderData.name && (
<div className="mt-4 text-right">
<p>{loaderData.name.toString()}</p>
<p>
<InlineLink to="/auth/logout">Logout</InlineLink>
</p>
</div>
)}
<Outlet />
<PageDetails buildDate={BUILD_DATE} buildVersion={BUILD_VERSION} pageId={pageId} />
</main>
Expand Down
10 changes: 4 additions & 6 deletions frontend/app/routes/public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next';

import type { Route } from './+types/index';

import { InlineLink } from '~/components/inline-link';
import { PageTitle } from '~/components/page-title';
import { getFixedT } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/public/layout';
import { Menu, MenuItem } from '~/components/menu';

export const handle = {
i18nNamespace: [...parentHandle.i18nNamespace, 'public'],
Expand All @@ -27,13 +27,11 @@ export default function Index() {

return (
<>
<Menu>
<MenuItem file="routes/protected/index.tsx">{t('public:index.navigate')}</MenuItem>
</Menu>
<PageTitle>{t('public:index.page-title')}</PageTitle>
<p className="mt-8">{t('public:index.about')}</p>
<ul className="ml-8 mt-8 list-disc">
<li>
<InlineLink file="routes/protected/index.tsx">{t('public:index.navigate')}</InlineLink>
</li>
</ul>
</>
);
}
5 changes: 5 additions & 0 deletions frontend/tests/components/__snapshots__/menu.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Menu > should correctly render a Menu with a MenuItem when the file property is provided > expected html 1`] = `"<div class="relative inline-block text-left"><button class="bg-slate-700 text-white hover:bg-slate-600 hover:underline text-lg inline-flex justify-center space-x-2 rounded-b-md border-b border-l border-r border-slate-700 px-4 py-2 ring-black ring-opacity-5" aria-haspopup="true" aria-expanded="false"><span>gcweb:app.menu</span><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 my-auto"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"></path></svg></button></div>"`;

exports[`Menu > should correctly render a Menu with a MenuItem when the to property is provided > expected html 1`] = `"<div class="relative inline-block text-left"><button class="bg-slate-700 text-white hover:bg-slate-600 hover:underline text-lg inline-flex justify-center space-x-2 rounded-b-md border-b border-l border-r border-slate-700 px-4 py-2 ring-black ring-opacity-5" aria-haspopup="true" aria-expanded="false"><span>gcweb:app.menu</span><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 my-auto"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"></path></svg></button></div>"`;
37 changes: 37 additions & 0 deletions frontend/tests/components/menu.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render } from '@testing-library/react';
import { createRoutesStub } from 'react-router';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { Menu, MenuItem } from '~/components/menu';

describe('Menu', () => {
beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(() => {});
});

it('should correctly render a Menu with a MenuItem when the file property is provided', () => {
const RoutesStub = createRoutesStub([
{
path: '/fr/public',
Component: () => <MenuItem file="routes/public/index.tsx">This is a test</MenuItem>,
},
]);

const { container } = render(<Menu><RoutesStub initialEntries={['/fr/public']} /></Menu>);

expect(container.innerHTML).toMatchSnapshot('expected html');
});

it('should correctly render a Menu with a MenuItem when the to property is provided', () => {
const RoutesStub = createRoutesStub([
{
path: '/fr/public',
Component: () => <MenuItem to="https://example.com/">This is a test</MenuItem>,
},
]);

const { container } = render(<Menu><RoutesStub initialEntries={['/fr/public']} /></Menu>);

expect(container.innerHTML).toMatchSnapshot('expected html');
});
});

0 comments on commit eed3300

Please sign in to comment.