feat: open search modal when query param is defined

This commit is contained in:
Sonny
2023-12-21 17:39:27 +01:00
committed by Sonny
parent f55d467f97
commit f7fed8bde4
13 changed files with 123 additions and 85 deletions

View File

@@ -4,14 +4,11 @@ import Footer from 'components/Footer/Footer';
import CreateItem from 'components/QuickActions/CreateItem';
import EditItem from 'components/QuickActions/EditItem';
import RemoveItem from 'components/QuickActions/RemoveItem';
import SearchModal from 'components/SearchModal/SearchModal';
import { motion } from 'framer-motion';
import useActiveCategory from 'hooks/useActiveCategory';
import { useTranslation } from 'next-i18next';
import LinkTag from 'next/link';
import { BiSearchAlt } from 'react-icons/bi';
import { RxHamburgerMenu } from 'react-icons/rx';
import quickActionStyles from '../QuickActions/quickactions.module.scss';
import LinkItem from './LinkItem';
import styles from './links.module.scss';
@@ -55,9 +52,6 @@ export default function Links({
)}
</span>
<span className={styles['category-controls']}>
<SearchModal childClassname={quickActionStyles['action']}>
<BiSearchAlt />
</SearchModal>
<CreateItem
type='link'
categoryId={id}

View File

@@ -1,8 +1,9 @@
import { motion } from 'framer-motion';
import { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { GrClose } from 'react-icons/gr';
import { motion } from 'framer-motion';
import styles from './modal.module.scss';
import clsx from 'clsx';
interface ModalProps {
close?: (...args: any) => void | Promise<void>;
@@ -13,6 +14,7 @@ interface ModalProps {
showCloseBtn?: boolean;
noHeader?: boolean;
padding?: string;
containerClassName?: string;
}
export default function Modal({
@@ -22,7 +24,12 @@ export default function Modal({
showCloseBtn = true,
noHeader = false,
padding = '1em 1.5em',
containerClassName = '',
}: ModalProps) {
const classNameContainer = clsx(
containerClassName,
styles['modal-container'],
);
const handleWrapperClick = (event) =>
event.target.classList?.[0] === styles['modal-wrapper'] && close && close();
@@ -35,7 +42,7 @@ export default function Modal({
exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }}
>
<motion.div
className={styles['modal-container']}
className={classNameContainer}
style={{ padding }}
initial={{ opacity: 0, y: '-6em' }}
animate={{ opacity: 1, y: 0 }}

View File

@@ -16,7 +16,6 @@
.modal-container {
background: $light-grey;
min-height: 250px;
min-width: 500px;
margin-top: 6em;
border-radius: 3px;

View File

@@ -0,0 +1,52 @@
import { useTranslation } from 'next-i18next';
interface SearchFilterProps {
canSearchLink: boolean;
setCanSearchLink: (value: boolean) => void;
canSearchCategory: boolean;
setCanSearchCategory: (value: boolean) => void;
}
export function SearchFilter({
canSearchLink,
setCanSearchLink,
canSearchCategory,
setCanSearchCategory,
}: Readonly<SearchFilterProps>) {
const { t } = useTranslation();
return (
<div
style={{
display: 'flex',
gap: '1em',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1em',
}}
>
<div style={{ display: 'flex', gap: '.25em' }}>
<input
type='checkbox'
name='filter-link'
id='filter-link'
onChange={({ target }) => setCanSearchLink(target.checked)}
checked={canSearchLink}
/>
<label htmlFor='filter-link'>{t('common:link.links')}</label>
</div>
<div style={{ display: 'flex', gap: '.25em' }}>
<input
type='checkbox'
name='filter-category'
id='filter-category'
onChange={({ target }) => setCanSearchCategory(target.checked)}
checked={canSearchCategory}
/>
<label htmlFor='filter-category'>
{t('common:category.categories')}
</label>
</div>
</div>
);
}

View File

@@ -13,11 +13,11 @@ export default function SearchListItem({
item,
selected,
closeModal,
}: {
}: Readonly<{
item: SearchItem;
selected: boolean;
closeModal: () => void;
}) {
}>) {
const id = useId();
const ref = useRef<HTMLLIElement>(null);
const { categories } = useCategories();

View File

@@ -12,6 +12,7 @@ import useGlobalHotkeys from 'hooks/useGlobalHotkeys';
import { useLocalStorage } from 'hooks/useLocalStorage';
import useModal from 'hooks/useModal';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import {
FormEvent,
ReactNode,
@@ -22,30 +23,35 @@ import {
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { BsSearch } from 'react-icons/bs';
import { CategoryWithLinks, LinkWithCategory, SearchItem } from 'types';
import { CategoryWithLinks, LinkWithCategory, SearchItem } from 'types/types';
import LabelSearchWithGoogle from './LabelSearchWithGoogle';
import { SearchFilter } from './SearchFilter';
import SearchList from './SearchList';
import styles from './search.module.scss';
interface SearchModalProps {
noHeader?: boolean;
children: ReactNode;
childClassname?: string;
}
export default function SearchModal({
noHeader = true,
children,
childClassname = '',
disableHotkeys = false,
}: {
noHeader?: boolean;
children: ReactNode;
childClassname?: string;
disableHotkeys?: boolean;
}) {
}: Readonly<SearchModalProps>) {
const { t } = useTranslation();
const router = useRouter();
const autoFocusRef = useAutoFocus();
const searchModal = useModal();
const { categories } = useCategories();
const { setActiveCategory } = useActiveCategory();
const { globalHotkeysEnabled, setGlobalHotkeysEnabled } = useGlobalHotkeys();
const searchQueryParam = (router.query.q as string) || '';
const searchModal = useModal(
!!searchQueryParam && typeof window !== 'undefined',
);
useEffect(
() => setGlobalHotkeysEnabled(!searchModal.isShowing),
[searchModal.isShowing, setGlobalHotkeysEnabled],
@@ -54,7 +60,10 @@ export default function SearchModal({
const handleCloseModal = useCallback(() => {
searchModal.close();
setSearch('');
}, [searchModal]);
router.replace({
query: undefined,
});
}, [router, searchModal]);
useHotkeys(
Keys.OPEN_SEARCH_KEY,
@@ -62,8 +71,9 @@ export default function SearchModal({
event.preventDefault();
searchModal.open();
},
{ enabled: !disableHotkeys && globalHotkeysEnabled },
{ enabled: globalHotkeysEnabled },
);
useHotkeys(Keys.CLOSE_SEARCH_KEY, handleCloseModal, {
enabled: searchModal.isShowing,
enableOnFormTags: ['INPUT'],
@@ -101,8 +111,7 @@ export default function SearchModal({
'search-category',
false,
);
const [search, setSearch] = useState<string>('');
const [search, setSearch] = useState<string>(searchQueryParam);
const [selectedItem, setSelectedItem] = useState<SearchItem | null>(
itemsSearch[0],
);
@@ -125,10 +134,7 @@ export default function SearchModal({
[canSearchCategory, canSearchLink, itemsSearch, search],
);
const handleSearchInputChange = useCallback(
(value: string) => setSearch(value),
[],
);
const handleSearchInputChange = (value: string) => setSearch(value);
const handleCanSearchLink = (checked: boolean) => setCanSearchLink(checked);
const handleCanSearchCategory = (checked: boolean) =>
@@ -192,7 +198,7 @@ export default function SearchModal({
placeholder={t('common:search')}
innerRef={autoFocusRef}
fieldClass={styles['search-input-field']}
inputClass={'reset'}
inputClass='reset'
/>
</div>
<SearchFilter
@@ -224,52 +230,3 @@ export default function SearchModal({
</>
);
}
function SearchFilter({
canSearchLink,
setCanSearchLink,
canSearchCategory,
setCanSearchCategory,
}: {
canSearchLink: boolean;
setCanSearchLink: (value: boolean) => void;
canSearchCategory: boolean;
setCanSearchCategory: (value: boolean) => void;
}) {
const { t } = useTranslation();
return (
<div
style={{
display: 'flex',
gap: '1em',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1em',
}}
>
<div style={{ display: 'flex', gap: '.25em' }}>
<input
type='checkbox'
name='filter-link'
id='filter-link'
onChange={({ target }) => setCanSearchLink(target.checked)}
checked={canSearchLink}
/>
<label htmlFor='filter-link'>{t('common:link.links')}</label>
</div>
<div style={{ display: 'flex', gap: '.25em' }}>
<input
type='checkbox'
name='filter-category'
id='filter-category'
onChange={({ target }) => setCanSearchCategory(target.checked)}
checked={canSearchCategory}
/>
<label htmlFor='filter-category'>
{t('common:category.categories')}
</label>
</div>
</div>
);
}

View File

@@ -26,7 +26,8 @@ form.search-form {
& input {
width: 100%;
font-size: 20px;
padding: 0.75em 0;
padding: 0.75em;
padding-left: 0;
border: 1px solid transparent;
}
}

View File

@@ -50,6 +50,7 @@ export default function SettingsModal() {
<Modal
title={t('common:settings')}
close={modal.close}
containerClassName={styles['modal']}
>
<Tabs className={styles['tabs']}>
<TabList className={clsx('reset', styles['tab-list'])}>

View File

@@ -1,5 +1,9 @@
@import 'styles/colors.scss';
.modal {
min-height: 250px;
}
.tabs {
width: 100%;
display: flex;

View File

@@ -1,9 +1,16 @@
import ButtonLink from 'components/ButtonLink';
import SearchModal from 'components/SearchModal/SearchModal';
import PATHS from 'constants/paths';
import useActiveCategory from 'hooks/useActiveCategory';
import { useTranslation } from 'next-i18next';
import styles from './side-nav.module.scss';
import dynamic from 'next/dynamic';
const SearchModal = dynamic(
() => {
return import('components/SearchModal/SearchModal');
},
{ ssr: false },
);
export default function NavigationLinks() {
const { t } = useTranslation();
@@ -12,7 +19,7 @@ export default function NavigationLinks() {
return (
<div className={styles['menu-controls']}>
<div className={styles['action']}>
<SearchModal disableHotkeys>{t('common:search')}</SearchModal>
<SearchModal>{t('common:search')}</SearchModal>
<kbd>S</kbd>
</div>
<div className={styles['action']}>

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
const useModal = () => {
const [isShowing, setIsShowing] = useState<boolean>(false);
const useModal = (defaultValue: boolean = false) => {
const [isShowing, setIsShowing] = useState<boolean>(defaultValue);
const toggle = () => setIsShowing((value) => !value);
const open = () => setIsShowing(true);

View File

@@ -31,7 +31,6 @@ const Document = () => (
/>
</Head>
<body>
<noscript>Vous devez activer JavaScript pour utiliser ce site</noscript>
<Main />
<NextScript />
</body>

View File

@@ -159,6 +159,7 @@ function HomeProviders(
export const getServerSideProps = withAuthentication(
async ({ query, session, user, locale }) => {
const queryCategoryId = (query?.categoryId as string) || '';
const searchQueryParam = (query?.q as string)?.toLowerCase() || '';
const categories = await getUserCategories(user);
if (categories.length === 0) {
@@ -169,6 +170,22 @@ export const getServerSideProps = withAuthentication(
};
}
const link = categories
.map((category) => category.links)
.flat()
.find(
(link: LinkWithCategory) =>
link.name.toLowerCase() === searchQueryParam ||
link.url.toLowerCase() === searchQueryParam,
);
if (link) {
return {
redirect: {
destination: link.url,
},
};
}
const activeCategory = categories.find(
({ id }) => id === Number(queryCategoryId),
);