feat: create new dashboard (layout, navbar, list of links)

This commit is contained in:
Sonny
2024-11-02 02:51:48 +01:00
committed by Sonny
parent 907dda300e
commit 757788bf9b
12 changed files with 404 additions and 128 deletions

View File

@@ -0,0 +1,26 @@
import { Anchor } from '@mantine/core';
import { AnchorHTMLAttributes, CSSProperties, ReactNode } from 'react';
export function ExternalLinkStyled({
children,
title,
...props
}: AnchorHTMLAttributes<HTMLAnchorElement> & {
children: ReactNode;
style?: CSSProperties;
title?: string;
className?: string;
}) {
return (
<Anchor<'a'>
component="a"
underline="never"
target="_blank"
rel="noreferrer"
title={title}
{...props}
>
{children}
</Anchor>
);
}

View File

@@ -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<HTMLButtonElement, UserButtonProps>(
({ image, name, email, icon, ...others }: UserButtonProps, ref) => (
<UnstyledButton
ref={ref}
style={{
color: 'var(--mantine-color-text)',
borderRadius: 'var(--mantine-radius-sm)',
}}
{...others}
>
<Group>
<Avatar src={image} radius="xl" />
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
{name}
</Text>
<Text c="dimmed" size="xs">
{email}
</Text>
</div>
{icon || <TbChevronRight size="1rem" />}
</Group>
</UnstyledButton>
)
);
export function MantineUserCard() {
const { user, isAuthenticated } = useUser();
return (
isAuthenticated && (
<Menu withArrow>
<Menu.Target>
<UserButton
image={user.avatarUrl}
name={user.fullname}
email={user.email}
/>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item>Logout</Menu.Item>
</Menu.Dropdown>
</Menu>
)
);
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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<HTMLImageElement>(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 (
<Favicon>
<div className={styles.favicon}>
{!isFailed ? (
<img
src={`/favicon?url=${url}`}
@@ -73,10 +50,13 @@ export default function LinkFavicon({ url, size = 32 }: LinkFaviconProps) {
<TfiWorld size={size} />
)}
{isLoading && (
<FaviconLoader style={{ height: `${size}px`, width: `${size}px` }}>
<TbLoader3 size={size} />
</FaviconLoader>
<Center
className={styles.faviconLoader}
style={{ height: `${size}px`, width: `${size}px` }}
>
<Loader size="xs" />
</Center>
)}
</Favicon>
</div>
);
}

View File

@@ -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 (
<LinkWrapper key={id} title={name}>
<LinkHeader>
<Card
className={styles.linkWrapper}
padding="xs"
pl="md"
pr="md"
radius="sm"
>
<Group className={styles.linkHeader} justify="space-between">
<LinkFavicon url={url} />
<ExternalLink href={url} className="reset">
<LinkName>
{name} {showUserControls && favorite && <StarIcon />}
</LinkName>
<ExternalLinkStyled href={url} style={{ flex: 1 }}>
<div className={styles.linkName}>
<Text c="blue" lineClamp={1}>
{name}{' '}
{showUserControls && favorite && <AiFillStar color="gold" />}
</Text>
</div>
<LinkItemURL url={url} />
</ExternalLink>
{showUserControls && <LinkControls link={link} />}
</LinkHeader>
{description && <LinkDescription>{description}</LinkDescription>}
</LinkWrapper>
</ExternalLinkStyled>
{/* {showUserControls && <LinkControls link={link} />} */}
</Group>
{description && (
<Text c="dimmed" size="sm">
{description}
</Text>
)}
</Card>
);
}
@@ -114,13 +60,17 @@ function LinkItemURL({ url }: { url: Link['url'] }) {
}
return (
<LinkUrl>
<Text className={styles.linkUrl} c="gray" size="xs" lineClamp={1}>
{origin}
<LinkUrlPathname>{text}</LinkUrlPathname>
</LinkUrl>
<span className={styles.linkUrlPathname}>{text}</span>
</Text>
);
} catch (error) {
console.error('error', error);
return <LinkUrl>{url}</LinkUrl>;
return (
<Text className={styles.linkUrl} c="gray" size="xs" lineClamp={1}>
{url}
</Text>
);
}
}