mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
feat: open search modal when query param is defined
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
.modal-container {
|
||||
background: $light-grey;
|
||||
min-height: 250px;
|
||||
min-width: 500px;
|
||||
margin-top: 6em;
|
||||
border-radius: 3px;
|
||||
|
||||
52
src/components/SearchModal/SearchFilter.tsx
Normal file
52
src/components/SearchModal/SearchFilter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'])}>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@import 'styles/colors.scss';
|
||||
|
||||
.modal {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -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']}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -31,7 +31,6 @@ const Document = () => (
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<noscript>Vous devez activer JavaScript pour utiliser ce site</noscript>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user