feat: add dropdown component

This commit is contained in:
Sonny
2024-05-12 20:06:05 +02:00
committed by Sonny
parent 3531038321
commit 0f1dc9b69c
13 changed files with 142 additions and 16 deletions

View File

@@ -26,13 +26,11 @@ export default class FaviconsController {
]; ];
async index(ctx: HttpContext) { async index(ctx: HttpContext) {
console.log('0');
const url = ctx.request.qs()?.url; const url = ctx.request.qs()?.url;
if (!url) { if (!url) {
throw new Error('Missing URL'); throw new Error('Missing URL');
} }
console.log('1');
const faviconRequestUrl = this.buildFaviconUrl(url, '/favicon.ico'); const faviconRequestUrl = this.buildFaviconUrl(url, '/favicon.ico');
try { try {
const favicon = await this.getFavicon(faviconRequestUrl); const favicon = await this.getFavicon(faviconRequestUrl);
@@ -42,7 +40,6 @@ export default class FaviconsController {
`[Favicon] [first: ${faviconRequestUrl}] Unable to retrieve favicon from favicon.ico url` `[Favicon] [first: ${faviconRequestUrl}] Unable to retrieve favicon from favicon.ico url`
); );
} }
console.log('2');
const requestDocument = await this.makeRequestWithUserAgent(url); const requestDocument = await this.makeRequestWithUserAgent(url);
const documentAsText = await requestDocument.text(); const documentAsText = await requestDocument.text();
@@ -57,7 +54,6 @@ export default class FaviconsController {
return this.sendDefaultImage(ctx); return this.sendDefaultImage(ctx);
} }
console.log('3');
const finalUrl = this.buildFaviconUrl(requestDocument.url, faviconPath); const finalUrl = this.buildFaviconUrl(requestDocument.url, faviconPath);
try { try {
if (!faviconPath) { if (!faviconPath) {
@@ -186,14 +182,12 @@ export default class FaviconsController {
Buffer.from(base64, 'base64'); Buffer.from(base64, 'base64');
private sendImage(ctx: HttpContext, { buffer, type, size }: Favicon) { private sendImage(ctx: HttpContext, { buffer, type, size }: Favicon) {
console.log('ouiiiiiiii', type, size);
ctx.response.header('Content-Type', type); ctx.response.header('Content-Type', type);
ctx.response.header('Content-Length', size); ctx.response.header('Content-Length', size);
ctx.response.send(buffer); ctx.response.send(buffer);
} }
private sendDefaultImage(ctx: HttpContext) { private sendDefaultImage(ctx: HttpContext) {
console.log('oui');
const readStream = createReadStream( const readStream = createReadStream(
resolve(process.cwd(), './public/empty-image.png') resolve(process.cwd(), './public/empty-image.png')
); );

View File

@@ -0,0 +1,56 @@
import KEYS from '#constants/keys';
import styled from '@emotion/styled';
import { ReactNode, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import DropdownContainer from '~/components/common/dropdown/dropdown_container';
import DropdownLabel from '~/components/common/dropdown/dropdown_label';
import useClickOutside from '~/hooks/use_click_outside';
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
import useToggle from '~/hooks/use_modal';
const DropdownStyle = styled.div<{ opened: boolean }>(({ opened, theme }) => ({
cursor: 'pointer',
userSelect: 'none',
position: 'relative',
minWidth: 'fit-content',
width: 'fit-content',
maxWidth: '250px',
backgroundColor: opened ? theme.colors.secondary : theme.colors.background,
padding: '4px',
borderRadius: theme.border.radius,
'&:hover': {
backgroundColor: theme.colors.secondary,
},
'& svg': {
height: '24px',
width: '24px',
},
}));
export default function Dropdown({
children,
label,
}: {
children: ReactNode;
label: ReactNode | string;
}) {
const dropdownRef = useRef<HTMLDivElement>(null);
const { isShowing, toggle, close } = useToggle();
const { globalHotkeysEnabled } = useGlobalHotkeys();
useClickOutside(dropdownRef, close);
useHotkeys(KEYS.ESCAPE_KEY, close, {
enabled: globalHotkeysEnabled,
enableOnFormTags: ['INPUT'],
});
return (
<DropdownStyle opened={isShowing} onClick={toggle} ref={dropdownRef}>
<DropdownLabel>{label}</DropdownLabel>
<DropdownContainer show={isShowing}>{children}</DropdownContainer>
</DropdownStyle>
);
}

View File

@@ -0,0 +1,17 @@
import styled from '@emotion/styled';
const DropdownContainer = styled.div<{ show: boolean }>(({ show, theme }) => ({
position: 'absolute',
top: 'calc(100% + 0.5em)',
right: 0,
minWidth: '175px',
backgroundColor: show ? theme.colors.secondary : theme.colors.background,
border: `2px solid ${theme.colors.secondary}`,
borderRadius: theme.border.radius,
boxShadow: theme.colors.boxShadow,
display: show ? 'flex' : 'none',
flexDirection: 'column',
overflow: 'hidden',
}));
export default DropdownContainer;

View File

@@ -0,0 +1,16 @@
import styled from '@emotion/styled';
const DropdownItem = styled.div(({ theme }) => ({
fontSize: '14px',
padding: '8px 12px',
display: 'flex',
gap: '0.35em',
alignItems: 'center',
borderRadius: theme.border.radius,
'&:hover': {
backgroundColor: theme.colors.background,
},
}));
export default DropdownItem;

View File

@@ -0,0 +1,11 @@
import styled from '@emotion/styled';
const DropdownLabel = styled.p(({ theme }) => ({
height: 'auto',
width: 'auto',
color: theme.colors.font,
display: 'flex',
gap: '0.35em',
}));
export default DropdownLabel;

View File

@@ -4,13 +4,16 @@ import { router } from '@inertiajs/react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import useGlobalHotkeys from '~/hooks/use_global_hotkeys'; import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
import useSearchParam from '~/hooks/use_search_param';
import { appendCollectionId } from '~/lib/navigation';
export default function BackToDashboard({ children }: { children: ReactNode }) { export default function BackToDashboard({ children }: { children: ReactNode }) {
const collectionId = useSearchParam('collectionId');
const { globalHotkeysEnabled } = useGlobalHotkeys(); const { globalHotkeysEnabled } = useGlobalHotkeys();
useHotkeys( useHotkeys(
KEYS.ESCAPE_KEY, KEYS.ESCAPE_KEY,
() => { () => {
router.visit(PATHS.DASHBOARD); router.visit(appendCollectionId(PATHS.DASHBOARD, collectionId));
}, },
{ enabled: globalHotkeysEnabled, enableOnFormTags: ['INPUT'] } { enabled: globalHotkeysEnabled, enableOnFormTags: ['INPUT'] }
); );

View File

@@ -0,0 +1,22 @@
import { RefObject, useEffect } from 'react';
// Source : https://stackoverflow.com/a/63359693
/**
* This Hook can be used for detecting clicks outside the Opened Menu
*/
export default function useClickOutside(
ref: RefObject<HTMLElement>,
onClickOutside: () => void
) {
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (ref?.current && !ref.current?.contains(event.target as any)) {
onClickOutside();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [ref, onClickOutside]);
}

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
const useModal = (defaultValue: boolean = false) => { const useToggle = (defaultValue: boolean = false) => {
const [isShowing, setIsShowing] = useState<boolean>(defaultValue); const [isShowing, setIsShowing] = useState<boolean>(defaultValue);
const toggle = () => setIsShowing((value) => !value); const toggle = () => setIsShowing((value) => !value);
@@ -15,4 +15,4 @@ const useModal = (defaultValue: boolean = false) => {
}; };
}; };
export default useModal; export default useToggle;

View File

@@ -2,7 +2,7 @@ import { useForm } from '@inertiajs/react';
import { ChangeEvent, FormEvent, useMemo } from 'react'; import { ChangeEvent, FormEvent, useMemo } from 'react';
import FormField from '~/components/common/form/_form_field'; import FormField from '~/components/common/form/_form_field';
import TextBox from '~/components/common/form/textbox'; import TextBox from '~/components/common/form/textbox';
import BackToDashboard from '~/components/common/navigation/bask_to_dashboard'; import BackToDashboard from '~/components/common/navigation/back_to_dashboard';
import FormLayout from '~/components/layouts/form_layout'; import FormLayout from '~/components/layouts/form_layout';
import { Visibility } from '../../../app/enums/visibility'; import { Visibility } from '../../../app/enums/visibility';

View File

@@ -16,7 +16,7 @@ import CollectionsContext from '~/contexts/collections_context';
import FavoritesContext from '~/contexts/favorites_context'; import FavoritesContext from '~/contexts/favorites_context';
import GlobalHotkeysContext from '~/contexts/global_hotkeys_context'; import GlobalHotkeysContext from '~/contexts/global_hotkeys_context';
import { useMediaQuery } from '~/hooks/use_media_query'; import { useMediaQuery } from '~/hooks/use_media_query';
import useModal from '~/hooks/use_modal'; import useToggle from '~/hooks/use_modal';
interface HomePageProps { interface HomePageProps {
collections: Collection[]; collections: Collection[];
@@ -31,7 +31,7 @@ const SideBar = styled.div(({ theme }) => ({
export default function HomePage(props: Readonly<HomePageProps>) { export default function HomePage(props: Readonly<HomePageProps>) {
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery('(max-width: 768px)');
const { isShowing, open, close } = useModal(); const { isShowing, open, close } = useToggle();
const handlers = useSwipeable({ const handlers = useSwipeable({
trackMouse: true, trackMouse: true,
onSwipedRight: open, onSwipedRight: open,

View File

@@ -3,7 +3,7 @@ import { useForm } from '@inertiajs/react';
import { ChangeEvent, FormEvent, useMemo } from 'react'; import { ChangeEvent, FormEvent, useMemo } from 'react';
import FormField from '~/components/common/form/_form_field'; import FormField from '~/components/common/form/_form_field';
import TextBox from '~/components/common/form/textbox'; import TextBox from '~/components/common/form/textbox';
import BackToDashboard from '~/components/common/navigation/bask_to_dashboard'; import BackToDashboard from '~/components/common/navigation/back_to_dashboard';
import FormLayout from '~/components/layouts/form_layout'; import FormLayout from '~/components/layouts/form_layout';
import useSearchParam from '~/hooks/use_search_param'; import useSearchParam from '~/hooks/use_search_param';
import { isValidHttpUrl } from '~/lib/navigation'; import { isValidHttpUrl } from '~/lib/navigation';
@@ -46,6 +46,7 @@ export default function CreateLinkPage({
title="Create a link" title="Create a link"
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
canSubmit={canSubmit} canSubmit={canSubmit}
collectionId={collectionId}
> >
<BackToDashboard> <BackToDashboard>
<TextBox <TextBox

View File

@@ -34,7 +34,7 @@ export const lightTheme: Theme = {
primary: primaryColor, primary: primaryColor,
secondary: '#fff', secondary: '#fff',
white: '#fff', white: '#ffffff',
lightGrey: '#dadce0', lightGrey: '#dadce0',
grey: '#888888', grey: '#888888',
@@ -46,6 +46,8 @@ export const lightTheme: Theme = {
lightRed, lightRed,
yellow, yellow,
boxShadow: '0 0 1em 0 rgba(102, 102, 102, 0.25)',
}, },
border, border,
@@ -60,10 +62,10 @@ export const darkTheme: Theme = {
primary: '#4fadfc', primary: '#4fadfc',
secondary: '#323a47', secondary: '#323a47',
white: '#fff', white: '#ffffff',
lightGrey: '#323a47', lightGrey: '#323a47',
grey: '#888888', grey: '#999999',
lightBlue, lightBlue,
blue: '#4fadfc', blue: '#4fadfc',
@@ -72,6 +74,8 @@ export const darkTheme: Theme = {
lightRed, lightRed,
yellow, yellow,
boxShadow: '0 0 1em 0 rgb(40 40 40)',
}, },
border, border,

View File

@@ -20,6 +20,8 @@ declare module '@emotion/react' {
lightRed: string; lightRed: string;
yellow: string; yellow: string;
boxShadow: string;
}; };
border: { border: {