diff --git a/app/controllers/collections_controller.ts b/app/controllers/collections_controller.ts index dfe6705..ff38fad 100644 --- a/app/controllers/collections_controller.ts +++ b/app/controllers/collections_controller.ts @@ -26,7 +26,7 @@ export default class CollectionsController { } // TODO: Create DTOs - return inertia.render('dashboard', { + return inertia.render('mantine_dashboard', { collections: collections.map((collection) => collection.serialize()), activeCollection: activeCollection?.serialize() || collections[0].serialize(), diff --git a/inertia/components/common/external_link_styled.tsx b/inertia/components/common/external_link_styled.tsx new file mode 100644 index 0000000..fe452ef --- /dev/null +++ b/inertia/components/common/external_link_styled.tsx @@ -0,0 +1,26 @@ +import { Anchor } from '@mantine/core'; +import { AnchorHTMLAttributes, CSSProperties, ReactNode } from 'react'; + +export function ExternalLinkStyled({ + children, + title, + ...props +}: AnchorHTMLAttributes & { + children: ReactNode; + style?: CSSProperties; + title?: string; + className?: string; +}) { + return ( + + component="a" + underline="never" + target="_blank" + rel="noreferrer" + title={title} + {...props} + > + {children} + + ); +} diff --git a/inertia/components/common/mantine_user_card.tsx b/inertia/components/common/mantine_user_card.tsx new file mode 100644 index 0000000..9282cb8 --- /dev/null +++ b/inertia/components/common/mantine_user_card.tsx @@ -0,0 +1,60 @@ +import { Avatar, Group, Menu, Text, UnstyledButton } from '@mantine/core'; +import { forwardRef } from 'react'; +import { TbChevronRight } from 'react-icons/tb'; +import useUser from '~/hooks/use_user'; + +interface UserButtonProps extends React.ComponentPropsWithoutRef<'button'> { + image: string; + name: string; + email: string; + icon?: React.ReactNode; +} + +const UserButton = forwardRef( + ({ image, name, email, icon, ...others }: UserButtonProps, ref) => ( + + + + +
+ + {name} + + + + {email} + +
+ + {icon || } +
+
+ ) +); + +export function MantineUserCard() { + const { user, isAuthenticated } = useUser(); + return ( + isAuthenticated && ( + + + + + + Logout + + + ) + ); +} diff --git a/inertia/components/dashboard/link/link.module.css b/inertia/components/dashboard/link/link.module.css new file mode 100644 index 0000000..00382c4 --- /dev/null +++ b/inertia/components/dashboard/link/link.module.css @@ -0,0 +1,44 @@ +.linkWrapper { + user-select: none; + cursor: pointer; + width: 100%; + background-color: light-dark(--mantine-color-gray-1, rgb(50, 58, 71)); + padding: 0.75em 1em; + border-radius: var(--border-radius); + border: 1px solid transparent; +} + +.linkWrapper:hover { + border: 1px solid var(--mantine-color-blue-4); +} + +.linkHeader { + display: flex; + gap: 1em; + align-items: center; +} + +.linkName { + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.linkDescription { + margin-top: 0.5em; + font-size: 0.8em; + word-wrap: break-word; +} + +.linkUrl { + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + font-size: 0.8em; +} + +.linkUrlPathname { + opacity: 0; +} diff --git a/inertia/components/dashboard/link/link_favicon.module.css b/inertia/components/dashboard/link/link_favicon.module.css new file mode 100644 index 0000000..67c6757 --- /dev/null +++ b/inertia/components/dashboard/link/link_favicon.module.css @@ -0,0 +1,26 @@ +.favicon { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.faviconLoader { + position: absolute; + top: 0; + left: 0; + background-color: var(--secondary-color); +} + +.faviconLoader > * { + animation: rotate 1s both reverse infinite linear; +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/inertia/components/dashboard/link/link_favicon.tsx b/inertia/components/dashboard/link/link_favicon.tsx index cb58c51..da4efbc 100644 --- a/inertia/components/dashboard/link/link_favicon.tsx +++ b/inertia/components/dashboard/link/link_favicon.tsx @@ -1,8 +1,7 @@ -import styled from '@emotion/styled'; +import { Center, Loader } from '@mantine/core'; import { useEffect, useRef, useState } from 'react'; -import { TbLoader3 } from 'react-icons/tb'; import { TfiWorld } from 'react-icons/tfi'; -import { rotate } from '~/styles/keyframes'; +import styles from './link_favicon.module.css'; const IMG_LOAD_TIMEOUT = 7_500; @@ -11,27 +10,6 @@ interface LinkFaviconProps { size?: number; } -const Favicon = styled.div({ - position: 'relative', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}); - -const FaviconLoader = styled.div(({ theme }) => ({ - position: 'absolute', - top: 0, - left: 0, - color: theme.colors.font, - backgroundColor: theme.colors.secondary, - - '& > *': { - animation: `${rotate} 1s both reverse infinite linear`, - }, -})); - -// The Favicon API should always return an image, so it's not really useful to keep the loader nor placeholder icon, -// but for slow connections and other random stuff, I'll keep this export default function LinkFavicon({ url, size = 32 }: LinkFaviconProps) { const imgRef = useRef(null); @@ -47,7 +25,6 @@ export default function LinkFavicon({ url, size = 32 }: LinkFaviconProps) { }; useEffect(() => { - // Ugly hack, onLoad cb not triggered on first load when SSR if (imgRef.current?.complete) { handleStopLoading(); return; @@ -57,7 +34,7 @@ export default function LinkFavicon({ url, size = 32 }: LinkFaviconProps) { }, [isLoading]); return ( - +
{!isFailed ? ( )} {isLoading && ( - - - +
+ +
)} - +
); } diff --git a/inertia/components/dashboard/link/link_item.tsx b/inertia/components/dashboard/link/link_item.tsx index c732d87..c873c79 100644 --- a/inertia/components/dashboard/link/link_item.tsx +++ b/inertia/components/dashboard/link/link_item.tsx @@ -1,76 +1,9 @@ -import styled from '@emotion/styled'; +import { Card, Group, Text } from '@mantine/core'; // Import de Mantine import { AiFillStar } from 'react-icons/ai'; -import ExternalLink from '~/components/common/external_link'; -import LinkControls from '~/components/dashboard/link/link_controls'; +import { ExternalLinkStyled } from '~/components/common/external_link_styled'; import LinkFavicon from '~/components/dashboard/link/link_favicon'; import { Link } from '~/types/app'; - -const LinkWrapper = styled.li(({ theme }) => ({ - userSelect: 'none', - cursor: 'pointer', - height: 'fit-content', - width: '100%', - color: theme.colors.primary, - backgroundColor: theme.colors.secondary, - padding: '0.75em 1em', - borderRadius: theme.border.radius, - - '&:hover': { - outlineWidth: '1px', - outlineStyle: 'solid', - }, -})); - -const LinkHeader = styled.div(({ theme }) => ({ - display: 'flex', - gap: '1em', - alignItems: 'center', - - '& > a': { - height: '100%', - maxWidth: 'calc(100% - 75px)', // TODO: fix this, it is ugly af :( - textDecoration: 'none', - display: 'flex', - flex: 1, - flexDirection: 'column', - transition: theme.transition.delay, - - '&, &:hover': { - border: 0, - }, - }, -})); - -const LinkName = styled.div({ - width: '100%', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', -}); - -const LinkDescription = styled.div(({ theme }) => ({ - marginTop: '0.5em', - color: theme.colors.font, - fontSize: '0.8em', - wordWrap: 'break-word', -})); - -const LinkUrl = styled.span(({ theme }) => ({ - width: '100%', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - color: theme.colors.grey, - fontSize: '0.8em', -})); - -const StarIcon = styled(AiFillStar)(({ theme }) => ({ - color: theme.colors.yellow, -})); - -const LinkUrlPathname = styled.span({ - opacity: 0, -}); +import styles from './link.module.css'; export default function LinkItem({ link, @@ -79,21 +12,34 @@ export default function LinkItem({ link: Link; showUserControls: boolean; }) { - const { id, name, url, description, favorite } = link; + const { name, url, description, favorite } = link; return ( - - + + - - - {name} {showUserControls && favorite && } - + +
+ + {name}{' '} + {showUserControls && favorite && } + +
-
- {showUserControls && } -
- {description && {description}} -
+ + {/* {showUserControls && } */} + + {description && ( + + {description} + + )} + ); } @@ -114,13 +60,17 @@ function LinkItemURL({ url }: { url: Link['url'] }) { } return ( - + {origin} - {text} - + {text} + ); } catch (error) { console.error('error', error); - return {url}; + return ( + + {url} + + ); } } diff --git a/inertia/mantine/components/dashboard/dashboard_navbar.tsx b/inertia/mantine/components/dashboard/dashboard_navbar.tsx new file mode 100644 index 0000000..bc0c111 --- /dev/null +++ b/inertia/mantine/components/dashboard/dashboard_navbar.tsx @@ -0,0 +1,83 @@ +import { Link } from '@inertiajs/react'; +import { route } from '@izzyjs/route/client'; +import { + AppShell, + Burger, + Divider, + Group, + NavLink, + ScrollArea, + Skeleton, + Text, +} from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { AiOutlineFolderAdd } from 'react-icons/ai'; +import { IoIosSearch } from 'react-icons/io'; +import { IoAdd, IoShieldHalfSharp } from 'react-icons/io5'; +import { PiGearLight } from 'react-icons/pi'; +import { MantineUserCard } from '~/components/common/mantine_user_card'; + +interface DashboardNavbarProps { + isOpen: boolean; + toggle: () => void; +} +export function DashboardNavbar({ isOpen, toggle }: DashboardNavbarProps) { + const { t } = useTranslation('common'); + const common = { + variant: 'subtle', + color: 'blue', + active: true, + }; + return ( + + + + Menu + + + + } + color="var(--mantine-color-red-5)" + /> + } + variant="subtle" + /> + } + /> + } + /> + } + /> + + {t('favorite')} • {0} + + + {Array(15) + .fill(0) + .map((_, index) => ( + + ))} + + + ); +} diff --git a/inertia/pages/dashboard.module.css b/inertia/pages/dashboard.module.css new file mode 100644 index 0000000..d40c847 --- /dev/null +++ b/inertia/pages/dashboard.module.css @@ -0,0 +1,3 @@ +.ml_bg_color { + background-color: light-dark(var(--ml-bg-light), var(--ml-bg-dark)); +} diff --git a/inertia/pages/mantine_dashboard.tsx b/inertia/pages/mantine_dashboard.tsx new file mode 100644 index 0000000..aee11d6 --- /dev/null +++ b/inertia/pages/mantine_dashboard.tsx @@ -0,0 +1,100 @@ +import { + AppShell, + Burger, + Group, + ScrollArea, + Stack, + Text, +} from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import DashboardProviders from '~/components/dashboard/dashboard_provider'; +import LinkItem from '~/components/dashboard/link/link_item'; +import { DashboardNavbar } from '~/mantine/components/dashboard/dashboard_navbar'; +import { MantineDashboardLayout } from '~/mantine/layouts/mantine_dashboard_layout'; +import { CollectionWithLinks } from '~/types/app'; +import '../styles/body_overflow_hidden.css'; +import classes from './dashboard.module.css'; + +interface DashboardPageProps { + collections: CollectionWithLinks[]; + activeCollection: CollectionWithLinks; +} + +export default function MantineDashboard(props: Readonly) { + const [openedNavbar, { toggle: toggleNavbar }] = useDisclosure(); + const [openedAside, { toggle: toggleAside }] = useDisclosure(); + + return ( + + + + + + + + Ma super collection + + + + + + + + + {props.activeCollection.links.map((link) => ( + + ))} + + + + + + Aside + + + + Footer + + + + ); +} diff --git a/inertia/styles/body_overflow_hidden.css b/inertia/styles/body_overflow_hidden.css new file mode 100644 index 0000000..d1b79bf --- /dev/null +++ b/inertia/styles/body_overflow_hidden.css @@ -0,0 +1,3 @@ +body { + overflow: hidden; +} diff --git a/inertia/styles/index.css b/inertia/styles/index.css index 402f336..e87781f 100644 --- a/inertia/styles/index.css +++ b/inertia/styles/index.css @@ -1,40 +1,41 @@ +:root { + --ml-bg-light: rgb(240, 238, 246); + --ml-bg-dark: rgb(34, 40, 49); +} + html, body { min-height: 100svh; width: 100%; - background-color: light-dark(var(--mantine-color-white), rgb(34, 40, 49)); + background-color: light-dark(var(--ml-bg-light), var(--ml-bg-dark)); } .__transition_fadeIn { - animation: fadeIn 0.25s ease; - opacity: 1; - scale: 1; + animation: fadeIn 0.15s ease both; } .__transition_fadeOut { - animation: fadeOut 0.25s ease; - opacity: 0; - scale: 0.9; + animation: fadeOut 0.15s ease both; } @keyframes fadeIn { from { opacity: 0; - scale: 0.9; + transform: scale(0.9); } to { opacity: 1; - scale: 1; + transform: none; } } @keyframes fadeOut { from { opacity: 1; - scale: 1; + transform: none; } to { opacity: 0; - scale: 0.9; + transform: scale(0.9); } }