chore: remove _app, _document and middleware

This commit is contained in:
Nicolas Meienberger 2023-10-08 15:27:07 +02:00 committed by Nicolas Meienberger
parent 7fefd2ed49
commit 8d690e408a
14 changed files with 92 additions and 266 deletions

View File

@ -53,6 +53,7 @@
"fs-extra": "^11.1.1",
"lodash.merge": "^4.6.2",
"next": "13.5.3",
"next-client-cookies": "^1.0.5",
"next-intl": "^2.20.0",
"next-safe-action": "^3.4.0",
"pg": "^8.11.1",

View File

@ -71,6 +71,9 @@ importers:
next:
specifier: 13.5.3
version: 13.5.3(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0)(sass@1.63.6)
next-client-cookies:
specifier: ^1.0.5
version: 1.0.5(next@13.5.3)(react@18.2.0)
next-intl:
specifier: ^2.20.0
version: 2.20.0(next@13.5.3)(react@18.2.0)
@ -8806,6 +8809,11 @@ packages:
hasBin: true
dev: true
/js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
dev: false
/js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
@ -10031,6 +10039,17 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/next-client-cookies@1.0.5(next@13.5.3)(react@18.2.0):
resolution: {integrity: sha512-PqmJyJCZotR/Vg8meaqHKT2RjHoNuDkaew6JBj0gA1tlY7+6aK0Zb00KLnZSQnbGYg9OEQquk5UyWp1QACvXjQ==}
peerDependencies:
next: ^13.0.0
react: '>= 16.8.0'
dependencies:
js-cookie: 3.0.5
next: 13.5.3(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0)(sass@1.63.6)
react: 18.2.0
dev: false
/next-intl@2.20.0(next@13.5.3)(react@18.2.0):
resolution: {integrity: sha512-zeGod4WuS/j8dhDM1eLlt+GTCqqq1b09l/eHxUWQjGrK87BeN+crKasHjZWMNm38g0ddWpBW6i9KDZcTn8/p1A==}
engines: {node: '>=10'}

View File

@ -0,0 +1,21 @@
'use client';
import React, { ComponentProps } from 'react';
import { CookiesProvider } from 'next-client-cookies';
import { ThemeProvider } from './ThemeProvider';
type Props = {
children: React.ReactNode;
cookies: ComponentProps<typeof CookiesProvider>['value'];
initialTheme?: string;
};
export const ClientProviders = ({ children, initialTheme, cookies }: Props) => {
return (
<CookiesProvider value={cookies}>
<ThemeProvider initialTheme={initialTheme}>{children}</ThemeProvider>
</CookiesProvider>
);
};
export const ClientCookiesProvider: typeof CookiesProvider = (props) => <CookiesProvider {...props} />;

View File

@ -0,0 +1,25 @@
'use client';
import { useUIStore } from '@/client/state/uiStore';
import React, { useEffect } from 'react';
import { useCookies } from 'next-client-cookies';
type Props = {
children: React.ReactNode;
initialTheme?: string;
};
export const ThemeProvider = (props: Props) => {
const { children, initialTheme } = props;
const cookies = useCookies();
const { theme } = useUIStore();
useEffect(() => {
if (theme) {
cookies.set('theme', theme || initialTheme || 'light', { path: '/' });
document.body.dataset.bsTheme = theme;
}
}, [cookies, initialTheme, theme]);
return children;
};

View File

@ -0,0 +1 @@
export { ThemeProvider } from './ThemeProvider';

View File

@ -0,0 +1 @@
export { ClientProviders } from './ClientProviders';

View File

@ -1,6 +1,7 @@
import React from 'react';
import type { Metadata } from 'next';
import { cookies } from 'next/headers';
import { Inter } from 'next/font/google';
import merge from 'lodash.merge';
import { NextIntlClientProvider } from 'next-intl';
@ -8,7 +9,9 @@ import { NextIntlClientProvider } from 'next-intl';
import './global.css';
import clsx from 'clsx';
import { Toaster } from 'react-hot-toast';
import Head from 'next/head';
import { getCurrentLocale } from '../utils/getCurrentLocale';
import { ClientProviders } from './components/ClientProviders';
const inter = Inter({
subsets: ['latin'],
@ -27,13 +30,26 @@ export default async function RootLayout({ children }: { children: React.ReactNo
const messages = (await import(`../client/messages/${locale}.json`)).default;
const mergedMessages = merge(englishMessages, messages);
const theme = cookies().get('theme');
return (
<html lang={locale} className={clsx(inter.className, 'border-top-wide border-primary')}>
<Head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
</Head>
<NextIntlClientProvider locale={locale} messages={mergedMessages}>
<body>
{children}
<Toaster />
</body>
<ClientProviders initialTheme={theme?.value} cookies={cookies().getAll()}>
<body data-bs-theme={theme?.value}>
{children}
<Toaster />
</body>
</ClientProviders>
</NextIntlClientProvider>
</html>
);

View File

@ -1,61 +0,0 @@
import React from 'react';
import { fireEvent, render, renderHook, screen } from '../../../../../tests/test-utils';
import { useUIStore } from '../../../state/uiStore';
import { Header } from './Header';
const pushFn = jest.fn();
jest.mock('next/router', () => {
const actualRouter = jest.requireActual('next-router-mock');
return {
...actualRouter,
useRouter: () => ({
...actualRouter.useRouter(),
push: pushFn,
}),
};
});
describe('Header', () => {
it('renders without crashing', () => {
render(<Header />);
});
it('renders the brand logo', () => {
const { container } = render(<Header />);
expect(container).toHaveTextContent('Tipi');
expect(container).toContainElement(screen.getByAltText('Tipi logo'));
});
it('renders the dark mode toggle', () => {
render(<Header />);
const darkModeToggle = screen.getByTestId('dark-mode-toggle');
expect(darkModeToggle).toContainElement(screen.getByTestId('icon-moon'));
});
it('renders the light mode toggle', () => {
render(<Header />);
const lightModeToggle = screen.getByTestId('light-mode-toggle');
expect(lightModeToggle).toContainElement(screen.getByTestId('icon-sun'));
});
it('Should toggle the dark mode on click of the dark mode toggle', () => {
const { result } = renderHook(() => useUIStore());
render(<Header />);
const darkModeToggle = screen.getByTestId('dark-mode-toggle');
fireEvent.click(darkModeToggle as Element);
expect(result.current.darkMode).toBe(true);
});
it('Should toggle the dark mode on click of the light mode toggle', () => {
const { result } = renderHook(() => useUIStore());
render(<Header />);
const lightModeToggle = screen.getByTestId('light-mode-toggle');
fireEvent.click(lightModeToggle as Element);
expect(result.current.darkMode).toBe(false);
});
});

View File

@ -1,75 +0,0 @@
import React from 'react';
import { IconBrandGithub, IconHeart, IconLogout, IconMoon, IconSun } from '@tabler/icons-react';
import Image from 'next/image';
import clsx from 'clsx';
import Link from 'next/link';
import { Tooltip } from 'react-tooltip';
import { useTranslations } from 'next-intl';
import { useUIStore } from '../../../state/uiStore';
import { NavBar } from '../NavBar';
interface IProps {
isUpdateAvailable?: boolean;
}
export const Header: React.FC<IProps> = ({ isUpdateAvailable }) => {
const { setDarkMode } = useUIStore();
const t = useTranslations('header');
return (
<header className="text-white navbar navbar-expand-md navbar-dark navbar-overlap d-print-none" data-bs-theme="dark">
<div className="container-xl">
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu">
<span className="navbar-toggler-icon" />
</button>
<Link href="/" passHref>
<h1 className="navbar-brand d-none-navbar-horizontal pe-0 pe-md-3">
<Image
priority
alt="Tipi logo"
className={clsx('navbar-brand-image me-3')}
width={100}
height={100}
src="/tipi.png"
style={{
width: '30px',
maxWidth: '30px',
height: 'auto',
}}
/>
Tipi
</h1>
</Link>
<div className="navbar-nav flex-row order-md-last">
<div className="nav-item d-none d-xl-flex me-3">
<div className="btn-list">
<a href="https://github.com/meienberger/runtipi" target="_blank" rel="noreferrer" className="btn btn-dark">
<IconBrandGithub data-testid="icon-github" className="me-1 icon" size={24} />
{t('source-code')}
</a>
<a href="https://github.com/meienberger/runtipi?sponsor=1" target="_blank" rel="noreferrer" className="btn btn-dark">
<IconHeart className="me-1 icon text-pink" size={24} />
{t('sponsor')}
</a>
</div>
</div>
<div style={{ zIndex: 1 }} className="d-flex">
<Tooltip anchorSelect=".darkMode">{t('dark-mode')}</Tooltip>
<div onClick={() => setDarkMode(true)} role="button" aria-hidden="true" className="darkMode nav-link px-0 hide-theme-dark cursor-pointer" data-testid="dark-mode-toggle">
<IconMoon data-testid="icon-moon" size={20} />
</div>
<Tooltip anchorSelect=".lightMode">{t('light-mode')}</Tooltip>
<div onClick={() => setDarkMode(false)} aria-hidden="true" className="lightMode nav-link px-0 hide-theme-light cursor-pointer" data-testid="light-mode-toggle">
<IconSun data-testid="icon-sun" size={20} />
</div>
<Tooltip anchorSelect=".logOut">{t('logout')}</Tooltip>
<div tabIndex={0} role="button" className="logOut nav-link px-0 cursor-pointer" data-testid="logout-button">
<IconLogout size={20} />
</div>
</div>
</div>
<NavBar isUpdateAvailable={isUpdateAvailable} />
</div>
</header>
);
};

View File

@ -1 +0,0 @@
export { Header } from './Header';

View File

@ -7,6 +7,7 @@ const defaultTranslator = createTranslator({ locale: 'en', messages: englishMess
type UIStore = {
menuItem: string;
darkMode: boolean;
theme?: string;
translator: typeof defaultTranslator;
setMenuItem: (menuItem: string) => void;
setDarkMode: (darkMode: boolean) => void;
@ -17,17 +18,16 @@ export const useUIStore = create<UIStore>((set) => ({
menuItem: 'dashboard',
darkMode: false,
translator: defaultTranslator,
theme: undefined,
setTranslator: (translator: typeof defaultTranslator) => {
set({ translator });
},
setDarkMode: (darkMode: boolean) => {
if (darkMode) {
localStorage.setItem('darkMode', darkMode.toString());
document.body.dataset.bsTheme = 'dark';
set({ theme: 'dark' });
}
if (!darkMode) {
localStorage.setItem('darkMode', darkMode.toString());
document.body.dataset.bsTheme = 'light';
set({ theme: 'light' });
}
set({ darkMode });
},

View File

@ -1,42 +0,0 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const COOKIE_MAX_AGE = 60 * 60 * 24; // 1 day
/**
* Middleware to set session ID in request headers
* @param {NextRequest} request - Request object
*/
export async function middleware(request: NextRequest) {
let sessionId = '';
const cookie = request.cookies.get('tipi.sid')?.value;
// Check if session ID exists in cookies
if (cookie) {
sessionId = cookie;
}
const requestHeaders = new Headers(request.headers);
if (sessionId) {
requestHeaders.set('x-session-id', sessionId);
}
const response = NextResponse.next({
request: { headers: requestHeaders },
});
if (sessionId) {
response.headers.set('x-session-id', sessionId);
response.cookies.set('tipi.sid', sessionId, {
maxAge: COOKIE_MAX_AGE,
httpOnly: true,
secure: false,
sameSite: false,
});
}
return response;
}

View File

@ -1,47 +0,0 @@
import React, { useEffect } from 'react';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import '../client/styles/global.css';
import '../client/styles/global.scss';
import 'react-tooltip/dist/react-tooltip.css';
import { Toaster } from 'react-hot-toast';
import { useUIStore } from '../client/state/uiStore';
import { StatusProvider } from '../client/components/hoc/StatusProvider';
/**
* Next.js App component
*
* @param {AppProps} props - props passed to the app
* @returns {JSX.Element} - JSX element
*/
function MyApp({ Component, pageProps }: AppProps) {
const { setDarkMode } = useUIStore();
// check theme on component mount
useEffect(() => {
const themeCheck = () => {
if (localStorage.darkMode === 'true' || (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.body.dataset.bsTheme = 'dark';
setDarkMode(true);
} else {
document.body.dataset.bsTheme = 'light';
setDarkMode(false);
}
};
themeCheck();
}, [setDarkMode]);
return (
<main className="h-100">
<Head>
<title>Tipi</title>
</Head>
<StatusProvider>
<Component {...pageProps} />
</StatusProvider>
<Toaster />
</main>
);
}
export default MyApp;

View File

@ -1,32 +0,0 @@
import React from 'react';
import { Html, Head, Main, NextScript } from 'next/document';
/**
* Next.js Document component
*
* @returns {JSX.Element} - JSX element
*/
export default function MyDocument() {
return (
<Html lang="en">
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js" async />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
</Head>
<body className="border-top-wide border-primary">
<Main />
<NextScript />
</body>
</Html>
);
}