Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New Search Command Palette UI #164

Merged
merged 2 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions apps/client/app/components/layout/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { UserAccountMobileNav, UserAccountNav } from './user-account/index.tsx'
import { Button } from '@/components/button/index.tsx'
import { APP_NAME, PAGES } from '@/constants/index.ts'
import useScroll from '@/hooks/use-scroll.ts'
import { classNames } from '@/lib/classNames.ts'
import { useAuth } from '@/providers/auth/index.tsx'

const navigation = (isLoggedIn: boolean) => [
Expand All @@ -26,27 +27,28 @@ const navigation = (isLoggedIn: boolean) => [
interface Props {
isHeroSearchInVisible: boolean
shouldHeaderBlur?: boolean
searchQuery?: string
}

export const Header = ({
isHeroSearchInVisible,
shouldHeaderBlur = true,
searchQuery,
}: Props) => {
const { isLoggedIn } = useAuth()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const scrolled = useScroll(50)

const headerBlurred = shouldHeaderBlur
? 'bg-white/95 backdrop-blur-xl sticky top-0 z-50'
: 'sticky top-0 z-50 bg-white'
? 'bg-white/95 backdrop-blur-xl sticky top-0 z-50 shadow-lg'
: 'sticky top-0 z-50 bg-white '

return (
<header
className={
isHeroSearchInVisible
? undefined
: `${scrolled ? headerBlurred : 'sticky top-0 z-50 bg-white'}`
}
className={classNames({
[headerBlurred]: !isHeroSearchInVisible && scrolled,
'z sticky top-0 z-50 bg-white': !isHeroSearchInVisible && !scrolled,
})}
>
<NoticeBanner />
<nav
Expand All @@ -62,7 +64,9 @@ export const Header = ({
</div>
</Link>
<div className="mx-8 hidden h-11 flex-grow md:flex">
{isHeroSearchInVisible ? null : <SearchPhotos />}
{isHeroSearchInVisible ? null : (
<SearchPhotos searchQuery={searchQuery} />
)}
</div>
<div className="flex lg:hidden">
<button
Expand Down Expand Up @@ -103,7 +107,7 @@ export const Header = ({
</nav>
{isHeroSearchInVisible ? null : (
<div className="mx-4 pb-4 md:pb-0">
<SearchPhotosForMobile />
<SearchPhotosForMobile searchQuery={searchQuery} />
</div>
)}
<Dialog
Expand Down
160 changes: 84 additions & 76 deletions apps/client/app/components/layout/header/search-for-mobile/index.tsx
Original file line number Diff line number Diff line change
@@ -1,114 +1,122 @@
import { Transition, Dialog } from '@headlessui/react'
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
import {
MagnifyingGlassIcon,
ViewfinderCircleIcon,
} from '@heroicons/react/24/outline'
import { XMarkIcon } from '@heroicons/react/24/solid'
import { Fragment, useState } from 'react'
import { TextSearch } from '../search/text/index.tsx'
import { useState } from 'react'
import { SearchPalette } from '../search/search-palette/index.tsx'
import { VisualSearch } from '../search/visual/index.tsx'
import { Button } from '@/components/button/index.tsx'

interface Props {
onClose: VoidFunction
isOpened: boolean
searchQuery?: string
}

const MENUS = {
TEXT_SEARCH: 'TEXT_SEARCH',
VISUAL_SEARCH: 'VISUAL_SEARCH',
}

const SearchModal = ({ onClose }: Props) => {
const [activeMenu, setActiveMenu] = useState(MENUS.TEXT_SEARCH)
const SearchModal = ({ isOpened, onClose, searchQuery }: Props) => {
const [activeMenu, setActiveMenu] = useState<string>('')

return (
<div className="py-2">
<div className="flex flex-row items-center justify-between px-3">
<div />
<div>
<div className="flex flex-row items-center rounded-lg bg-zinc-100">
<button
onClick={() => setActiveMenu(MENUS.TEXT_SEARCH)}
type="button"
className={`p-4 ${
activeMenu === MENUS.TEXT_SEARCH
? 'rounded-l-lg bg-zinc-300'
: ''
} `}
<Dialog
className="relative z-50"
open={isOpened}
onClose={() => {
onClose()
setActiveMenu('')
}}
>
<DialogBackdrop
transition
className="fixed inset-0 bg-black/20 backdrop-blur-sm transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
/>

<div className="fixed inset-0 z-10 flex w-screen flex-col justify-center overflow-y-auto p-4 sm:p-6 md:p-20">
{activeMenu === MENUS.VISUAL_SEARCH ? (
<DialogPanel
transition
className="mx-auto w-auto transform divide-y divide-gray-100 overflow-hidden rounded-xl bg-zinc-100 p-3 shadow-2xl ring-1 ring-black/5 transition-all data-[closed]:scale-95 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
>
<VisualSearch />
</DialogPanel>
) : null}

{activeMenu === MENUS.TEXT_SEARCH ? (
<SearchPalette
isOpened={true}
onClose={() => {
onClose()
setActiveMenu('')
}}
searchQuery={searchQuery}
/>
) : null}

{activeMenu === '' ? (
<>
<DialogPanel
transition
className="mx-auto flex w-auto transform items-center justify-center divide-y divide-gray-100 overflow-hidden rounded-xl bg-zinc-100 shadow-2xl ring-1 ring-black/5 transition-all data-[closed]:scale-95 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
>
<MagnifyingGlassIcon className="h-5 w-5" />
</button>
<button
onClick={() => setActiveMenu(MENUS.VISUAL_SEARCH)}
type="button"
className={`p-4 ${
activeMenu === MENUS.VISUAL_SEARCH
? 'rounded-r-lg bg-zinc-300'
: ''
} `}
<button
onClick={() => setActiveMenu(MENUS.TEXT_SEARCH)}
type="button"
className={`mx-10 p-4`}
>
<MagnifyingGlassIcon className="size-32 text-zinc-400" />
<h1 className="text-lg font-bold">Textual Search</h1>
</button>
</DialogPanel>
<DialogPanel
transition
className="mx-auto mt-5 flex w-auto transform items-center justify-center divide-y divide-gray-100 overflow-hidden rounded-xl bg-zinc-100 shadow-2xl ring-1 ring-black/5 transition-all data-[closed]:scale-95 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
>
<ViewfinderCircleIcon className="h-5 w-5" />
</button>
</div>
</div>
<Button onClick={onClose} variant="unstyled">
<XMarkIcon className="h-7 w-7 text-zinc-600" />
</Button>
<button
onClick={() => setActiveMenu(MENUS.VISUAL_SEARCH)}
type="button"
className={`mx-10 p-4`}
>
<ViewfinderCircleIcon className="size-32 text-zinc-400" />
<h1 className="text-lg font-bold">Visual Search</h1>
</button>
</DialogPanel>
</>
) : null}
</div>

{activeMenu === MENUS.TEXT_SEARCH ? (
<>
<div className="mt-5">
<input
type="text"
className="w-full rounded-md border-none bg-zinc-100 py-3 text-gray-900 placeholder:text-gray-500 focus:ring-0"
placeholder="Search for photos"
/>
</div>
<div className="-mx-2 mt-5 h-[58vh] overflow-y-scroll px-2">
<TextSearch />
</div>
</>
) : null}

{activeMenu === MENUS.VISUAL_SEARCH ? <VisualSearch /> : null}
</div>
</Dialog>
)
}

export const SearchPhotosForMobile = () => {
interface SearchPhotosForMobileProps {
searchQuery?: string
}

export const SearchPhotosForMobile = ({
searchQuery,
}: SearchPhotosForMobileProps) => {
const [isSearchFocused, setIsSearchFocused] = useState(false)

return (
<>
<div className="flex md:hidden">
<Button
variant="unstyled"
variant="outlined"
onClick={() => setIsSearchFocused(true)}
className="flex-start block flex w-full items-center rounded-full border border-zinc-400 bg-white px-5 py-2 text-base font-medium text-gray-400"
className="flex-start flex w-full items-center justify-start rounded-full px-5 py-3 text-base font-medium text-gray-400"
>
<MagnifyingGlassIcon className="mr-3 h-5 w-5 text-zinc-400" />
Search for photos
{searchQuery ?? 'Search for photos'}
</Button>
</div>
<Transition show={isSearchFocused} as={Fragment}>
<Dialog onClose={() => setIsSearchFocused(false)} className="">
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
<Transition.Child
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<div className="absolute bottom-0 z-50 mt-2 h-[80vh] w-screen overflow-hidden rounded-lg rounded-t-xl border border-zinc-200 bg-white p-2 shadow-lg ring-1 ring-gray-900/5">
<SearchModal onClose={() => setIsSearchFocused(false)} />
</div>
</Transition.Child>
</Dialog>
</Transition>
<SearchModal
isOpened={isSearchFocused}
onClose={() => setIsSearchFocused(false)}
/>
</>
)
}
60 changes: 19 additions & 41 deletions apps/client/app/components/layout/header/search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Transition, Popover } from '@headlessui/react'
import {
MagnifyingGlassIcon,
XMarkIcon,
ViewfinderCircleIcon,
} from '@heroicons/react/24/outline'
import { Fragment, useState } from 'react'
import { TextSearch } from './text/index.tsx'
import { SearchPalette } from './search-palette/index.tsx'
import { VisualSearch } from './visual/index.tsx'
import { Button } from '@/components/button/index.tsx'

interface Props {
isSittingOnADarkBackground?: true
searchQuery?: string
}

export const SearchPhotos = ({ isSittingOnADarkBackground }: Props) => {
const [query, setQuery] = useState('')
export const SearchPhotos = ({
isSittingOnADarkBackground,
searchQuery,
}: Props) => {
const [isSearchFocused, setIsSearchFocused] = useState(false)

return (
Expand All @@ -33,45 +34,22 @@ export const SearchPhotos = ({ isSittingOnADarkBackground }: Props) => {
/>
</div>
<div className="flex-grow">
<input
type="text"
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full border-none bg-transparent py-2.5 text-gray-900 placeholder:text-gray-500 focus:ring-0"
placeholder="Search for photos"
/>
<Transition
as={Fragment}
show={isSearchFocused}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
<div
onClick={() => setIsSearchFocused(true)}
className="flex h-11 w-full cursor-text items-center border-none bg-transparent py-2.5 pl-3 text-gray-900 placeholder:text-gray-500 focus:ring-0"
>
<div className="absolute left-2 top-full z-40 mt-2 w-[45vw] overflow-hidden rounded-lg bg-white p-2 shadow-lg ring-1 ring-gray-900/5">
<TextSearch />
</div>
</Transition>
<span className="text-zinc-500">
{searchQuery ?? 'Search for photos'}
</span>
</div>
<SearchPalette
searchQuery={searchQuery}
isOpened={isSearchFocused}
onClose={() => setIsSearchFocused(false)}
/>
</div>

<div className="inset-y-0 right-0 flex items-center pr-6">
{query.length ? (
<Button
variant="unstyled"
type="button"
onClick={() => setQuery('')}
className="mr-4 border-r border-zinc-400"
>
<XMarkIcon
className="h-5 w-5 cursor-pointer text-gray-600"
aria-hidden="true"
/>
</Button>
) : null}
<Popover className="relative">
<Popover.Button className="flex items-center gap-x-1 text-sm font-semibold leading-6 text-gray-900 outline-none ring-0">
<ViewfinderCircleIcon
Expand All @@ -89,7 +67,7 @@ export const SearchPhotos = ({ isSittingOnADarkBackground }: Props) => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-1 top-full z-30 mt-5 w-screen max-w-md overflow-hidden rounded-lg bg-white p-3 shadow-lg ring-1 ring-gray-900/5">
<Popover.Panel className="absolute right-1 top-full z-30 mt-5 w-screen max-w-md overflow-hidden rounded-lg bg-white p-5 shadow-lg ring-1 ring-gray-900/5">
<VisualSearch />
</Popover.Panel>
</Transition>
Expand Down
Loading
Loading