mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-10 15:35:35 +00:00
refactor: use tabs instead of spaces
This commit is contained in:
@@ -11,27 +11,27 @@ import { appendLinkId } from '~/lib/navigation';
|
||||
import { Link } from '~/types/app';
|
||||
|
||||
export default function LinkControls({ link }: { link: Link }) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('common');
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
label={<BsThreeDotsVertical css={{ color: theme.colors.grey }} />}
|
||||
css={{ backgroundColor: theme.colors.secondary }}
|
||||
svgSize={18}
|
||||
>
|
||||
<FavoriteDropdownItem link={link} />
|
||||
<DropdownItemLink
|
||||
href={appendLinkId(route('link.edit-form').url, link.id)}
|
||||
>
|
||||
<GoPencil /> {t('link.edit')}
|
||||
</DropdownItemLink>
|
||||
<DropdownItemLink
|
||||
href={appendLinkId(route('link.delete-form').url, link.id)}
|
||||
danger
|
||||
>
|
||||
<IoTrashOutline /> {t('link.delete')}
|
||||
</DropdownItemLink>
|
||||
</Dropdown>
|
||||
);
|
||||
return (
|
||||
<Dropdown
|
||||
label={<BsThreeDotsVertical css={{ color: theme.colors.grey }} />}
|
||||
css={{ backgroundColor: theme.colors.secondary }}
|
||||
svgSize={18}
|
||||
>
|
||||
<FavoriteDropdownItem link={link} />
|
||||
<DropdownItemLink
|
||||
href={appendLinkId(route('link.edit-form').url, link.id)}
|
||||
>
|
||||
<GoPencil /> {t('link.edit')}
|
||||
</DropdownItemLink>
|
||||
<DropdownItemLink
|
||||
href={appendLinkId(route('link.delete-form').url, link.id)}
|
||||
danger
|
||||
>
|
||||
<IoTrashOutline /> {t('link.delete')}
|
||||
</DropdownItemLink>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,76 +7,76 @@ import { rotate } from '~/styles/keyframes';
|
||||
const IMG_LOAD_TIMEOUT = 7_500;
|
||||
|
||||
interface LinkFaviconProps {
|
||||
url: string;
|
||||
size?: number;
|
||||
url: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Favicon = styled.div({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
color: theme.colors.font,
|
||||
backgroundColor: theme.colors.secondary,
|
||||
|
||||
'& > *': {
|
||||
animation: `${rotate} 1s both reverse infinite linear`,
|
||||
},
|
||||
'& > *': {
|
||||
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);
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const [isFailed, setFailed] = useState<boolean>(false);
|
||||
const [isLoading, setLoading] = useState<boolean>(true);
|
||||
const [isFailed, setFailed] = useState<boolean>(false);
|
||||
const [isLoading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const setFallbackFavicon = () => setFailed(true);
|
||||
const handleStopLoading = () => setLoading(false);
|
||||
const setFallbackFavicon = () => setFailed(true);
|
||||
const handleStopLoading = () => setLoading(false);
|
||||
|
||||
const handleErrorLoading = () => {
|
||||
setFallbackFavicon();
|
||||
handleStopLoading();
|
||||
};
|
||||
const handleErrorLoading = () => {
|
||||
setFallbackFavicon();
|
||||
handleStopLoading();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Ugly hack, onLoad cb not triggered on first load when SSR
|
||||
if (imgRef.current?.complete) {
|
||||
handleStopLoading();
|
||||
return;
|
||||
}
|
||||
const id = setTimeout(() => handleErrorLoading(), IMG_LOAD_TIMEOUT);
|
||||
return () => clearTimeout(id);
|
||||
}, [isLoading]);
|
||||
useEffect(() => {
|
||||
// Ugly hack, onLoad cb not triggered on first load when SSR
|
||||
if (imgRef.current?.complete) {
|
||||
handleStopLoading();
|
||||
return;
|
||||
}
|
||||
const id = setTimeout(() => handleErrorLoading(), IMG_LOAD_TIMEOUT);
|
||||
return () => clearTimeout(id);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<Favicon>
|
||||
{!isFailed ? (
|
||||
<img
|
||||
src={`/favicon?url=${url}`}
|
||||
onError={handleErrorLoading}
|
||||
onLoad={handleStopLoading}
|
||||
height={size}
|
||||
width={size}
|
||||
alt="icon"
|
||||
ref={imgRef}
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<TfiWorld size={size} />
|
||||
)}
|
||||
{isLoading && (
|
||||
<FaviconLoader style={{ height: `${size}px`, width: `${size}px` }}>
|
||||
<TbLoader3 size={size} />
|
||||
</FaviconLoader>
|
||||
)}
|
||||
</Favicon>
|
||||
);
|
||||
return (
|
||||
<Favicon>
|
||||
{!isFailed ? (
|
||||
<img
|
||||
src={`/favicon?url=${url}`}
|
||||
onError={handleErrorLoading}
|
||||
onLoad={handleStopLoading}
|
||||
height={size}
|
||||
width={size}
|
||||
alt="icon"
|
||||
ref={imgRef}
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<TfiWorld size={size} />
|
||||
)}
|
||||
{isLoading && (
|
||||
<FaviconLoader style={{ height: `${size}px`, width: `${size}px` }}>
|
||||
<TbLoader3 size={size} />
|
||||
</FaviconLoader>
|
||||
)}
|
||||
</Favicon>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,121 +6,121 @@ 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,
|
||||
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',
|
||||
},
|
||||
'&:hover': {
|
||||
outlineWidth: '1px',
|
||||
outlineStyle: 'solid',
|
||||
},
|
||||
}));
|
||||
|
||||
const LinkHeader = styled.div(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: '1em',
|
||||
alignItems: 'center',
|
||||
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,
|
||||
'& > 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,
|
||||
},
|
||||
},
|
||||
'&, &:hover': {
|
||||
border: 0,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const LinkName = styled.div({
|
||||
width: '100%',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
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',
|
||||
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',
|
||||
width: '100%',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
color: theme.colors.grey,
|
||||
fontSize: '0.8em',
|
||||
}));
|
||||
|
||||
const StarIcon = styled(AiFillStar)(({ theme }) => ({
|
||||
color: theme.colors.yellow,
|
||||
color: theme.colors.yellow,
|
||||
}));
|
||||
|
||||
const LinkUrlPathname = styled.span({
|
||||
opacity: 0,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
export default function LinkItem({
|
||||
link,
|
||||
showUserControls = false,
|
||||
link,
|
||||
showUserControls = false,
|
||||
}: {
|
||||
link: Link;
|
||||
showUserControls: boolean;
|
||||
link: Link;
|
||||
showUserControls: boolean;
|
||||
}) {
|
||||
const { id, name, url, description, favorite } = link;
|
||||
return (
|
||||
<LinkWrapper key={id} title={name}>
|
||||
<LinkHeader>
|
||||
<LinkFavicon url={url} />
|
||||
<ExternalLink href={url} className="reset">
|
||||
<LinkName>
|
||||
{name} {showUserControls && favorite && <StarIcon />}
|
||||
</LinkName>
|
||||
<LinkItemURL url={url} />
|
||||
</ExternalLink>
|
||||
{showUserControls && <LinkControls link={link} />}
|
||||
</LinkHeader>
|
||||
{description && <LinkDescription>{description}</LinkDescription>}
|
||||
</LinkWrapper>
|
||||
);
|
||||
const { id, name, url, description, favorite } = link;
|
||||
return (
|
||||
<LinkWrapper key={id} title={name}>
|
||||
<LinkHeader>
|
||||
<LinkFavicon url={url} />
|
||||
<ExternalLink href={url} className="reset">
|
||||
<LinkName>
|
||||
{name} {showUserControls && favorite && <StarIcon />}
|
||||
</LinkName>
|
||||
<LinkItemURL url={url} />
|
||||
</ExternalLink>
|
||||
{showUserControls && <LinkControls link={link} />}
|
||||
</LinkHeader>
|
||||
{description && <LinkDescription>{description}</LinkDescription>}
|
||||
</LinkWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkItemURL({ url }: { url: Link['url'] }) {
|
||||
try {
|
||||
const { origin, pathname, search } = new URL(url);
|
||||
let text = '';
|
||||
try {
|
||||
const { origin, pathname, search } = new URL(url);
|
||||
let text = '';
|
||||
|
||||
if (pathname !== '/') {
|
||||
text += pathname;
|
||||
}
|
||||
if (pathname !== '/') {
|
||||
text += pathname;
|
||||
}
|
||||
|
||||
if (search !== '') {
|
||||
if (text === '') {
|
||||
text += '/';
|
||||
}
|
||||
text += search;
|
||||
}
|
||||
if (search !== '') {
|
||||
if (text === '') {
|
||||
text += '/';
|
||||
}
|
||||
text += search;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkUrl>
|
||||
{origin}
|
||||
<LinkUrlPathname>{text}</LinkUrlPathname>
|
||||
</LinkUrl>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
return <LinkUrl>{url}</LinkUrl>;
|
||||
}
|
||||
return (
|
||||
<LinkUrl>
|
||||
{origin}
|
||||
<LinkUrlPathname>{text}</LinkUrlPathname>
|
||||
</LinkUrl>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
return <LinkUrl>{url}</LinkUrl>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,34 +5,34 @@ import { sortByCreationDate } from '~/lib/array';
|
||||
import { Link } from '~/types/app';
|
||||
|
||||
const LinkListStyle = styled.ul({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
gap: '0.5em',
|
||||
padding: '3px',
|
||||
flexDirection: 'column',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
gap: '0.5em',
|
||||
padding: '3px',
|
||||
flexDirection: 'column',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
});
|
||||
|
||||
export default function LinkList({
|
||||
links,
|
||||
showControls = true,
|
||||
links,
|
||||
showControls = true,
|
||||
}: {
|
||||
links: Link[];
|
||||
showControls?: boolean;
|
||||
links: Link[];
|
||||
showControls?: boolean;
|
||||
}) {
|
||||
if (links.length === 0) {
|
||||
return <NoLink />;
|
||||
}
|
||||
if (links.length === 0) {
|
||||
return <NoLink />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkListStyle>
|
||||
{sortByCreationDate(links).map((link) => (
|
||||
<LinkItem link={link} key={link.id} showUserControls={showControls} />
|
||||
))}
|
||||
</LinkListStyle>
|
||||
);
|
||||
return (
|
||||
<LinkListStyle>
|
||||
{sortByCreationDate(links).map((link) => (
|
||||
<LinkItem link={link} key={link.id} showUserControls={showControls} />
|
||||
))}
|
||||
</LinkListStyle>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,60 +7,60 @@ import { appendCollectionId } from '~/lib/navigation';
|
||||
import { fadeIn } from '~/styles/keyframes';
|
||||
|
||||
const NoCollectionStyle = styled.div({
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
animation: `${fadeIn} 0.3s both`,
|
||||
minWidth: 0,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
animation: `${fadeIn} 0.3s both`,
|
||||
});
|
||||
|
||||
const Text = styled.p({
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export function NoCollection() {
|
||||
const { t } = useTranslation('home');
|
||||
return (
|
||||
<NoCollectionStyle>
|
||||
<Text>{t('select-collection')}</Text>
|
||||
<Link href={route('collection.create-form').url}>
|
||||
{t('or-create-one')}
|
||||
</Link>
|
||||
</NoCollectionStyle>
|
||||
);
|
||||
const { t } = useTranslation('home');
|
||||
return (
|
||||
<NoCollectionStyle>
|
||||
<Text>{t('select-collection')}</Text>
|
||||
<Link href={route('collection.create-form').url}>
|
||||
{t('or-create-one')}
|
||||
</Link>
|
||||
</NoCollectionStyle>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoLink() {
|
||||
const { t } = useTranslation('common');
|
||||
const { activeCollection } = useActiveCollection();
|
||||
return (
|
||||
<NoCollectionStyle>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t(
|
||||
'home:no-link',
|
||||
{ name: activeCollection?.name ?? '' } as any,
|
||||
{
|
||||
interpolation: { escapeValue: false },
|
||||
}
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Link
|
||||
href={appendCollectionId(
|
||||
route('link.create-form').url,
|
||||
activeCollection?.id
|
||||
)}
|
||||
>
|
||||
{t('link.create')}
|
||||
</Link>
|
||||
</NoCollectionStyle>
|
||||
);
|
||||
const { t } = useTranslation('common');
|
||||
const { activeCollection } = useActiveCollection();
|
||||
return (
|
||||
<NoCollectionStyle>
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t(
|
||||
'home:no-link',
|
||||
{ name: activeCollection?.name ?? '' } as any,
|
||||
{
|
||||
interpolation: { escapeValue: false },
|
||||
}
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Link
|
||||
href={appendCollectionId(
|
||||
route('link.create-form').url,
|
||||
activeCollection?.id
|
||||
)}
|
||||
>
|
||||
{t('link.create')}
|
||||
</Link>
|
||||
</NoCollectionStyle>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user