mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
feat: apply new style on side-navigation component
This commit is contained in:
9
inertia/components/common/text_ellipsis.tsx
Normal file
9
inertia/components/common/text_ellipsis.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const TextEllipsis = styled.p({
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export default TextEllipsis;
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import VisibilityBadge from '~/components/dashboard/side_nav/visibilty/visibilty';
|
||||
import VisibilityBadge from '~/components/visibilty/visibilty';
|
||||
import useActiveCollection from '~/hooks/use_active_collection';
|
||||
|
||||
const CollectionNameWrapper = styled.div({
|
||||
|
||||
@@ -9,7 +9,6 @@ const IMG_LOAD_TIMEOUT = 7_500;
|
||||
interface LinkFaviconProps {
|
||||
url: string;
|
||||
size?: number;
|
||||
noMargin?: boolean;
|
||||
}
|
||||
|
||||
const Favicon = styled.div({
|
||||
@@ -31,11 +30,7 @@ const FaviconLoader = styled.div(({ theme }) => ({
|
||||
|
||||
// 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,
|
||||
noMargin = false,
|
||||
}: LinkFaviconProps) {
|
||||
export default function LinkFavicon({ url, size = 32 }: LinkFaviconProps) {
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const [isFailed, setFailed] = useState<boolean>(false);
|
||||
@@ -60,7 +55,7 @@ export default function LinkFavicon({
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<Favicon style={{ marginRight: !noMargin ? '1em' : '0' }}>
|
||||
<Favicon>
|
||||
{!isFailed ? (
|
||||
<img
|
||||
src={`/favicon?url=${url}`}
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
import PATHS from '#constants/paths';
|
||||
import type Collection from '#models/collection';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
|
||||
import VisibilityBadge from '~/components/dashboard/side_nav/visibilty/visibilty';
|
||||
import useActiveCollection from '~/hooks/use_active_collection';
|
||||
import useCollections from '~/hooks/use_collections';
|
||||
import { arrayMove } from '~/lib/array';
|
||||
import sortCcollectionsByNextId from '~/lib/collection';
|
||||
import { makeRequest } from '~/lib/request';
|
||||
|
||||
interface CollectionItemProps {
|
||||
collection: Collection;
|
||||
index: number;
|
||||
}
|
||||
|
||||
type CollectionDragItem = {
|
||||
collectionId: Collection['id'];
|
||||
index: number;
|
||||
};
|
||||
|
||||
const CollectionItemStyle = styled.li<{ active: boolean }>(
|
||||
({ theme, active }) => ({
|
||||
color: active ? theme.colors.blue : theme.colors.font,
|
||||
borderBottom: active
|
||||
? `'2px solid ${theme.colors.lightestGrey} !important'`
|
||||
: '2px solid transparent !important',
|
||||
display: 'flex',
|
||||
gap: '0.25em',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
transition: `${theme.transition.delay} box-shadow`,
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.blue,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const CollectionContent = styled.div({
|
||||
width: 'calc(100% - 24px)',
|
||||
display: 'flex',
|
||||
gap: '0.35em',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const CollectionNameWrapper = styled.div({
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
gap: '0.35em',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const CollectionName = styled.div({
|
||||
minWidth: 0,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
const CollectionLinksCount = styled.div(({ theme }) => ({
|
||||
minWidth: 'fit-content',
|
||||
fontSize: '0.85em',
|
||||
color: theme.colors.grey,
|
||||
}));
|
||||
|
||||
export default function CollectionItem({
|
||||
collection,
|
||||
index,
|
||||
}: Readonly<CollectionItemProps>): JSX.Element {
|
||||
const { activeCollection, setActiveCollection } = useActiveCollection();
|
||||
const { collections, setCollections } = useCollections();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ref = useRef<HTMLLIElement>();
|
||||
|
||||
const sendMoveCollectionRequest = useCallback(
|
||||
async (currentCollection: Collection, nextId?: string) => {
|
||||
if (currentCollection.id === nextId) return;
|
||||
|
||||
await makeRequest({
|
||||
url: `${PATHS.API.COLLECTION}/${currentCollection.id}`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
name: currentCollection.name,
|
||||
nextId,
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
setCollections((prevCollections) => {
|
||||
const newCollections = [...prevCollections];
|
||||
const collectionIndex = newCollections.findIndex(
|
||||
(c) => c.id === currentCollection.id
|
||||
);
|
||||
|
||||
const previousCollectionIndex = newCollections.findIndex(
|
||||
(c) => c.nextId === currentCollection.id
|
||||
);
|
||||
const prevNextCollectionIndex = newCollections.findIndex(
|
||||
(c) => c.nextId === nextId
|
||||
);
|
||||
|
||||
newCollections[collectionIndex] = {
|
||||
...newCollections[collectionIndex],
|
||||
nextId,
|
||||
};
|
||||
if (previousCollectionIndex !== -1) {
|
||||
newCollections[previousCollectionIndex] = {
|
||||
...newCollections[previousCollectionIndex],
|
||||
nextId: currentCollection.nextId,
|
||||
};
|
||||
}
|
||||
if (prevNextCollectionIndex !== -1) {
|
||||
newCollections[prevNextCollectionIndex] = {
|
||||
...newCollections[prevNextCollectionIndex],
|
||||
nextId: currentCollection.id,
|
||||
};
|
||||
}
|
||||
|
||||
return sortCcollectionsByNextId(newCollections);
|
||||
});
|
||||
},
|
||||
[setCollections]
|
||||
);
|
||||
const moveCollection = useCallback(
|
||||
(currentIndex: number, newIndex: number) => {
|
||||
// @ts-ignore
|
||||
setCollections((prevCollections: Collection[]) =>
|
||||
arrayMove(prevCollections, currentIndex, newIndex)
|
||||
);
|
||||
},
|
||||
[setCollections]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const [_, drop] = useDrop({
|
||||
accept: 'collection',
|
||||
hover: (dragItem: CollectionDragItem) => {
|
||||
if (ref.current && dragItem.collectionId !== collection.id) {
|
||||
moveCollection(dragItem.index, index);
|
||||
dragItem.index = index;
|
||||
}
|
||||
},
|
||||
drop: (item) => {
|
||||
const currentCollection = collections.find(
|
||||
(c) => c.id === item.collectionId
|
||||
);
|
||||
const nextCollection = collections[item.index + 1];
|
||||
if (
|
||||
currentCollection?.nextId === null &&
|
||||
nextCollection?.id === undefined
|
||||
)
|
||||
return;
|
||||
if (currentCollection?.nextId !== nextCollection?.id) {
|
||||
sendMoveCollectionRequest(
|
||||
currentCollection!,
|
||||
nextCollection?.id ?? null
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [{ opacity }, drag] = useDrag({
|
||||
type: 'collection',
|
||||
item: () => ({ index, collectionId: collection.id }),
|
||||
collect: (monitor: any) => ({
|
||||
opacity: monitor.isDragging() ? 0.1 : 1,
|
||||
}),
|
||||
end: (dragItem: CollectionDragItem, monitor) => {
|
||||
const didDrop = monitor.didDrop();
|
||||
if (!didDrop) {
|
||||
moveCollection(dragItem.index, index);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (collection.id === activeCollection?.id) {
|
||||
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}, [collection.id, activeCollection?.id]);
|
||||
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<CollectionItemStyle
|
||||
style={{
|
||||
transition: 'none',
|
||||
opacity,
|
||||
}}
|
||||
onClick={() => setActiveCollection(collection)}
|
||||
title={collection.name}
|
||||
active={collection.id === activeCollection?.id}
|
||||
// @ts-ignore
|
||||
ref={ref}
|
||||
>
|
||||
{collection.id === activeCollection?.id ? (
|
||||
<AiFillFolderOpen size={24} />
|
||||
) : (
|
||||
<AiOutlineFolder size={24} />
|
||||
)}
|
||||
|
||||
<CollectionContent>
|
||||
<CollectionNameWrapper>
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
<CollectionLinksCount>
|
||||
— {collection.links.length}
|
||||
</CollectionLinksCount>
|
||||
</CollectionNameWrapper>
|
||||
<VisibilityBadge
|
||||
label={t('common:collection.visibility')}
|
||||
visibility={collection.visibility}
|
||||
/>
|
||||
</CollectionContent>
|
||||
</CollectionItemStyle>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useMemo } from 'react';
|
||||
import { DndProvider, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import UnstyledList from '~/components/common/unstyled/unstyled_list';
|
||||
import CollectionItem from '~/components/dashboard/side_nav/collection/collection_item';
|
||||
import useActiveCollection from '~/hooks/use_active_collection';
|
||||
import useCollections from '~/hooks/use_collections';
|
||||
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
|
||||
import Keys from '../../../../../app/constants/keys';
|
||||
|
||||
const CollectionListStyle = styled.div({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export default function CollectionList() {
|
||||
const { t } = useTranslation();
|
||||
const { collections } = useCollections();
|
||||
const { activeCollection, setActiveCollection } = useActiveCollection();
|
||||
const { globalHotkeysEnabled } = useGlobalHotkeys();
|
||||
|
||||
const linksCount = useMemo(
|
||||
() =>
|
||||
collections.reduce((acc, current) => (acc += current.links.length), 0),
|
||||
[collections]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
Keys.ARROW_UP,
|
||||
() => {
|
||||
const currentCollectionIndex = collections.findIndex(
|
||||
({ id }) => id === activeCollection?.id
|
||||
);
|
||||
if (currentCollectionIndex === -1 || currentCollectionIndex === 0) return;
|
||||
|
||||
setActiveCollection(collections[currentCollectionIndex - 1]);
|
||||
},
|
||||
{ enabled: globalHotkeysEnabled }
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
Keys.ARROW_DOWN,
|
||||
() => {
|
||||
const currentCollectionIndex = collections.findIndex(
|
||||
({ id }) => id === activeCollection?.id
|
||||
);
|
||||
if (
|
||||
currentCollectionIndex === -1 ||
|
||||
currentCollectionIndex === collections.length - 1
|
||||
)
|
||||
return;
|
||||
|
||||
setActiveCollection(collections[currentCollectionIndex + 1]);
|
||||
},
|
||||
{ enabled: globalHotkeysEnabled }
|
||||
);
|
||||
|
||||
return (
|
||||
<CollectionListStyle>
|
||||
<h4>
|
||||
{t('collection.collections', { count: linksCount })} • {linksCount}
|
||||
</h4>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ListOfCollections />
|
||||
</DndProvider>
|
||||
</CollectionListStyle>
|
||||
);
|
||||
}
|
||||
|
||||
function ListOfCollections() {
|
||||
const [, drop] = useDrop(() => ({ accept: 'collection' }));
|
||||
const { collections } = useCollections();
|
||||
|
||||
return (
|
||||
<UnstyledList
|
||||
css={{
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
// @ts-ignore
|
||||
ref={drop}
|
||||
>
|
||||
{collections.map((collection, index) => (
|
||||
<CollectionItem
|
||||
collection={collection}
|
||||
key={collection.id}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</UnstyledList>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const FavoriteListContainer = styled.div({
|
||||
height: '100%',
|
||||
minHeight: 0,
|
||||
paddingInline: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export default FavoriteListContainer;
|
||||
@@ -0,0 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ItemLink } from '~/components/dashboard/side_nav/nav_item';
|
||||
|
||||
const FavoriteItem = styled(ItemLink)(({ theme }) => ({
|
||||
backgroundColor: theme.colors.white,
|
||||
}));
|
||||
|
||||
export default FavoriteItem;
|
||||
@@ -0,0 +1,49 @@
|
||||
import styled from '@emotion/styled';
|
||||
import TextEllipsis from '~/components/common/text_ellipsis';
|
||||
import LinkFavicon from '~/components/dashboard/link/link_favicon';
|
||||
import FavoriteListContainer from '~/components/dashboard/side_nav/favorite/favorite_container';
|
||||
import FavoriteItem from '~/components/dashboard/side_nav/favorite/favorite_item';
|
||||
import useFavorites from '~/hooks/use_favorites';
|
||||
|
||||
const FavoriteLabel = styled.p(({ theme }) => ({
|
||||
color: theme.colors.grey,
|
||||
}));
|
||||
|
||||
const NoFavorite = () => (
|
||||
<FavoriteLabel css={{ textAlign: 'center' }}>
|
||||
Your favorites will appear here
|
||||
</FavoriteLabel>
|
||||
);
|
||||
|
||||
const FavoriteListStyle = styled.div({
|
||||
padding: '1px',
|
||||
paddingRight: '5px',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
gap: '.35em',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
export default function FavoriteList() {
|
||||
const { favorites } = useFavorites();
|
||||
if (favorites.length === 0) {
|
||||
return <NoFavorite key="no-favorite" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FavoriteListContainer>
|
||||
<FavoriteLabel css={{ marginBlock: '0.35em', paddingInline: '15px' }}>
|
||||
Favorites • {favorites.length}
|
||||
</FavoriteLabel>
|
||||
<FavoriteListStyle>
|
||||
{favorites.map(({ id, name, url }) => (
|
||||
<FavoriteItem href={url} key={id}>
|
||||
<LinkFavicon url={url} size={24} />
|
||||
<TextEllipsis>{name}</TextEllipsis>
|
||||
</FavoriteItem>
|
||||
))}
|
||||
</FavoriteListStyle>
|
||||
</FavoriteListContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import type Link from '#models/link';
|
||||
import styled from '@emotion/styled';
|
||||
import ExternalLink from '~/components/common/external_link';
|
||||
import LinkFavicon from '~/components/dashboard/link/link_favicon';
|
||||
|
||||
const FavoriteLinkStyle = styled.li(({ theme }) => ({
|
||||
width: '100%',
|
||||
backgroundColor: theme.colors.white,
|
||||
padding: 0,
|
||||
border: `1px solid ${theme.colors.lightestGrey}`,
|
||||
borderBottom: `2px solid ${theme.colors.lightestGrey}`,
|
||||
borderRadius: theme.border.radius,
|
||||
transition: theme.transition.delay,
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.blue,
|
||||
backgroundColor: theme.colors.lightGrey,
|
||||
borderBottom: `2px solid ${theme.colors.blue}`,
|
||||
},
|
||||
}));
|
||||
|
||||
const LinkStyle = styled(ExternalLink)({
|
||||
width: '100%',
|
||||
color: 'inherit',
|
||||
padding: '0.5em 1em',
|
||||
border: '0 !important',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25em',
|
||||
});
|
||||
|
||||
const LinkName = styled.span({
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
const CollectionName = styled.span(({ theme }) => ({
|
||||
maxWidth: '75px',
|
||||
fontSize: '0.85em',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: theme.colors.grey,
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
}));
|
||||
|
||||
const FavoriteItem = ({
|
||||
link,
|
||||
}: Readonly<{
|
||||
link: Link;
|
||||
}>) => (
|
||||
<FavoriteLinkStyle>
|
||||
<LinkStyle
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={link.name}
|
||||
>
|
||||
<LinkFavicon url={link.url} size={24} noMargin />
|
||||
<LinkName>{link.name}</LinkName>
|
||||
<CollectionName> - {link.collection.name}</CollectionName>
|
||||
</LinkStyle>
|
||||
</FavoriteLinkStyle>
|
||||
);
|
||||
export default FavoriteItem;
|
||||
@@ -1,44 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import UnstyledList from '~/components/common/unstyled/unstyled_list';
|
||||
import FavoriteItem from '~/components/dashboard/side_nav/favorites/favorite_item';
|
||||
import useFavorites from '~/hooks/use_favorites';
|
||||
|
||||
const FavoriteListWrapper = styled.div({
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
marginBottom: '15px',
|
||||
});
|
||||
|
||||
const FavoriteListLegend = styled.h4(({ theme }) => ({
|
||||
userSelect: 'none',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '0.85em',
|
||||
fontWeight: '500',
|
||||
color: theme.colors.grey,
|
||||
marginBottom: '5px',
|
||||
}));
|
||||
|
||||
const FavoriteListStyle = styled(UnstyledList)({
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export default function FavoriteList() {
|
||||
const { t } = useTranslation('common');
|
||||
const { favorites } = useFavorites();
|
||||
|
||||
return (
|
||||
favorites.length !== 0 && (
|
||||
<FavoriteListWrapper>
|
||||
<FavoriteListLegend>{t('favorite')}</FavoriteListLegend>
|
||||
<FavoriteListStyle>
|
||||
{favorites.map((link) => (
|
||||
<FavoriteItem link={link} key={link.id} />
|
||||
))}
|
||||
</FavoriteListStyle>
|
||||
</FavoriteListWrapper>
|
||||
)
|
||||
);
|
||||
}
|
||||
28
inertia/components/dashboard/side_nav/nav_item.tsx
Normal file
28
inertia/components/dashboard/side_nav/nav_item.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
export const Item = styled.div(({ theme }) => ({
|
||||
userSelect: 'none',
|
||||
height: '40px',
|
||||
width: '280px',
|
||||
backgroundColor: theme.colors.lightGrey,
|
||||
padding: '8px 12px',
|
||||
borderRadius: theme.border.radius,
|
||||
display: 'flex',
|
||||
gap: '.75em',
|
||||
alignItems: 'center',
|
||||
|
||||
'& > svg': {
|
||||
height: '24px',
|
||||
width: '24px',
|
||||
},
|
||||
|
||||
// Disable hover effect for UserCard
|
||||
'&:hover:not(.disable-hover)': {
|
||||
backgroundColor: theme.colors.white,
|
||||
outlineWidth: '1px',
|
||||
outlineStyle: 'solid',
|
||||
},
|
||||
}));
|
||||
|
||||
export const ItemLink = Item.withComponent(Link);
|
||||
@@ -1,46 +0,0 @@
|
||||
import PATHS from '#constants/paths';
|
||||
import styled from '@emotion/styled';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useActiveCollection from '~/hooks/use_active_collection';
|
||||
|
||||
const MenuControls = styled.div({
|
||||
margin: '10px 0',
|
||||
display: 'flex',
|
||||
gap: '0.25em',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
const MenuAction = styled.div({
|
||||
fontSize: '0.95em',
|
||||
display: 'flex',
|
||||
gap: '0.5em',
|
||||
});
|
||||
|
||||
export default function NavigationLinks() {
|
||||
const { t } = useTranslation('common');
|
||||
const { activeCollection } = useActiveCollection();
|
||||
|
||||
return (
|
||||
<MenuControls>
|
||||
{/* <MenuAction>
|
||||
<SearchModal>{t('common:search')}</SearchModal>
|
||||
<kbd>S</kbd>
|
||||
</MenuAction> */}
|
||||
<MenuAction>
|
||||
<Link href={PATHS.COLLECTION.CREATE}>{t('collection.create')}</Link>
|
||||
<kbd>C</kbd>
|
||||
</MenuAction>
|
||||
<MenuAction>
|
||||
<Link
|
||||
href={`${PATHS.LINK.CREATE}?collectionId=${activeCollection?.id}`}
|
||||
>
|
||||
{t('link.create')}
|
||||
</Link>
|
||||
<kbd>L</kbd>
|
||||
</MenuAction>
|
||||
</MenuControls>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +1,51 @@
|
||||
import PATHS from '#constants/paths';
|
||||
import styled from '@emotion/styled';
|
||||
import CollectionList from '~/components/dashboard/side_nav/collection/collection_list';
|
||||
import FavoriteList from '~/components/dashboard/side_nav/favorites/favorite_list';
|
||||
import NavigationLinks from '~/components/dashboard/side_nav/navigation_links/navigation_links';
|
||||
import UserCard from '~/components/dashboard/side_nav/user_card/user_card';
|
||||
import { AiOutlineFolderAdd } from 'react-icons/ai';
|
||||
import { BsGear } from 'react-icons/bs';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { MdOutlineAdminPanelSettings } from 'react-icons/md';
|
||||
import FavoriteList from '~/components/dashboard/side_nav/favorite/favorite_list';
|
||||
import { ItemLink } from '~/components/dashboard/side_nav/nav_item';
|
||||
import UserCard from '~/components/dashboard/side_nav/user_card';
|
||||
|
||||
const SideMenu = styled.nav({
|
||||
height: '100%',
|
||||
width: '325px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '.35em',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
const BlockWrapper = styled.section(({ theme }) => ({
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
const AdminButton = styled(ItemLink)(({ theme }) => ({
|
||||
color: theme.colors.lightRed,
|
||||
}));
|
||||
|
||||
'& h4': {
|
||||
userSelect: 'none',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '0.85em',
|
||||
fontWeight: 500,
|
||||
color: theme.colors.grey,
|
||||
marginBottom: '5px',
|
||||
},
|
||||
const SettingsButton = styled(ItemLink)(({ theme }) => ({
|
||||
color: theme.colors.grey,
|
||||
}));
|
||||
|
||||
'& ul': {
|
||||
flex: 1,
|
||||
animation: 'fadein 0.3s both',
|
||||
},
|
||||
|
||||
'& ul li': {
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
height: 'fit-content',
|
||||
width: '100%',
|
||||
fontSize: '0.9em',
|
||||
backgroundColor: theme.colors.white,
|
||||
padding: '0.5em 1em',
|
||||
borderBottom: `2px solid ${theme.colors.lightestGrey}`,
|
||||
borderRadius: theme.border.radius,
|
||||
transition: theme.transition.delay,
|
||||
|
||||
'&:not(:last-child)': {
|
||||
marginBottom: '5px',
|
||||
},
|
||||
},
|
||||
const AddButton = styled(ItemLink)(({ theme }) => ({
|
||||
color: theme.colors.primary,
|
||||
}));
|
||||
|
||||
const SideNavigation = () => (
|
||||
<SideMenu>
|
||||
<BlockWrapper>
|
||||
<FavoriteList />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper css={{ minHeight: '0', flex: '1' }}>
|
||||
<CollectionList />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<NavigationLinks />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<div css={{ paddingInline: '10px' }}>
|
||||
<UserCard />
|
||||
</BlockWrapper>
|
||||
<AdminButton href="/admin">
|
||||
<MdOutlineAdminPanelSettings /> Administrator
|
||||
</AdminButton>
|
||||
<SettingsButton href="/settings">
|
||||
<BsGear />
|
||||
Settings
|
||||
</SettingsButton>
|
||||
<AddButton href={PATHS.LINK.CREATE}>
|
||||
<IoAdd /> Create link
|
||||
</AddButton>
|
||||
<AddButton href={PATHS.COLLECTION.CREATE}>
|
||||
<AiOutlineFolderAdd /> Create collection
|
||||
</AddButton>
|
||||
</div>
|
||||
<FavoriteList />
|
||||
</SideMenu>
|
||||
);
|
||||
|
||||
|
||||
16
inertia/components/dashboard/side_nav/user_card.tsx
Normal file
16
inertia/components/dashboard/side_nav/user_card.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import RoundedImage from '~/components/common/rounded_image';
|
||||
import { Item } from '~/components/dashboard/side_nav/nav_item';
|
||||
import useUser from '~/hooks/use_user';
|
||||
|
||||
export default function UserCard() {
|
||||
const { user, isAuthenticated } = useUser();
|
||||
const altImage = `${user?.nickName}'s avatar`;
|
||||
return (
|
||||
isAuthenticated && (
|
||||
<Item className="disable-hover">
|
||||
<RoundedImage src={user.avatarUrl} width={24} alt={altImage} />
|
||||
{user.nickName}
|
||||
</Item>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import PATHS from '#constants/paths';
|
||||
import styled from '@emotion/styled';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdAdminPanelSettings } from 'react-icons/md';
|
||||
import RoundedImage from '~/components/common/rounded_image';
|
||||
import useUser from '~/hooks/use_user';
|
||||
|
||||
const UserCardWrapper = styled.div(({ theme }) => ({
|
||||
userSelect: 'none',
|
||||
height: 'fit-content',
|
||||
width: '100%',
|
||||
color: theme.colors.black,
|
||||
backgroundColor: theme.colors.white,
|
||||
border: `1px solid ${theme.colors.lightestGrey}`,
|
||||
padding: '7px 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
|
||||
'& button, & a': {
|
||||
cursor: 'pointer',
|
||||
color: theme.colors.blue,
|
||||
display: 'flex',
|
||||
transition: theme.transition.delay,
|
||||
|
||||
'&:hover': {
|
||||
transform: 'scale(1.3)',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const UserCardStyle = styled.div({
|
||||
display: 'flex',
|
||||
gap: '0.5em',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const UserControls = styled.div({
|
||||
display: 'flex',
|
||||
gap: '0.35em',
|
||||
});
|
||||
|
||||
export default function UserCard() {
|
||||
const { user } = useUser();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const avatarLabel = t('avatar', {
|
||||
name: user.name,
|
||||
});
|
||||
return (
|
||||
<UserCardWrapper>
|
||||
<UserCardStyle>
|
||||
<RoundedImage
|
||||
src={user.avatarUrl}
|
||||
width={28}
|
||||
height={28}
|
||||
alt={avatarLabel}
|
||||
title={avatarLabel}
|
||||
/>
|
||||
{user.nickName}
|
||||
</UserCardStyle>
|
||||
<UserControls>
|
||||
{user.is_admin && (
|
||||
<Link href={PATHS.ADMIN} className="reset">
|
||||
<MdAdminPanelSettings color="red" size={24} />
|
||||
</Link>
|
||||
)}
|
||||
{/* <SettingsModal /> */}
|
||||
</UserControls>
|
||||
</UserCardWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IoEarthOutline } from 'react-icons/io5';
|
||||
import { Visibility } from '../../../../../app/enums/visibility';
|
||||
import { Visibility } from '../../../app/enums/visibility';
|
||||
|
||||
const VisibilityStyle = styled.span(({ theme }) => ({
|
||||
fontWeight: 300,
|
||||
@@ -5,8 +5,8 @@ const documentStyle = css({
|
||||
'html, body, #app': {
|
||||
height: '100svh',
|
||||
width: '100svw',
|
||||
fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
|
||||
fontSize: '17px',
|
||||
fontFamily: "'Poppins', sans-serif",
|
||||
fontSize: '14px',
|
||||
color: theme.colors.font,
|
||||
backgroundColor: theme.colors.background,
|
||||
display: 'flex',
|
||||
|
||||
@@ -8,7 +8,7 @@ const white = '#fff';
|
||||
|
||||
const lightestGrey = '#dadce0';
|
||||
const lightGrey = '#f0eef6';
|
||||
const grey = '#aaa';
|
||||
const grey = '#888888';
|
||||
const darkGrey = '#4b5563';
|
||||
|
||||
const lightestBlue = '#d3e8fa';
|
||||
@@ -17,7 +17,8 @@ const blue = '#3f88c5';
|
||||
const darkBlue = '#005aa5';
|
||||
const darkestBlue = '#1f2937';
|
||||
|
||||
const lightRed = '#ffbabab9';
|
||||
const lightestRed = '#ffbabab9';
|
||||
const lightRed = '#FF5A5A';
|
||||
const red = '#d8000c';
|
||||
|
||||
const lightGreen = '#c1ffbab9';
|
||||
@@ -48,6 +49,7 @@ export const theme: Theme = {
|
||||
darkBlue,
|
||||
darkestBlue,
|
||||
|
||||
lightestRed,
|
||||
lightRed,
|
||||
red,
|
||||
|
||||
|
||||
13
inertia/types/app.d.ts
vendored
13
inertia/types/app.d.ts
vendored
@@ -1,4 +1,11 @@
|
||||
import { Serialize } from '@tuyau/utils/types';
|
||||
import type UserModel from '../../app/models/user';
|
||||
import type Collection from '#models/collection';
|
||||
|
||||
type User = Serialize<UserModel>;
|
||||
type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
nickName: string;
|
||||
avatarUrl: string;
|
||||
isAdmin: boolean;
|
||||
collections: Collection[];
|
||||
};
|
||||
|
||||
1
inertia/types/emotion.d.ts
vendored
1
inertia/types/emotion.d.ts
vendored
@@ -24,6 +24,7 @@ declare module '@emotion/react' {
|
||||
darkBlue: string;
|
||||
darkestBlue: string;
|
||||
|
||||
lightestRed: string;
|
||||
lightRed: string;
|
||||
red: string;
|
||||
|
||||
|
||||
19
inertia/types/inertia.d.ts
vendored
19
inertia/types/inertia.d.ts
vendored
@@ -1,8 +1,17 @@
|
||||
import type { User } from './app';
|
||||
|
||||
export type InertiaPage<T extends Record<string, unknown> = Record<string, unknown>> = T & {
|
||||
auth: {
|
||||
user?: User;
|
||||
isAuthenticated: boolean;
|
||||
};
|
||||
type Auth =
|
||||
| {
|
||||
isAuthenticated: true;
|
||||
user: User;
|
||||
}
|
||||
| {
|
||||
isAuthenticated: false;
|
||||
user: null;
|
||||
};
|
||||
|
||||
export type InertiaPage<
|
||||
T extends Record<string, unknown> = Record<string, unknown>,
|
||||
> = T & {
|
||||
auth: Auth;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link
|
||||
href='https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,700;1,400;1,700&display=swap'
|
||||
rel='stylesheet'
|
||||
/>
|
||||
|
||||
<title inertia>MyLinks</title>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user