mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 23:15:36 +00:00
feat: create new dashboard (layout, navbar, list of links)
This commit is contained in:
26
inertia/components/common/external_link_styled.tsx
Normal file
26
inertia/components/common/external_link_styled.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
60
inertia/components/common/mantine_user_card.tsx
Normal file
60
inertia/components/common/mantine_user_card.tsx
Normal 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>
|
||||
)
|
||||
);
|
||||
}
|
||||
44
inertia/components/dashboard/link/link.module.css
Normal file
44
inertia/components/dashboard/link/link.module.css
Normal 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;
|
||||
}
|
||||
26
inertia/components/dashboard/link/link_favicon.module.css
Normal file
26
inertia/components/dashboard/link/link_favicon.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user