diff --git a/inertia/components/dashboard/dashboard_navbar.tsx b/inertia/components/dashboard/dashboard_navbar.tsx index fc6cb44..630ac71 100644 --- a/inertia/components/dashboard/dashboard_navbar.tsx +++ b/inertia/components/dashboard/dashboard_navbar.tsx @@ -1,4 +1,4 @@ -import { Link } from '@inertiajs/react'; +import { Link, router } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { AppShell, @@ -11,6 +11,7 @@ import { ScrollArea, Text, } from '@mantine/core'; +import { openSpotlight } from '@mantine/spotlight'; import { useTranslation } from 'react-i18next'; import { AiOutlineFolderAdd } from 'react-icons/ai'; import { IoIosSearch } from 'react-icons/io'; @@ -18,7 +19,12 @@ import { IoAdd, IoShieldHalfSharp } from 'react-icons/io5'; import { PiGearLight } from 'react-icons/pi'; import { MantineUserCard } from '~/components/common/user_card'; import { FavoriteList } from '~/components/dashboard/favorite/favorite_list'; +import { SearchSpotlight } from '~/components/search/search'; +import useShortcut from '~/hooks/use_shortcut'; import useUser from '~/hooks/use_user'; +import { appendCollectionId } from '~/lib/navigation'; +import { useActiveCollection } from '~/stores/collection_store'; +import { useGlobalHotkeysStore } from '~/stores/global_hotkeys_store'; interface DashboardNavbarProps { isOpen: boolean; @@ -27,6 +33,11 @@ interface DashboardNavbarProps { export function DashboardNavbar({ isOpen, toggle }: DashboardNavbarProps) { const { t } = useTranslation('common'); const { isAuthenticated, user } = useUser(); + + const { activeCollection } = useActiveCollection(); + const { globalHotkeysEnabled, setGlobalHotkeysEnabled } = + useGlobalHotkeysStore(); + const common = { variant: 'subtle', color: 'blue', @@ -40,6 +51,33 @@ export function DashboardNavbar({ isOpen, toggle }: DashboardNavbarProps) { }, }, }; + + useShortcut( + 'OPEN_CREATE_LINK_KEY', + () => + router.visit( + appendCollectionId(route('link.create-form').url, activeCollection?.id) + ), + { + enabled: globalHotkeysEnabled, + } + ); + + useShortcut( + 'OPEN_CREATE_COLLECTION_KEY', + () => router.visit(route('collection.create-form').url), + { + enabled: globalHotkeysEnabled, + } + ); + + useShortcut('OPEN_SEARCH_KEY', () => openSpotlight(), { + enabled: globalHotkeysEnabled, + }); + + const onSpotlightOpen = () => setGlobalHotkeysEnabled(false); + const onSpotlightClose = () => setGlobalHotkeysEnabled(true); + return ( @@ -65,12 +103,21 @@ export function DashboardNavbar({ isOpen, toggle }: DashboardNavbarProps) { color="var(--mantine-color-text)" disabled /> - } - disabled - /> + <> + {/* Search button */} + } + onClick={() => openSpotlight()} + rightSection={S} + /> + {/* Search spotlight / modal */} + + void; + closeCallback?: () => void; +} + +export function SearchSpotlight({ + openCallback, + closeCallback, +}: SearchSpotlightProps) { + const { t } = useTranslation('common'); + + const [searchTerm, setSearchTerm] = useState(''); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (searchTerm.trim() === '') { + return setResults([]); + } + + setLoading(true); + const controller = new AbortController(); + const { path, method } = route('search', { qs: { term: searchTerm } }); + makeRequest({ + method, + url: path, + controller, + }) + .then(({ results: _results }) => { + setResults(_results); + setLoading(false); + }) + .catch((error) => { + if (error! instanceof DOMException) { + setLoading(false); + } + }); + + return () => { + controller.abort('Canceled by user'); + setLoading(false); + }; + }, [searchTerm]); + + const handleResultSubmit = (result: SearchResult) => { + if (result.type === 'collection') { + return router.visit( + appendCollectionId(route('dashboard').path, result.id) + ); + } + + if (result.type === 'link') { + return window.open(result.url, '_blank'); + } + }; + + const actions: SpotlightActionData[] = results.map((result) => ({ + id: `${result.id.toString()}${result.type}`, + label: result.name, + description: result.description, + onClick: () => handleResultSubmit(result), + leftSection: + result.type === 'collection' ? ( + + ) : ( + + ), + })); + + const spotlightPromptText = loading + ? t('loading') + : searchTerm.trim() === '' + ? t('search') + : t('no-results'); + + return ( + , + placeholder: t('search'), + }} + onQueryChange={(query) => setSearchTerm(query)} + onSpotlightOpen={openCallback} + onSpotlightClose={closeCallback} + clearQueryOnClose + closeOnActionTrigger + /> + ); +} diff --git a/inertia/i18n/locales/en/common.json b/inertia/i18n/locales/en/common.json index 9614bce..fa4fe6c 100644 --- a/inertia/i18n/locales/en/common.json +++ b/inertia/i18n/locales/en/common.json @@ -78,5 +78,7 @@ "member-since": "Member since", "footer": { "made_by": "Made with ❤\uFE0F by" - } + }, + "loading": "Loading...", + "no-results": "No results found" } \ No newline at end of file diff --git a/inertia/i18n/locales/fr/common.json b/inertia/i18n/locales/fr/common.json index 5967b9a..c676f8e 100644 --- a/inertia/i18n/locales/fr/common.json +++ b/inertia/i18n/locales/fr/common.json @@ -78,5 +78,7 @@ "member-since": "Membre depuis", "footer": { "made_by": "Fait avec ❤\uFE0F par" - } + }, + "loading": "Chargement...", + "no-results": "Aucun résultat trouvé" } \ No newline at end of file diff --git a/inertia/pages/dashboard.tsx b/inertia/pages/dashboard.tsx index 6f5f3bc..8e4d5f2 100644 --- a/inertia/pages/dashboard.tsx +++ b/inertia/pages/dashboard.tsx @@ -1,5 +1,3 @@ -import { router } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { AppShell, ScrollArea } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { useEffect } from 'react'; @@ -11,12 +9,10 @@ import { MantineFooter } from '~/components/footer/footer'; import { useDisableOverflow } from '~/hooks/use_disable_overflow'; import useShortcut from '~/hooks/use_shortcut'; import { DashboardLayout } from '~/layouts/dashboard_layout'; -import { appendCollectionId } from '~/lib/navigation'; import { useActiveCollection, useCollectionsSetter, } from '~/stores/collection_store'; -import { useGlobalHotkeysStore } from '~/stores/global_hotkeys_store'; import { CollectionWithLinks } from '~/types/app'; import classes from './dashboard.module.css'; @@ -36,7 +32,6 @@ export default function MantineDashboard(props: Readonly) { const { activeCollection } = useActiveCollection(); const { _setCollections, setActiveCollection } = useCollectionsSetter(); - const { globalHotkeysEnabled } = useGlobalHotkeysStore(); useShortcut('ESCAPE_KEY', () => { closeNavbar(); @@ -50,24 +45,6 @@ export default function MantineDashboard(props: Readonly) { setActiveCollection(props.activeCollection); }, []); - useShortcut( - 'OPEN_CREATE_LINK_KEY', - () => - router.visit( - appendCollectionId(route('link.create-form').url, activeCollection?.id) - ), - { - enabled: globalHotkeysEnabled, - } - ); - useShortcut( - 'OPEN_CREATE_COLLECTION_KEY', - () => router.visit(route('collection.create-form').url), - { - enabled: globalHotkeysEnabled, - } - ); - const headerHeight = !!activeCollection?.description ? HEADER_SIZE_WITH_DESCRIPTION : HEADER_SIZE_WITHOUT_DESCRIPTION; diff --git a/inertia/types/search.d.ts b/inertia/types/search.d.ts index ebc13aa..1f4e535 100644 --- a/inertia/types/search.d.ts +++ b/inertia/types/search.d.ts @@ -3,6 +3,7 @@ type SearchResultCommon = { name: string; matched_part?: string; rank?: number; + description?: string; }; export type SearchResultCollection = SearchResultCommon & { @@ -11,7 +12,7 @@ export type SearchResultCollection = SearchResultCommon & { export type SearchResultLink = SearchResultCommon & { type: 'link'; - collection_id: number; + collectionId: number; url: string; }; diff --git a/package.json b/package.json index 54c7378..fd7f47c 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@izzyjs/route": "^1.2.0", "@mantine/core": "^7.13.4", "@mantine/hooks": "^7.13.4", - "@mantine/spotlight": "^7.13.4", + "@mantine/spotlight": "^7.13.5", "@vinejs/vine": "^2.1.0", "bentocache": "^1.0.0-beta.9", "dayjs": "^1.11.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffd6cd5..c0f96cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,8 +51,8 @@ importers: specifier: ^7.13.4 version: 7.13.4(react@18.3.1) '@mantine/spotlight': - specifier: ^7.13.4 - version: 7.13.4(@mantine/core@7.13.4(@mantine/hooks@7.13.4(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.4(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.13.5 + version: 7.13.5(@mantine/core@7.13.4(@mantine/hooks@7.13.4(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.4(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@vinejs/vine': specifier: ^2.1.0 version: 2.1.0 @@ -931,18 +931,18 @@ packages: peerDependencies: react: ^18.2.0 - '@mantine/spotlight@7.13.4': - resolution: {integrity: sha512-rplcuOa9tSia8WmWjdqQvn/WG76BQ9d2/Gy6t3e5wHM3TURPPFYCcwUHp9HUvNELj98Hx6TUJ9BmZqyi/TxIPg==} + '@mantine/spotlight@7.13.5': + resolution: {integrity: sha512-gVwhyDNJxjnvKZG44O99g4fKVsVJEmo3CZC/aMkoRoz9IvKrRpWx11pPfsk/eQJ5kp5nsy49fNRX24yKRkW7Mg==} peerDependencies: - '@mantine/core': 7.13.4 - '@mantine/hooks': 7.13.4 - react: ^18.2.0 - react-dom: ^18.2.0 + '@mantine/core': 7.13.5 + '@mantine/hooks': 7.13.5 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x - '@mantine/store@7.13.4': - resolution: {integrity: sha512-DUlnXizE7aCjbVg2J3XLLKsOzt2c2qfQl2Xmx9l/BPE4FFZZKUqGDkYaTDbTAmnN3FVZ9xXycL7bAlq9udO8mA==} + '@mantine/store@7.13.5': + resolution: {integrity: sha512-+enhEaZpVKn0x3PLN3Txlk/06eIuq2wQhlFQnBe4dnD+C9VZZhXdff/IYCtXwB4XojwJl3rln7BSL4Ih4rSGmw==} peerDependencies: - react: ^18.2.0 + react: ^18.x || ^19.x '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} @@ -5699,15 +5699,15 @@ snapshots: dependencies: react: 18.3.1 - '@mantine/spotlight@7.13.4(@mantine/core@7.13.4(@mantine/hooks@7.13.4(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.4(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mantine/spotlight@7.13.5(@mantine/core@7.13.4(@mantine/hooks@7.13.4(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.13.4(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@mantine/core': 7.13.4(@mantine/hooks@7.13.4(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/hooks': 7.13.4(react@18.3.1) - '@mantine/store': 7.13.4(react@18.3.1) + '@mantine/store': 7.13.5(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@mantine/store@7.13.4(react@18.3.1)': + '@mantine/store@7.13.5(react@18.3.1)': dependencies: react: 18.3.1