feat: add multiple way to show collections and links

This commit is contained in:
Sonny
2025-08-21 02:27:51 +02:00
parent 18fe979069
commit 4ef2b639b6
41 changed files with 785 additions and 164 deletions

View File

@@ -0,0 +1,29 @@
import { ScrollArea, Stack, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { CollectionFavoriteItem } from '~/components/dashboard/collection/item/collection_favorite_item';
import { CollectionItem } from '~/components/dashboard/collection/item/collection_item';
import { useCollections } from '~/hooks/collections/use_collections';
import { useIsMobile } from '~/hooks/use_is_mobile';
import styles from './list/collection_list.module.css';
export function CollectionList() {
const { t } = useTranslation();
const collections = useCollections();
const isMobile = useIsMobile();
return (
<Stack gap="xs" h="100%" w={isMobile ? '100%' : '350px'}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Text c="dimmed" ml="md" mb="sm">
{t('collection.collections')} {collections.length}
</Text>
<ScrollArea className={styles.collectionList}>
<CollectionFavoriteItem />
{collections.map((collection) => (
<CollectionItem collection={collection} />
))}
</ScrollArea>
</div>
</Stack>
);
}

View File

@@ -0,0 +1,20 @@
import { CollectionListDisplay } from '#shared/types/index';
import { ComboList } from '~/components/common/combo_list/combo_list';
import { useDisplayPreferences } from '~/hooks/use_display_preferences';
import { getCollectionListDisplayOptions } from '~/lib/display_preferences';
export function CollectionListSelector() {
const { displayPreferences, handleUpdateDisplayPreferences } =
useDisplayPreferences();
return (
<ComboList
selectedValue={displayPreferences.collectionListDisplay}
values={getCollectionListDisplayOptions()}
setValue={(value) =>
handleUpdateDisplayPreferences({
collectionListDisplay: value as CollectionListDisplay,
})
}
/>
);
}

View File

@@ -0,0 +1,58 @@
import { router } from '@inertiajs/react';
import { route } from '@izzyjs/route/client';
import { Chip, Group, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useActiveCollection } from '~/hooks/collections/use_active_collection';
import { useCollections } from '~/hooks/collections/use_collections';
import { appendCollectionId } from '~/lib/navigation';
export function InlineCollectionList() {
const { t } = useTranslation();
const collections = useCollections();
const activeCollection = useActiveCollection();
const handleCollectionChange = (value?: string) => {
if (value) {
router.visit(appendCollectionId(route('dashboard').path, Number(value)));
return;
}
router.visit(route('dashboard').path);
};
const fields = [
{
label: t('common:favorite'),
value: 'favorite',
},
...collections.map((c) => ({
label: (
<Group gap="xs" wrap="nowrap">
<>{c.name}</>
<Text size="xs" c="dimmed">
{c.links.length}
</Text>
</Group>
),
value: c.id.toString(),
})),
];
return (
<Group gap="xs" w="100%">
{fields.map((field) => (
<Chip
key={field.value}
checked={
activeCollection?.id
? activeCollection.id === Number(field.value)
: field.value === 'favorite'
}
variant="light"
onClick={() => handleCollectionChange(field.value)}
>
{field.label}
</Chip>
))}
</Group>
);
}

View File

@@ -0,0 +1,29 @@
import { Link } from '@inertiajs/react';
import { route } from '@izzyjs/route/client';
import { Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { TbStar, TbStarFilled } from 'react-icons/tb';
import { useActiveCollection } from '~/hooks/collections/use_active_collection';
import classes from './collection_item.module.css';
export function CollectionFavoriteItem() {
const { t } = useTranslation();
const activeCollection = useActiveCollection();
const isActiveCollection = !activeCollection?.id;
const FolderIcon = isActiveCollection ? TbStarFilled : TbStar;
return (
<Link
className={classes.link}
data-active={isActiveCollection || undefined}
href={route('dashboard').path}
key="favorite"
title="Favorite"
>
<FolderIcon className={classes.linkIcon} />
<Text maw={'200px'} style={{ wordBreak: 'break-all' }}>
{t('favorite')}
</Text>
</Link>
);
}

View File

@@ -8,11 +8,11 @@ import { appendCollectionId } from '~/lib/navigation';
import { CollectionWithLinks } from '~/types/app';
import classes from './collection_item.module.css';
export default function CollectionItem({
collection,
}: {
interface CollectionItemProps {
collection: CollectionWithLinks;
}) {
}
export function CollectionItem({ collection }: CollectionItemProps) {
const itemRef = useRef<HTMLAnchorElement>(null);
const activeCollection = useActiveCollection();
const isActiveCollection = collection.id === activeCollection?.id;
@@ -34,7 +34,10 @@ export default function CollectionItem({
title={collection.name}
>
<FolderIcon className={classes.linkIcon} />
<Text lineClamp={1} maw={'200px'} style={{ wordBreak: 'break-all' }}>
<Text
lineClamp={1}
style={{ wordBreak: 'break-all', whiteSpace: 'pre-line' }}
>
{collection.name}
</Text>
</Link>

View File

@@ -0,0 +1,50 @@
import { Button, Drawer, Portal, rem, Text } from '@mantine/core';
import { useDisclosure, useHeadroom } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { TbFolder } from 'react-icons/tb';
import { CollectionFavoriteItem } from '~/components/dashboard/collection/item/collection_favorite_item';
import { useCollections } from '~/hooks/collections/use_collections';
import { CollectionItem } from './item/collection_item';
export function MobileCollectionList() {
const { t } = useTranslation();
const [opened, handler] = useDisclosure();
const collections = useCollections();
const pinned = useHeadroom({ fixedAt: 0 });
return (
<>
<Drawer
opened={opened}
onClose={handler.close}
title={t('collection.collections', { count: collections.length })}
>
<CollectionFavoriteItem />
{collections.map((collection) => (
<CollectionItem collection={collection} />
))}
</Drawer>
<Portal>
<Button
onClick={handler.open}
variant="outline"
size="xs"
style={{
position: 'fixed',
left: '50%',
bottom: pinned ? rem(16) : rem(-100),
width: `calc(100% - ${rem(16)} * 2)`,
backgroundColor: 'var(--mantine-color-body)',
transition: 'all 0.2s ease-in-out',
transform: 'translateX(-50%)',
}}
>
<TbFolder size={18} />
<Text ml={4}>
{t('collection.collections', { count: collections.length })}
</Text>
</Button>
</Portal>
</>
);
}

View File

@@ -0,0 +1,32 @@
import { Badge, CopyButton } from '@mantine/core';
import { t } from 'i18next';
import { TbCopy } from 'react-icons/tb';
import { useActiveCollection } from '~/hooks/collections/use_active_collection';
import { useAppUrl } from '~/hooks/use_app_url';
const COPY_TIMEOUT = 3_000;
export function SharedCollectionCopyLink() {
const appUrl = useAppUrl();
const activeCollection = useActiveCollection();
if (!activeCollection) {
return null;
}
const copyUrl = `${appUrl}/shared/${activeCollection.id}`;
return (
<CopyButton value={copyUrl} timeout={COPY_TIMEOUT}>
{({ copied, copy }) => (
<Badge
variant={copied ? 'filled' : 'light'}
onClick={copy}
style={{ cursor: 'pointer' }}
>
{copied ? t('success-copy') : t('visibility.public')}
{!copied && <TbCopy style={{ marginLeft: 4 }} />}
</Badge>
)}
</CopyButton>
);
}