mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
feat: add dropdown for links and collection header
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
98
inertia/components/dashboard/link_list/link_controls.tsx
Normal file
98
inertia/components/dashboard/link_list/link_controls.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user