diff --git a/config/project.ts b/config/project.ts index ab948a3..4ae94eb 100644 --- a/config/project.ts +++ b/config/project.ts @@ -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'; diff --git a/inertia/app/app.tsx b/inertia/app/app.tsx index 744c577..f0e47c8 100644 --- a/inertia/app/app.tsx +++ b/inertia/app/app.tsx @@ -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) => ); + + return currentPage; }, setup({ el, App, props }) { diff --git a/inertia/app/ssr.tsx b/inertia/app/ssr.tsx index 1a90544..907e5ee 100644 --- a/inertia/app/ssr.tsx +++ b/inertia/app/ssr.tsx @@ -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) => ); + return pageComponent; }, setup: ({ App, props }) => , }); diff --git a/inertia/components/common/floating_navbar/floating_navbar.module.css b/inertia/components/common/floating_navbar/floating_navbar.module.css new file mode 100644 index 0000000..0e0c2b8 --- /dev/null +++ b/inertia/components/common/floating_navbar/floating_navbar.module.css @@ -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; +} diff --git a/inertia/components/common/floating_navbar/floating_navbar.tsx b/inertia/components/common/floating_navbar/floating_navbar.tsx new file mode 100644 index 0000000..dbc6258 --- /dev/null +++ b/inertia/components/common/floating_navbar/floating_navbar.tsx @@ -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 = ( + <> + + Dashboard + + + Github + + + Extension + + + ); + + return ( + <> + + + + + MyLinks's logo + + + + {!isMobile && {links}} + {isMobile && } + {auth.isAuthenticated && } + {!auth.isAuthenticated && ( + + )} + + + + {/* Mobile drawer */} + + + {links} + + + + + ); +} diff --git a/inertia/components/common/floating_navbar/user_dropdown.module.css b/inertia/components/common/floating_navbar/user_dropdown.module.css new file mode 100644 index 0000000..0f2b469 --- /dev/null +++ b/inertia/components/common/floating_navbar/user_dropdown.module.css @@ -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) + ); +} diff --git a/inertia/components/common/floating_navbar/user_dropdown.tsx b/inertia/components/common/floating_navbar/user_dropdown.tsx new file mode 100644 index 0000000..f7847ec --- /dev/null +++ b/inertia/components/common/floating_navbar/user_dropdown.tsx @@ -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 ( + + + + + + + {auth.user?.fullname} + + + + + + + {t('common:user')} + } + component={InternalLinkUnstyled} + href={`/user/${auth.user?.fullname}`} + color="inherit" + > + {t('common:profile')} + + + {auth.user?.isAdmin && ( + <> + {t('common:admin')} + {t('common:admin')} + + )} + + {t('common:settings')} + } + component={InternalLinkUnstyled} + href="/auth/logout" + color="inherit" + > + {t('common:logout')} + + + + ); +} diff --git a/inertia/components/common/footer/footer.module.css b/inertia/components/common/footer/footer.module.css new file mode 100644 index 0000000..066e6c8 --- /dev/null +++ b/inertia/components/common/footer/footer.module.css @@ -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; +} diff --git a/inertia/components/common/footer/footer.tsx b/inertia/components/common/footer/footer.tsx new file mode 100644 index 0000000..647fbe5 --- /dev/null +++ b/inertia/components/common/footer/footer.tsx @@ -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 = () => ( + + + + Made with ❤️ by{' '} + + {AUTHOR_NAME} + + + • + + + {packageJson.version} + + + • + + + + + + +); diff --git a/inertia/components/common/links/external_link_styled.tsx b/inertia/components/common/links/external_link_styled.tsx new file mode 100644 index 0000000..dd260ae --- /dev/null +++ b/inertia/components/common/links/external_link_styled.tsx @@ -0,0 +1,28 @@ +import { Anchor } from '@mantine/core'; +import { AnchorHTMLAttributes, CSSProperties, ReactNode } from 'react'; + +interface ExternalLinkStyledProps + extends AnchorHTMLAttributes { + children: ReactNode; + style?: CSSProperties; + title?: string; + className?: string; +} + +export const ExternalLinkStyled = ({ + children, + title, + href, + ...props +}: ExternalLinkStyledProps) => ( + + component="a" + target="_blank" + rel="noreferrer" + title={title} + href={href} + {...props} + > + {children} + +); diff --git a/inertia/components/common/links/external_link_unstyled.tsx b/inertia/components/common/links/external_link_unstyled.tsx new file mode 100644 index 0000000..b4b1a52 --- /dev/null +++ b/inertia/components/common/links/external_link_unstyled.tsx @@ -0,0 +1,26 @@ +import { Anchor, CSSProperties } from '@mantine/core'; +import { AnchorHTMLAttributes, ReactNode } from 'react'; + +interface ExternalLinkUnstyledProps + extends AnchorHTMLAttributes { + children: ReactNode; + style?: CSSProperties; + title?: string; + className?: string; + newTab?: boolean; +} +export const ExternalLinkUnstyled = ({ + children, + newTab = true, + ...props +}: ExternalLinkUnstyledProps) => ( + + {children} + +); diff --git a/inertia/components/common/links/internal_link.tsx b/inertia/components/common/links/internal_link.tsx new file mode 100644 index 0000000..1a6ce18 --- /dev/null +++ b/inertia/components/common/links/internal_link.tsx @@ -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) => void; + route?: ApiRouteName; + href?: string; + forceRefresh?: boolean; + style?: CSSProperties; + className?: string; + params?: Record; +} + +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 ( + + component={forceRefresh ? 'a' : Link} + href={url} + style={style} + onClick={onClick} + className={className} + > + {children} + + ); +}; diff --git a/inertia/components/common/links/internal_link_unstyled.tsx b/inertia/components/common/links/internal_link_unstyled.tsx new file mode 100644 index 0000000..755d556 --- /dev/null +++ b/inertia/components/common/links/internal_link_unstyled.tsx @@ -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) => void; + route?: ApiRouteName; + href?: string; + forceRefresh?: boolean; + style?: CSSProperties; + className?: string; + params?: Record; +} + +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 ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; diff --git a/inertia/components/common/language_switcher.tsx b/inertia/components/common/locale_switcher.tsx similarity index 92% rename from inertia/components/common/language_switcher.tsx rename to inertia/components/common/locale_switcher.tsx index 49799e0..06a5459 100644 --- a/inertia/components/common/language_switcher.tsx +++ b/inertia/components/common/locale_switcher.tsx @@ -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 ( diff --git a/inertia/components/common/theme_switcher.tsx b/inertia/components/common/theme_switcher.tsx index 5e515a1..22f4d43 100644 --- a/inertia/components/common/theme_switcher.tsx +++ b/inertia/components/common/theme_switcher.tsx @@ -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(); diff --git a/inertia/components/common/user_card.tsx b/inertia/components/common/user_card.tsx index 920fe90..7318423 100644 --- a/inertia/components/common/user_card.tsx +++ b/inertia/components/common/user_card.tsx @@ -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; diff --git a/inertia/components/dashboard/dashboard_navbar.tsx b/inertia/components/dashboard/dashboard_navbar.tsx index 630ac71..370805c 100644 --- a/inertia/components/dashboard/dashboard_navbar.tsx +++ b/inertia/components/dashboard/dashboard_navbar.tsx @@ -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'; diff --git a/inertia/components/footer/footer.tsx b/inertia/components/footer/footer.tsx index 2deffc0..fb14127 100644 --- a/inertia/components/footer/footer.tsx +++ b/inertia/components/footer/footer.tsx @@ -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() { - - + + diff --git a/inertia/components/navbar/navbar.tsx b/inertia/components/navbar/navbar.tsx index b488e27..c5f2697 100644 --- a/inertia/components/navbar/navbar.tsx +++ b/inertia/components/navbar/navbar.tsx @@ -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() { - - + + {!isAuthenticated ? ( - +