feat: update default layout

This commit is contained in:
Sonny
2025-08-06 19:50:53 +02:00
parent d56bd1ef80
commit 97ba56b1e7
36 changed files with 627 additions and 119 deletions

View File

@@ -1,12 +1,13 @@
const PROJECT_NAME = 'MyLinks';
const PROJECT_DESCRIPTION =
'Another bookmark manager that lets you manage and share your favorite links in an intuitive interface';
const PROJECT_URL = 'https://www.mylinks.app';
const APP_COLOR = '#f0eef6';
export const APP_COLOR = '#f0eef6';
export default {
name: PROJECT_NAME,
description: PROJECT_DESCRIPTION,
url: PROJECT_URL,
color: APP_COLOR,
};
export const PROJECT_NAME = 'MyLinks';
export const PROJECT_DESCRIPTION =
'Another bookmark manager that lets you manage and share your favorite links in an intuitive interface';
export const PROJECT_URL = 'https://www.mylinks.app';
export const PROJECT_REPO_GITHUB_URL = 'https://github.com/my-links/my-links';
export const PROJECT_EXTENSION_URL =
'https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma';
export const AUTHOR_NAME = 'Sonny';
export const AUTHOR_GITHUB_URL = 'https://github.com/Sonny93';
export const AUTHOR_WEBSITE_URL = 'https://www.sonny.dev/?utm_source=mylinks';

View File

@@ -4,6 +4,7 @@ import { isSSREnableForPage } from 'config-ssr';
import 'dayjs/locale/en';
import 'dayjs/locale/fr';
import { createRoot, hydrateRoot } from 'react-dom/client';
import DefaultLayout from '~/layouts/default_layout';
import '../i18n/index';
const appName = import.meta.env.VITE_APP_NAME || 'MyLinks';
@@ -13,11 +14,17 @@ createInertiaApp({
title: (title) => `${appName}${title && ` - ${title}`}`,
resolve: (name) => {
return resolvePageComponent(
resolve: async (name) => {
const currentPage: any = await resolvePageComponent(
`../pages/${name}.tsx`,
import.meta.glob('../pages/**/*.tsx')
);
currentPage.default.layout =
currentPage.default.layout ||
((p: any) => <DefaultLayout children={p} />);
return currentPage;
},
setup({ el, App, props }) {

View File

@@ -1,5 +1,6 @@
import { createInertiaApp } from '@inertiajs/react';
import ReactDOMServer from 'react-dom/server';
import DefaultLayout from '~/layouts/default_layout';
export default function render(page: any) {
return createInertiaApp({
@@ -7,7 +8,11 @@ export default function render(page: any) {
render: ReactDOMServer.renderToString,
resolve: (name) => {
const pages = import.meta.glob('../pages/**/*.tsx', { eager: true });
return pages[`../pages/${name}.tsx`];
let pageComponent: any = pages[`../pages/${name}.tsx`];
pageComponent.default.layout =
pageComponent?.default?.layout ||
((pageChildren: any) => <DefaultLayout children={pageChildren} />);
return pageComponent;
},
setup: ({ App, props }) => <App {...props} />,
});

View File

@@ -0,0 +1,36 @@
.navbarWrapper {
z-index: 9;
}
.navbar {
height: rem(60);
background-color: color-mix(
in srgb,
var(--mantine-color-body) 50%,
transparent
);
padding-inline: var(--mantine-spacing-lg);
transition: transform 400ms ease;
backdrop-filter: blur(16px);
overflow: hidden;
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 9;
}
.navbar__content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 100%;
max-width: 100%;
margin-inline: auto;
}
.navbar__content > div:last-child {
flex: 1;
justify-content: flex-end;
}

View File

@@ -0,0 +1,108 @@
import {
PROJECT_EXTENSION_URL,
PROJECT_NAME,
PROJECT_REPO_GITHUB_URL,
} from '#config/project';
import {
Box,
Burger,
Button,
Drawer,
Flex,
Group,
Image,
rem,
useMantineTheme,
} from '@mantine/core';
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
import { useEffect } from 'react';
import { UserDropdown } from '~/components/common/floating_navbar/user_dropdown';
import { ExternalLinkUnstyled } from '~/components/common/links/external_link_unstyled';
import { InternalLink } from '~/components/common/links/internal_link';
import { useAuth } from '~/hooks/use_auth';
import classes from './floating_navbar.module.css';
interface FloatingNavbarProps {
width: string;
}
export function FloatingNavbar({ width }: FloatingNavbarProps) {
const auth = useAuth();
const theme = useMantineTheme();
const [opened, handler] = useDisclosure(false);
const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`, false);
useEffect(() => {
if (opened && !isMobile) {
handler.close();
}
}, [isMobile]);
const links = (
<>
<InternalLink href="/dashboard" style={{ fontSize: rem(16) }}>
Dashboard
</InternalLink>
<ExternalLinkUnstyled
href={PROJECT_REPO_GITHUB_URL}
style={{ fontSize: rem(16) }}
>
Github
</ExternalLinkUnstyled>
<ExternalLinkUnstyled
href={PROJECT_EXTENSION_URL}
style={{ fontSize: rem(16) }}
>
Extension
</ExternalLinkUnstyled>
</>
);
return (
<>
<Box className={classes.navbar}>
<Group className={classes.navbar__content} style={{ width }}>
<Group>
<InternalLink style={{ fontSize: rem(24) }} route="home">
<Image
src="/logo.png"
h={35}
alt="MyLinks's logo"
referrerPolicy="no-referrer"
/>
</InternalLink>
</Group>
<Group>
{!isMobile && <Group>{links}</Group>}
{isMobile && <Burger opened={opened} onClick={handler.toggle} />}
{auth.isAuthenticated && <UserDropdown />}
{!auth.isAuthenticated && (
<Button
variant="default"
component={ExternalLinkUnstyled}
newTab={false}
href="/auth/google"
>
Log in
</Button>
)}
</Group>
</Group>
{/* Mobile drawer */}
<Drawer
opened={opened}
onClose={handler.close}
padding="md"
title={PROJECT_NAME}
zIndex={999999}
onClick={handler.close}
>
<Flex direction="column" gap="md">
{links}
</Flex>
</Drawer>
</Box>
</>
);
}

View File

@@ -0,0 +1,24 @@
.user {
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border-radius: var(--mantine-radius-sm);
transition: background-color 100ms ease;
&:hover {
background-color: light-dark(
var(--mantine-color-white),
var(--mantine-color-dark-8)
);
}
@media (max-width: 768px) {
display: none;
}
}
.userActive {
background-color: light-dark(
var(--mantine-color-white),
var(--mantine-color-dark-8)
);
}

View File

@@ -0,0 +1,73 @@
import { Avatar, Group, Menu, Text, UnstyledButton } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import cx from 'clsx';
import { useTranslation } from 'react-i18next';
import { TbChevronDown, TbLogout, TbUser } from 'react-icons/tb';
import { InternalLink } from '~/components/common/links/internal_link';
import { InternalLinkUnstyled } from '~/components/common/links/internal_link_unstyled';
import { useAuth } from '~/hooks/use_auth';
import classes from './user_dropdown.module.css';
export function UserDropdown() {
const auth = useAuth();
const [userMenuOpened, { open: openUserMenu, close: closeUserMenu }] =
useDisclosure(false);
const { t } = useTranslation();
return (
<Menu
width={260}
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
onClose={closeUserMenu}
onOpen={openUserMenu}
withinPortal
>
<Menu.Target>
<UnstyledButton
className={cx(classes.user, { [classes.userActive]: userMenuOpened })}
>
<Group gap={7}>
<Avatar
src={auth.user?.avatarUrl}
alt={auth.user?.fullname}
radius="xl"
size={20}
/>
<Text fw={500} size="sm" lh={1} mr={3}>
{auth.user?.fullname}
</Text>
<TbChevronDown size={12} />
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{t('common:user')}</Menu.Label>
<Menu.Item
leftSection={<TbUser size={16} />}
component={InternalLinkUnstyled}
href={`/user/${auth.user?.fullname}`}
color="inherit"
>
{t('common:profile')}
</Menu.Item>
{auth.user?.isAdmin && (
<>
<Menu.Label>{t('common:admin')}</Menu.Label>
<InternalLink href="/admin">{t('common:admin')}</InternalLink>
</>
)}
<Menu.Label>{t('common:settings')}</Menu.Label>
<Menu.Item
leftSection={<TbLogout size={16} />}
component={InternalLinkUnstyled}
href="/auth/logout"
color="inherit"
>
{t('common:logout')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
}

View File

@@ -0,0 +1,23 @@
.footer {
background-color: color-mix(
in srgb,
var(--mantine-color-body) 50%,
transparent
);
padding: var(--mantine-spacing-sm) var(--mantine-spacing-lg);
}
.footer__content {
max-width: 100%;
gap: var(--mantine-spacing-xs);
margin-inline: auto;
}
.footer__content p {
font-size: var(--mantine-font-size-sm) !important;
color: var(--mantine-color-dimmed) !important;
}
.footer__content a {
font-size: var(--mantine-font-size-sm) !important;
}

View File

@@ -0,0 +1,33 @@
import { AUTHOR_GITHUB_URL, AUTHOR_NAME } from '#config/project';
import PATHS from '#core/constants/paths';
import { Anchor, Group, Text } from '@mantine/core';
import ExternalLink from '~/components/common/external_link';
import { ExternalLinkStyled } from '~/components/common/links/external_link_styled';
import { LocaleSwitcher } from '~/components/common/locale_switcher';
import { ThemeSwitcher } from '~/components/common/theme_switcher';
import packageJson from '../../../../package.json';
import classes from './footer.module.css';
export const Footer = () => (
<Group className={classes.footer}>
<Group className={classes.footer__content}>
<Text>
Made with by{' '}
<ExternalLinkStyled href={AUTHOR_GITHUB_URL}>
{AUTHOR_NAME}
</ExternalLinkStyled>
</Text>
<Text>
<Anchor size="sm" component={ExternalLink} href={PATHS.REPO_GITHUB}>
{packageJson.version}
</Anchor>
</Text>
<Group gap="sm" mt={4} mb={4}>
<ThemeSwitcher />
<LocaleSwitcher />
</Group>
</Group>
</Group>
);

View File

@@ -0,0 +1,28 @@
import { Anchor } from '@mantine/core';
import { AnchorHTMLAttributes, CSSProperties, ReactNode } from 'react';
interface ExternalLinkStyledProps
extends AnchorHTMLAttributes<HTMLAnchorElement> {
children: ReactNode;
style?: CSSProperties;
title?: string;
className?: string;
}
export const ExternalLinkStyled = ({
children,
title,
href,
...props
}: ExternalLinkStyledProps) => (
<Anchor<'a'>
component="a"
target="_blank"
rel="noreferrer"
title={title}
href={href}
{...props}
>
{children}
</Anchor>
);

View File

@@ -0,0 +1,26 @@
import { Anchor, CSSProperties } from '@mantine/core';
import { AnchorHTMLAttributes, ReactNode } from 'react';
interface ExternalLinkUnstyledProps
extends AnchorHTMLAttributes<HTMLAnchorElement> {
children: ReactNode;
style?: CSSProperties;
title?: string;
className?: string;
newTab?: boolean;
}
export const ExternalLinkUnstyled = ({
children,
newTab = true,
...props
}: ExternalLinkUnstyledProps) => (
<Anchor
component="a"
target={newTab ? '_blank' : undefined}
rel="noreferrer"
{...props}
style={{ ...props.style, textDecoration: 'none' }}
>
{children}
</Anchor>
);

View File

@@ -0,0 +1,50 @@
import { ApiRouteName } from '#shared/types/index';
import { Link } from '@inertiajs/react';
import { Anchor } from '@mantine/core';
import { useTuyau } from '@tuyau/inertia/react';
import { CSSProperties } from 'react';
interface InternalLinkProps {
children: React.ReactNode;
onClick?: (event: React.MouseEvent<any>) => void;
route?: ApiRouteName;
href?: string;
forceRefresh?: boolean;
style?: CSSProperties;
className?: string;
params?: Record<string, string>;
}
export const InternalLink = ({
children,
onClick,
route,
href,
forceRefresh,
style,
className,
params,
}: InternalLinkProps) => {
const tuyau = useTuyau();
if ((!route && !href) || !tuyau) {
throw new Error('InternalLink: route, href or tuyau is missing');
}
const url = route ? tuyau.$route(route, params).path : href;
if (!url) {
throw new Error('InternalLink: url not found');
}
return (
<Anchor<'a' | typeof Link>
component={forceRefresh ? 'a' : Link}
href={url}
style={style}
onClick={onClick}
className={className}
>
{children}
</Anchor>
);
};

View File

@@ -0,0 +1,61 @@
import { ApiRouteName } from '#shared/types/index';
import { Link } from '@inertiajs/react';
import { useTuyau } from '@tuyau/inertia/react';
import { CSSProperties } from 'react';
interface InternalLinkProps {
children: React.ReactNode;
onClick?: (event: React.MouseEvent<any>) => void;
route?: ApiRouteName;
href?: string;
forceRefresh?: boolean;
style?: CSSProperties;
className?: string;
params?: Record<string, string>;
}
export const InternalLinkUnstyled = ({
children,
onClick,
route,
href,
forceRefresh,
style,
className,
params,
}: InternalLinkProps) => {
const tuyau = useTuyau();
if ((!route && !href) || !tuyau) {
throw new Error('InternalLink: route, href or tuyau is missing');
}
const url = route ? tuyau.$route(route, params).path : href;
if (!url) {
throw new Error('InternalLink: url not found');
}
if (forceRefresh) {
return (
<a
href={url}
style={{ ...style, textDecoration: 'none' }}
onClick={onClick}
className={className}
>
{children}
</a>
);
}
return (
<Link
href={url}
style={{ ...style, textDecoration: 'none' }}
onClick={onClick}
className={className}
>
{children}
</Link>
);
};

View File

@@ -2,7 +2,7 @@ import { ActionIcon, Image } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { LS_LANG_KEY } from '~/constants';
export function MantineLanguageSwitcher() {
export function LocaleSwitcher() {
const { i18n } = useTranslation();
const newLanguage = i18n.language === 'en' ? 'fr' : 'en';
return (

View File

@@ -2,7 +2,7 @@ import { ActionIcon, useMantineColorScheme } from '@mantine/core';
import { TbMoonStars, TbSun } from 'react-icons/tb';
import { makeRequest } from '~/lib/request';
export function MantineThemeSwitcher() {
export function ThemeSwitcher() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const handleThemeChange = () => {
toggleColorScheme();

View File

@@ -3,7 +3,7 @@ import { Avatar, Group, Menu, Text, UnstyledButton } from '@mantine/core';
import { forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import { TbChevronRight } from 'react-icons/tb';
import useUser from '~/hooks/use_user';
import useUser from '~/hooks/use_auth';
interface UserButtonProps extends React.ComponentPropsWithoutRef<'button'> {
image: string;

View File

@@ -20,8 +20,8 @@ import { PiGearLight } from 'react-icons/pi';
import { MantineUserCard } from '~/components/common/user_card';
import { FavoriteList } from '~/components/dashboard/favorite/favorite_list';
import { SearchSpotlight } from '~/components/search/search';
import useUser from '~/hooks/use_auth';
import useShortcut from '~/hooks/use_shortcut';
import useUser from '~/hooks/use_user';
import { appendCollectionId } from '~/lib/navigation';
import { useActiveCollection } from '~/stores/collection_store';
import { useGlobalHotkeysStore } from '~/stores/global_hotkeys_store';

View File

@@ -4,8 +4,8 @@ import { route } from '@izzyjs/route/client';
import { Anchor, Group, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import ExternalLink from '~/components/common/external_link';
import { MantineLanguageSwitcher } from '~/components/common/language_switcher';
import { MantineThemeSwitcher } from '~/components/common/theme_switcher';
import { LocaleSwitcher } from '~/components/common/locale_switcher';
import { ThemeSwitcher } from '~/components/common/theme_switcher';
import packageJson from '../../../package.json';
import classes from './footer.module.css';
@@ -46,8 +46,8 @@ export function MantineFooter() {
</Group>
<Group gap="sm" mt={4} mb={4}>
<MantineThemeSwitcher />
<MantineLanguageSwitcher />
<ThemeSwitcher />
<LocaleSwitcher />
</Group>
<Group gap="xs" justify="flex-end" wrap="nowrap">

View File

@@ -15,9 +15,9 @@ import {
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import ExternalLink from '~/components/common/external_link';
import { MantineLanguageSwitcher } from '~/components/common/language_switcher';
import { MantineThemeSwitcher } from '~/components/common/theme_switcher';
import useUser from '~/hooks/use_user';
import { LocaleSwitcher } from '~/components/common/locale_switcher';
import { ThemeSwitcher } from '~/components/common/theme_switcher';
import useUser from '~/hooks/use_auth';
import classes from './mobile.module.css';
export default function Navbar() {
@@ -47,8 +47,8 @@ export default function Navbar() {
</Group>
<Group gap="xs">
<MantineThemeSwitcher />
<MantineLanguageSwitcher />
<ThemeSwitcher />
<LocaleSwitcher />
{!isAuthenticated ? (
<Button
component="a"

View File

@@ -0,0 +1,25 @@
import { usePage } from '@inertiajs/react';
import type { Auth, InertiaPage } from '~/types/inertia';
export const useAuth = () => usePage<InertiaPage>().props.auth;
export const withAuth = <T extends object>(
Component: React.ComponentType<T & { auth: Auth }>
): React.ComponentType<Omit<T, 'auth'>> => {
return (props: Omit<T, 'auth'>) => {
const auth = useAuth();
return <Component {...(props as T)} auth={auth} />;
};
};
export const withAuthRequired = <T extends object>(
Component: React.ComponentType<T & { auth: Auth }>
): React.ComponentType<Omit<T, 'auth'>> => {
return (props: Omit<T, 'auth'>) => {
const auth = useAuth();
if (!auth.isAuthenticated) {
return null;
}
return <Component {...(props as T)} auth={auth} />;
};
};

View File

@@ -1,5 +0,0 @@
import { usePage } from '@inertiajs/react';
import type { InertiaPage } from '~/types/inertia';
const useUser = () => usePage<InertiaPage>().props.auth;
export default useUser;

View File

@@ -1,7 +1,11 @@
import { router } from '@inertiajs/react';
import { api } from '#adonis/api';
import { PageProps } from '@adonisjs/inertia/types';
import { router, usePage } from '@inertiajs/react';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import '@mantine/core/styles.css';
import '@mantine/spotlight/styles.css';
import { createTuyau } from '@tuyau/client';
import { TuyauProvider } from '@tuyau/inertia/react';
import dayjs from 'dayjs';
import { ReactNode, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,9 +14,15 @@ import '../styles/index.css';
const TRANSITION_IN_CLASS = '__transition_fadeIn';
const TRANSITION_OUT_CLASS = '__transition_fadeOut';
export default function BaseLayout({ children }: { children: ReactNode }) {
export function BaseLayout({ children }: { children: ReactNode }) {
const { i18n } = useTranslation();
dayjs.locale(i18n.language);
const { props } = usePage<PageProps & { appBaseUrl: string }>();
const tuyauClient = createTuyau({
api,
baseUrl: props.appBaseUrl,
});
const findAppElement = () => document.getElementById('app');
@@ -50,9 +60,9 @@ export default function BaseLayout({ children }: { children: ReactNode }) {
}, []);
return (
<>
<TuyauProvider client={tuyauClient}>
<ColorSchemeScript />
<MantineProvider>{children}</MantineProvider>
</>
</TuyauProvider>
);
}

View File

@@ -1,33 +0,0 @@
import { Container } from '@mantine/core';
import { PropsWithChildren } from 'react';
import { MantineFooter } from '~/components/footer/footer';
import Navbar from '~/components/navbar/navbar';
import BaseLayout from '~/layouts/_base_layout';
const ContentLayout = ({ children }: PropsWithChildren) => (
<Container
style={{
minHeight: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<Navbar />
<main
style={{
flex: 1,
}}
>
{children}
</main>
<MantineFooter />
</Container>
);
const LayoutWrapper = ({ children }: PropsWithChildren) => (
<BaseLayout>
<ContentLayout>{children}</ContentLayout>
</BaseLayout>
);
export { LayoutWrapper as ContentLayout };

View File

@@ -1,5 +1,5 @@
import { PropsWithChildren } from 'react';
import BaseLayout from '~/layouts/_base_layout';
import { BaseLayout } from '~/layouts/_base_layout';
const LayoutWrapper = ({ children }: PropsWithChildren) => (
<BaseLayout>{children}</BaseLayout>

View File

@@ -0,0 +1,44 @@
import { Box, rem } from '@mantine/core';
import { PropsWithChildren } from 'react';
import { FloatingNavbar } from '~/components/common/floating_navbar/floating_navbar';
import { Footer } from '~/components/common/footer/footer';
import { BaseLayout } from './_base_layout';
const DefaultLayout = ({ children }: PropsWithChildren) => (
<BaseLayout>
<Layout>{children}</Layout>
</BaseLayout>
);
export default DefaultLayout;
const LAYOUT_WIDTH = '1500px';
const Layout = ({ children }: PropsWithChildren) => (
<>
{/* Top navbar */}
<FloatingNavbar width={LAYOUT_WIDTH} />
{/* Page content */}
<Box
style={{
paddingInline: 'var(--mantine-spacing-lg)',
flex: 1,
}}
>
<Box
style={{
height: '100%',
maxWidth: '100%',
width: LAYOUT_WIDTH,
marginInline: 'auto',
marginBlock: rem(60),
}}
>
{children}
</Box>
</Box>
{/* Footer */}
<Footer />
</>
);

View File

@@ -3,9 +3,9 @@ import { route } from '@izzyjs/route/client';
import { Anchor, Button, Container, Group, rem, Title } from '@mantine/core';
import { FormEvent, PropsWithChildren } from 'react';
import { useTranslation } from 'react-i18next';
import { MantineFooter } from '~/components/footer/footer';
import { Footer } from '~/components/common/footer/footer';
import i18n from '~/i18n';
import BaseLayout from '~/layouts/_base_layout';
import { BaseLayout } from '~/layouts/_base_layout';
import { appendCollectionId } from '~/lib/navigation';
export interface FormLayoutProps extends PropsWithChildren {
@@ -87,7 +87,7 @@ function FormLayout({
{textSubmitButton}
</Button>
</Group>
<MantineFooter />
<Footer />
</form>
</main>
</Container>

View File

@@ -1,15 +1,8 @@
import { ReactNode } from 'react';
import {
UsersTable,
UsersTableProps,
} from '~/components/admin/users/users_table';
import { ContentLayout } from '~/layouts/content_layout';
function AdminDashboardPage(props: UsersTableProps) {
export default function AdminDashboardPage(props: UsersTableProps) {
return <UsersTable {...props} />;
}
AdminDashboardPage.layout = (page: ReactNode) => (
<ContentLayout children={page} />
);
export default AdminDashboardPage;

View File

@@ -7,7 +7,6 @@
font-family:
Greycliff CF,
var(--mantine-font-family);
font-weight: 900;
margin-bottom: var(--mantine-spacing-md);
text-align: center;

View File

@@ -1,11 +1,9 @@
import { Container, Text, Title } from '@mantine/core';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { FeatureList } from '~/components/home/feature_list';
import classes from './home.module.css';
import { ContentLayout } from '~/layouts/content_layout';
function HomePage() {
export default function HomePage() {
const { t } = useTranslation('about');
return (
<Container className={classes.wrapper}>
@@ -21,6 +19,3 @@ function HomePage() {
</Container>
);
}
HomePage.layout = (page: ReactNode) => <ContentLayout children={page} />;
export default HomePage;

View File

@@ -1,8 +1,6 @@
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { ContentLayout } from '~/layouts/content_layout';
function PrivacyPage() {
export default function PrivacyPage() {
const { t } = useTranslation('privacy');
return (
<>
@@ -43,6 +41,3 @@ function PrivacyPage() {
</>
);
}
PrivacyPage.layout = (page: ReactNode) => <ContentLayout children={page} />;
export default PrivacyPage;

View File

@@ -1,8 +1,7 @@
import { Flex, Text } from '@mantine/core';
import { ReactNode, useEffect } from 'react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { LinkList } from '~/components/dashboard/link/list/link_list';
import { ContentLayout } from '~/layouts/content_layout';
import { useCollectionsSetter } from '~/stores/collection_store';
import type { CollectionWithLinks, PublicUser } from '~/types/app';
@@ -10,7 +9,7 @@ interface SharedPageProps {
collection: CollectionWithLinks & { author: PublicUser };
}
function SharedPage({ collection }: SharedPageProps) {
export default function SharedPage({ collection }: SharedPageProps) {
const { t } = useTranslation('common');
const { setActiveCollection } = useCollectionsSetter();
@@ -43,6 +42,3 @@ function SharedPage({ collection }: SharedPageProps) {
</>
);
}
SharedPage.layout = (page: ReactNode) => <ContentLayout>{page}</ContentLayout>;
export default SharedPage;

View File

@@ -1,10 +1,8 @@
import { Link } from '@inertiajs/react';
import { route } from '@izzyjs/route/client';
import { ReactNode } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { ContentLayout } from '~/layouts/content_layout';
function TermsPage() {
export default function TermsPage() {
const { t } = useTranslation('terms');
return (
<>
@@ -52,6 +50,3 @@ function TermsPage() {
</>
);
}
TermsPage.layout = (page: ReactNode) => <ContentLayout children={page} />;
export default TermsPage;

View File

@@ -3,11 +3,26 @@
--ml-bg-dark: rgb(34, 40, 49);
}
html,
body {
min-height: 100svh;
width: 100%;
background-color: light-dark(var(--ml-bg-light), var(--ml-bg-dark));
/* Fix nprogress position */
#nprogress {
position: relative;
z-index: 9999999;
}
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* For light mode */
:root[data-mantine-color-scheme='light'] {
--mantine-color-body: var(--ml-bg-light) !important;
}
/* For dark mode */
:root[data-mantine-color-scheme='dark'] {
--mantine-color-body: var(--ml-bg-dark) !important;
}
.__transition_fadeIn {

View File

@@ -22,6 +22,7 @@
"#adonis/api": "./.adonisjs/api.ts",
"#auth/*": "./app/auth/*.js",
"#collections/*": "./app/collections/*.js",
"#config/*": "./config/*.js",
"#core/*": "./app/core/*.js",
"#favicons/*": "./app/favicons/*.js",
"#home/*": "./app/home/*.js",
@@ -34,8 +35,7 @@
"#database/*": "./database/*.js",
"#tests/*": "./tests/*.js",
"#shared/*": "./shared/*.js",
"#start/*": "./start/*.js",
"#config/*": "./config/*.js"
"#start/*": "./start/*.js"
},
"devDependencies": {
"@adonisjs/assembler": "^7.8.2",
@@ -90,6 +90,7 @@
"@vinejs/vine": "^3.0.1",
"@vitejs/plugin-react-oxc": "^0.3.0",
"bentocache": "^1.5.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"edge.js": "^6.3.0",
"i18next": "^25.3.2",

3
pnpm-lock.yaml generated
View File

@@ -74,6 +74,9 @@ importers:
bentocache:
specifier: ^1.5.0
version: 1.5.0(knex@3.1.0(pg@8.16.3))
clsx:
specifier: ^2.1.1
version: 2.1.1
dayjs:
specifier: ^1.11.13
version: 1.11.13

View File

@@ -1,4 +1,4 @@
import project from '#config/project';
import { APP_COLOR, PROJECT_DESCRIPTION, PROJECT_NAME } from '#config/project';
import { getDirname } from '@adonisjs/core/helpers';
import inertia from '@adonisjs/inertia/client';
import adonisjs from '@adonisjs/vite/client';
@@ -16,11 +16,11 @@ export default defineConfig({
enabled: true,
},
manifest: {
name: project.name,
short_name: project.name,
description: project.description,
theme_color: project.color,
background_color: project.color,
name: PROJECT_NAME,
short_name: PROJECT_NAME,
description: PROJECT_DESCRIPTION,
theme_color: APP_COLOR,
background_color: APP_COLOR,
scope: '/',
display: 'standalone',
orientation: 'portrait',