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(frontend): add menu component #53

Merged
merged 2 commits into from
Dec 24, 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
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');
});
});
Loading