mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 15:05:35 +00:00
feat: add dropdown component
This commit is contained in:
@@ -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')
|
||||||
);
|
);
|
||||||
|
|||||||
56
inertia/components/common/dropdown/dropdown.tsx
Normal file
56
inertia/components/common/dropdown/dropdown.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
inertia/components/common/dropdown/dropdown_container.tsx
Normal file
17
inertia/components/common/dropdown/dropdown_container.tsx
Normal 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;
|
||||||
16
inertia/components/common/dropdown/dropdown_item.tsx
Normal file
16
inertia/components/common/dropdown/dropdown_item.tsx
Normal 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;
|
||||||
11
inertia/components/common/dropdown/dropdown_label.tsx
Normal file
11
inertia/components/common/dropdown/dropdown_label.tsx
Normal 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;
|
||||||
@@ -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'] }
|
||||||
);
|
);
|
||||||
22
inertia/hooks/use_click_outside.tsx
Normal file
22
inertia/hooks/use_click_outside.tsx
Normal 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]);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
2
inertia/types/emotion.d.ts
vendored
2
inertia/types/emotion.d.ts
vendored
@@ -20,6 +20,8 @@ declare module '@emotion/react' {
|
|||||||
lightRed: string;
|
lightRed: string;
|
||||||
|
|
||||||
yellow: string;
|
yellow: string;
|
||||||
|
|
||||||
|
boxShadow: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
border: {
|
border: {
|
||||||
|
|||||||
Reference in New Issue
Block a user