feat: add dropdown for links and collection header

This commit is contained in:
Sonny
2024-05-13 00:04:01 +02:00
committed by Sonny
parent 0f1dc9b69c
commit 2f0e1dd375
13 changed files with 204 additions and 131 deletions

View File

@@ -6,18 +6,33 @@ import type { HttpContext } from '@adonisjs/core/http';
export default class CollectionsController {
// Dashboard
async index({ auth, inertia, response }: HttpContext) {
async index({ auth, inertia, request, response }: HttpContext) {
const collections = await this.getCollectionByAuthorId(auth.user!.id);
if (collections.length === 0) {
return response.redirect('/collections/create');
}
return inertia.render('dashboard', { collections });
const activeCollectionId = request.qs()?.collectionId ?? '';
const activeCollection = collections.find(
(c) => c.id === activeCollectionId
);
if (!activeCollection && !!activeCollectionId) {
return response.redirect('/dashboard');
}
return inertia.render('dashboard', {
collections,
activeCollection: activeCollection || collections[0],
});
}
// Create collection form
async showCreatePage({ inertia }: HttpContext) {
return inertia.render('collections/create');
async showCreatePage({ inertia, auth }: HttpContext) {
const collections = await this.getCollectionByAuthorId(auth.user!.id);
return inertia.render('collections/create', {
disableHomeLink: collections.length === 0,
});
}
// Method called when creating a collection

View File

@@ -12,10 +12,10 @@ export default defineConfig({
sharedData: {
errors: (ctx) => ctx.session?.flashMessages.get('errors'),
auth: async (ctx) => {
await ctx.auth.check();
await ctx.auth?.check();
return {
user: ctx.auth.user,
isAuthenticated: ctx.auth.isAuthenticated,
user: ctx.auth?.user || null,
isAuthenticated: ctx.auth?.isAuthenticated || false,
};
},
},

View File

@@ -32,9 +32,11 @@ const DropdownStyle = styled.div<{ opened: boolean }>(({ opened, theme }) => ({
export default function Dropdown({
children,
label,
className,
}: {
children: ReactNode;
label: ReactNode | string;
className?: string;
}) {
const dropdownRef = useRef<HTMLDivElement>(null);
const { isShowing, toggle, close } = useToggle();
@@ -48,7 +50,12 @@ export default function Dropdown({
});
return (
<DropdownStyle opened={isShowing} onClick={toggle} ref={dropdownRef}>
<DropdownStyle
opened={isShowing}
onClick={toggle}
ref={dropdownRef}
className={className}
>
<DropdownLabel>{label}</DropdownLabel>
<DropdownContainer show={isShowing}>{children}</DropdownContainer>
</DropdownStyle>

View File

@@ -1,6 +1,7 @@
import styled from '@emotion/styled';
const DropdownContainer = styled.div<{ show: boolean }>(({ show, theme }) => ({
zIndex: 99,
position: 'absolute',
top: 'calc(100% + 0.5em)',
right: 0,

View File

@@ -1,11 +1,10 @@
import styled from '@emotion/styled';
import { Link } from '@inertiajs/react';
const DropdownItem = styled.div(({ theme }) => ({
const DropdownItemBase = styled.div(({ theme }) => ({
fontSize: '14px',
whiteSpace: 'nowrap',
padding: '8px 12px',
display: 'flex',
gap: '0.35em',
alignItems: 'center',
borderRadius: theme.border.radius,
'&:hover': {
@@ -13,4 +12,17 @@ const DropdownItem = styled.div(({ theme }) => ({
},
}));
export default DropdownItem;
const DropdownItemButton = styled(DropdownItemBase)({
display: 'flex',
gap: '0.75em',
alignItems: 'center',
});
const DropdownItemLink = styled(DropdownItemBase.withComponent(Link))({
width: '100%',
display: 'flex',
gap: '0.75em',
alignItems: 'center',
});
export { DropdownItemButton, DropdownItemLink };

View File

@@ -1,34 +1,28 @@
import PATHS from '#constants/paths';
import styled from '@emotion/styled';
import QuickResourceAction from '~/components/dashboard/quick_action/quick_action';
import useActiveCollection from '~/hooks/use_active_collection';
import { BsThreeDotsVertical } from 'react-icons/bs';
import { HiOutlinePencil } from 'react-icons/hi2';
import { IoIosAddCircleOutline } from 'react-icons/io';
import { IoTrashOutline } from 'react-icons/io5';
import Dropdown from '~/components/common/dropdown/dropdown';
import { DropdownItemLink } from '~/components/common/dropdown/dropdown_item';
const CollectionControlsStyle = styled.span({
display: 'flex',
gap: '0.5em',
alignItems: 'center',
});
const DeleteItem = styled(DropdownItemLink)(({ theme }) => ({
color: theme.colors.lightRed,
}));
export default function CollectionControls() {
const { activeCollection } = useActiveCollection();
return (
activeCollection && (
<CollectionControlsStyle>
<QuickResourceAction
resource="link"
action="create"
collectionId={activeCollection.id}
/>
<QuickResourceAction
resource="collection"
action="edit"
resourceId={activeCollection.id}
/>
<QuickResourceAction
resource="collection"
action="remove"
resourceId={activeCollection.id}
/>
</CollectionControlsStyle>
)
);
}
const CollectionControls = () => (
<Dropdown label={<BsThreeDotsVertical />}>
<DropdownItemLink href={PATHS.LINK.CREATE}>
<IoIosAddCircleOutline /> Add
</DropdownItemLink>
<DropdownItemLink href={PATHS.COLLECTION.EDIT}>
<HiOutlinePencil /> Edit
</DropdownItemLink>
<DeleteItem href={PATHS.COLLECTION.REMOVE}>
<IoTrashOutline /> Delete
</DeleteItem>
</Dropdown>
);
export default CollectionControls;

View File

@@ -0,0 +1,98 @@
import PATHS from '#constants/paths';
import type Link from '#models/link';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useCallback } from 'react';
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
import { BsThreeDotsVertical } from 'react-icons/bs';
import { HiOutlinePencil } from 'react-icons/hi2';
import { IoTrashOutline } from 'react-icons/io5';
import Dropdown from '~/components/common/dropdown/dropdown';
import {
DropdownItemButton,
DropdownItemLink,
} from '~/components/common/dropdown/dropdown_item';
import useCollections from '~/hooks/use_collections';
import { appendCollectionId } from '~/lib/navigation';
import { makeRequest } from '~/lib/request';
const StartItem = styled(DropdownItemButton)(({ theme }) => ({
color: theme.colors.yellow,
}));
const DeleteItem = styled(DropdownItemLink)(({ theme }) => ({
color: theme.colors.lightRed,
}));
export default function LinkControls({ link }: { link: Link }) {
const theme = useTheme();
const { collections, setCollections } = useCollections();
const toggleFavorite = useCallback(
(linkId: Link['id']) => {
let linkIndex = 0;
const collectionIndex = collections.findIndex(({ links }) => {
const lIndex = links.findIndex((l) => l.id === linkId);
if (lIndex !== -1) {
linkIndex = lIndex;
}
return lIndex !== -1;
});
const collectionLink = collections[collectionIndex].links[linkIndex];
const collectionsCopy = [...collections];
collectionsCopy[collectionIndex].links[linkIndex] = {
...collectionLink,
favorite: !collectionLink.favorite,
};
setCollections(collectionsCopy);
},
[collections, setCollections]
);
const onFavorite = () => {
makeRequest({
url: `${PATHS.API.LINK}/${link.id}`,
method: 'PUT',
body: {
name: link.name,
url: link.url,
favorite: !link.favorite,
collectionId: link.collectionId,
},
})
.then(() => toggleFavorite(link.id))
.catch(console.error);
};
console.log(link.favorite, link.favorite ? 'oui' : 'non');
return (
<Dropdown
label={<BsThreeDotsVertical />}
css={{ backgroundColor: theme.colors.secondary }}
>
<StartItem onClick={onFavorite}>
{!link.favorite ? (
<>
<AiFillStar /> Add to favorites
</>
) : (
<>
<AiOutlineStar /> Remove from favorites
</>
)}
</StartItem>
<DropdownItemLink
href={appendCollectionId(PATHS.LINK.EDIT, link.collectionId)}
>
<HiOutlinePencil /> Edit
</DropdownItemLink>
<DeleteItem
href={appendCollectionId(PATHS.LINK.REMOVE, link.collectionId)}
>
<IoTrashOutline /> Delete
</DeleteItem>
</Dropdown>
);
}

View File

@@ -1,14 +1,9 @@
import PATHS from '#constants/paths';
import type Link from '#models/link';
import styled from '@emotion/styled';
import { useCallback } from 'react';
import { AiFillStar } from 'react-icons/ai';
import ExternalLink from '~/components/common/external_link';
import LinkFavicon from '~/components/dashboard/link/link_favicon';
import QuickResourceAction from '~/components/dashboard/quick_action/quick_action';
import QuickLinkFavorite from '~/components/dashboard/quick_action/quick_favorite_link';
import useCollections from '~/hooks/use_collections';
import { makeRequest } from '~/lib/request';
import LinkControls from '~/components/dashboard/link_list/link_controls';
const LinkWrapper = styled.li(({ theme }) => ({
userSelect: 'none',
@@ -20,16 +15,21 @@ const LinkWrapper = styled.li(({ theme }) => ({
padding: '0.75em 1em',
border: `1px solid ${theme.colors.lightGrey}`,
borderRadius: theme.border.radius,
outline: '3px solid transparent',
'&:hover': {
outlineWidth: '1px',
outlineStyle: 'solid',
},
}));
const LinkHeader = styled.div(({ theme }) => ({
display: 'flex',
gap: '1em',
alignItems: 'center',
'& > a': {
height: '100%',
maxWidth: 'calc(100% - 125px)', // TODO: fix this, it is ugly af :(
maxWidth: 'calc(100% - 75px)', // TODO: fix this, it is ugly af :(
textDecoration: 'none',
display: 'flex',
flex: 1,
@@ -49,22 +49,6 @@ const LinkName = styled.div({
overflow: 'hidden',
});
const LinkControls = styled.div({
display: 'none',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
'& svg': {
height: '20px',
width: '20px',
},
'&:hover *': {
transform: 'scale(1.3)',
},
});
const LinkDescription = styled.div(({ theme }) => ({
marginTop: '0.5em',
color: theme.colors.font,
@@ -97,46 +81,6 @@ export default function LinkItem({
showUserControls: boolean;
}) {
const { id, name, url, description, favorite } = link;
const { collections, setCollections } = useCollections();
const toggleFavorite = useCallback(
(linkId: Link['id']) => {
let linkIndex = 0;
const collectionIndex = collections.findIndex(({ links }) => {
const lIndex = links.findIndex((l) => l.id === linkId);
if (lIndex !== -1) {
linkIndex = lIndex;
}
return lIndex !== -1;
});
const collectionLink = collections[collectionIndex].links[linkIndex];
const collectionsCopy = [...collections];
collectionsCopy[collectionIndex].links[linkIndex] = {
...collectionLink,
favorite: !collectionLink.favorite,
};
setCollections(collectionsCopy);
},
[collections, setCollections]
);
const onFavorite = () => {
makeRequest({
url: `${PATHS.API.LINK}/${link.id}`,
method: 'PUT',
body: {
name,
url,
favorite: !favorite,
collectionId: link.collectionId,
},
})
.then(() => toggleFavorite(link.id))
.catch(console.error);
};
return (
<LinkWrapper key={id}>
<LinkHeader>
@@ -147,21 +91,7 @@ export default function LinkItem({
</LinkName>
<LinkItemURL url={url} />
</ExternalLink>
{showUserControls && (
<LinkControls>
<QuickLinkFavorite onClick={onFavorite} isFavorite={favorite} />
<QuickResourceAction
resource="link"
action="edit"
resourceId={id}
/>
<QuickResourceAction
resource="link"
action="remove"
resourceId={id}
/>
</LinkControls>
)}
{showUserControls && <LinkControls link={link} />}
</LinkHeader>
{description && <LinkDescription>{description}</LinkDescription>}
</LinkWrapper>

View File

@@ -19,8 +19,9 @@ const LinksWrapper = styled.div({
});
const CollectionHeaderWrapper = styled.h2(({ theme }) => ({
fontWeight: 400,
color: theme.colors.primary,
fontWeight: 500,
paddingInline: '1em',
display: 'flex',
gap: '0.4em',
alignItems: 'center',

View File

@@ -10,6 +10,9 @@ const FooterStyle = styled.footer(({ theme }) => ({
color: theme.colors.grey,
textAlign: 'center',
paddingTop: '0.75em',
'& a:hover': {
textDecoration: 'underline',
},
}));
export default function Footer() {

View File

@@ -1,13 +1,20 @@
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import Footer from '~/components/footer/footer';
import Navbar from '../navbar/navbar';
import BaseLayout from './_base_layout';
const ContentLayoutStyle = styled.div(({ theme }) => ({
height: 'auto',
height: '100%',
width: theme.media.small_desktop,
maxWidth: '100%',
padding: '1em',
display: 'flex',
flexDirection: 'column',
'& main': {
flex: 1,
},
}));
const ContentLayout = ({ children }: { children: ReactNode }) => (
@@ -15,6 +22,7 @@ const ContentLayout = ({ children }: { children: ReactNode }) => (
<ContentLayoutStyle>
<Navbar />
<main>{children}</main>
<Footer />
</ContentLayoutStyle>
</BaseLayout>
);

View File

@@ -1,10 +1,11 @@
import PATHS from '#constants/paths';
import styled from '@emotion/styled';
import { Link } from '@inertiajs/react';
import { FormEvent, ReactNode } from 'react';
import Button from '~/components/common/form/_button';
import Form from '~/components/common/form/_form';
import BaseLayout from './_base_layout';
import { appendCollectionId } from '~/lib/navigation';
import PATHS from '#constants/paths';
const FormLayoutStyle = styled.div(({ theme }) => ({
height: 'fit-content',
@@ -26,6 +27,7 @@ interface FormLayoutProps {
textSubmitButton?: string;
disableHomeLink?: boolean;
collectionId?: string;
}
const FormLayout = ({
@@ -35,6 +37,7 @@ const FormLayout = ({
handleSubmit,
textSubmitButton = 'Confirm',
disableHomeLink = false,
collectionId,
}: FormLayoutProps) => (
<BaseLayout>
<FormLayoutStyle>
@@ -46,8 +49,9 @@ const FormLayout = ({
</Button>
</Form>
{!disableHomeLink && (
// <Link href={collectionId ? `/?collectionId=${collectionId}` : '/'}>{t('common:back-home')}</Link>
<Link href={PATHS.DASHBOARD}> Revenir à l'accueil</Link>
<Link href={appendCollectionId(PATHS.DASHBOARD, collectionId)}>
Back to home
</Link>
)}
</FormLayoutStyle>
</BaseLayout>

View File

@@ -2,11 +2,11 @@ import type Collection from '#models/collection';
export const appendCollectionId = (
url: string,
collectionId?: Collection['id']
) => `${url}${collectionId && `?collectionId=${collectionId}`}}`;
collectionId?: Collection['id'] | null | undefined
) => `${url}${collectionId ? `?collectionId=${collectionId}` : ''}`;
export const appendResourceId = (url: string, resourceId?: string) =>
`${url}${resourceId && `/${resourceId}`}}`;
`${url}${resourceId ? `/${resourceId}` : ''}`;
export function isValidHttpUrl(urlParam: string) {
let url;