chore: remove _app, _document and middleware
This commit is contained in:
parent
7fefd2ed49
commit
8d690e408a
14 changed files with 92 additions and 266 deletions
|
@ -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",
|
||||
|
|
|
@ -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'}
|
||||
|
|
21
src/app/components/ClientProviders/ClientProviders.tsx
Normal file
21
src/app/components/ClientProviders/ClientProviders.tsx
Normal 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} />;
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { ThemeProvider } from './ThemeProvider';
|
1
src/app/components/ClientProviders/index.ts
Normal file
1
src/app/components/ClientProviders/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { ClientProviders } from './ClientProviders';
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { Header } from './Header';
|
|
@ -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 });
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue