Skip to content

Commit

Permalink
new search implementation in the index root route + its conditionall …
Browse files Browse the repository at this point in the history
…custom scrollrestoration component instead of remix.run's provided +fixes +edits
  • Loading branch information
filipc30 committed Feb 7, 2024
1 parent 35e4ab5 commit 45d0e09
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 26 deletions.
28 changes: 28 additions & 0 deletions app/components/custom-scroll-restoration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useLocation } from '@remix-run/react'
import { useLayoutEffect, useRef, useMemo } from 'react'

export function CustomScrollRestoration() {
const { pathname, search } = useLocation()
const scrollPositions = useRef<{ [key: string]: number }>({})

const paths = useMemo(() => ['/', '', '/?search='], [])

// Only store scroll position for the specific routes
useLayoutEffect(() => {
if (paths.includes(pathname)) {
scrollPositions.current[pathname + search] = window.scrollY
}
}, [pathname, search, paths])

// Only restore scroll position for the specific routes
useLayoutEffect(() => {
if (paths.includes(pathname)) {
const storedPosition = scrollPositions.current[pathname + search]
window.scrollTo(0, storedPosition || 0)
} else {
window.scrollTo(0, 0) // Scroll to top in other routes than "paths"
}
}, [pathname, search, paths])

return null
}
15 changes: 15 additions & 0 deletions app/components/headers/header-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ export function HeaderBase({ routeAdmin }: { routeAdmin?: boolean }) {
)}
</NavLink>

<NavLink to="dealers">
{({ isActive }) => (
<>
<Button
variant={isActive ? 'highlight' : 'secondary'}
className="max-md:hidden"
>
dealers
</Button>

<Icon size="lg" className={cn(isActive && "text-purple-500", "md:hidden")} name="file-text" />
</>
)}
</NavLink>

<NavLink to="pages">
{({ isActive }) => (
<>
Expand Down
28 changes: 15 additions & 13 deletions app/components/search-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export function SearchBar({
actionUrl,
status,
autoFocus = false,
autoSubmit = false,
autoSubmit = false, //hides the search status button if True
carModelUrl,
}: {
actionUrl: 'search' | 'brands' | 'admin/users' | 'pages' | 'carmodels'
actionUrl: '' | 'search' | 'brands' | 'admin/users' | 'pages' | 'carmodels'
status: 'idle' | 'pending' | 'success' | 'error'
autoFocus?: boolean
autoSubmit?: boolean
Expand Down Expand Up @@ -55,17 +55,19 @@ export function SearchBar({
autoFocus={autoFocus}
/>
</div>
<div>
<StatusButton
type="submit"
status={isSubmitting ? 'pending' : status}
className="flex w-full items-center justify-center"
size="sm"
>
<Icon name="magnifying-glass" size="sm" />
<span className="sr-only">Search</span>
</StatusButton>
</div>
{!autoSubmit && (
<div>
<StatusButton
type="submit"
status={isSubmitting ? 'pending' : status}
className="flex w-full items-center justify-center"
size="sm"
>
<Icon name="magnifying-glass" size="sm" />
<span className="sr-only">Search</span>
</StatusButton>
</div>
)}
</Form>
)
}
20 changes: 16 additions & 4 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import {
Scripts,
ScrollRestoration,
useLoaderData,
useLocation,
useMatches,
} from '@remix-run/react'
import { withSentry } from '@sentry/remix'
import { AuthenticityTokenProvider } from 'remix-utils/csrf/react'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { Confetti } from './components/confetti.tsx'
import { CustomScrollRestoration } from './components/custom-scroll-restoration.tsx'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
import { FooterBase } from './components/footers/footer-base.tsx'
import { HeaderBase } from './components/headers/header-base.tsx'
Expand Down Expand Up @@ -194,24 +196,34 @@ function Document({
theme?: Theme
env?: Record<string, string>
}) {
const { pathname } = useLocation()
const paths = ['/', '', '/?search=']

return (
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
<head>
<ClientHintCheck nonce={nonce} />
<Meta />
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1"
/>
<Links />
</head>
<body className='max-2xl:text-body-base'>
<body className="max-2xl:text-body-base">
{children}
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(env)}`,
}}
/>
<ScrollRestoration nonce={nonce} />
{paths.includes(pathname) ? (
<CustomScrollRestoration />
) : (
<ScrollRestoration nonce={nonce} />
)}
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
Expand All @@ -232,7 +244,7 @@ function App() {
<Document nonce={nonce} theme={theme} env={data.ENV}>
<HeaderBase routeAdmin={routeAdmin} />

<div className='bg-main-gradient-light dark:bg-main-gradient-dark main-custom-height'>
<div className="main-custom-height bg-main-gradient-light dark:bg-main-gradient-dark">
<Outlet />
</div>

Expand Down
141 changes: 134 additions & 7 deletions app/routes/_base+/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,79 @@
import { type MetaFunction } from '@remix-run/node'
import { useMatches } from '@remix-run/react'
import { json, type DataFunctionArgs, type MetaFunction } from '@remix-run/node'
import { Link, useLoaderData, useSearchParams } from '@remix-run/react'
import { useMemo } from 'react'
import { z } from 'zod'
import { LogoChaosEscape } from '#app/components/chaos-escape-logo.tsx'
import { ErrorList } from '#app/components/forms.tsx'
import { SearchBar } from '#app/components/search-bar.tsx'
import { Button } from '#app/components/ui/button.tsx'
import { prisma } from '#app/utils/db.server.ts'
import { cn, useDelayedIsPending } from '#app/utils/misc.tsx'

const SearchResultSchema = z.object({
id: z.string(),
url: z.string(),
title: z.string().nullable(),
type: z.enum(['CarBrand', 'CarModel', 'Dealer']),
carBrandTitle: z.string().nullable(),
})

const SearchResultsSchema = z.array(SearchResultSchema)

export async function loader({ request }: DataFunctionArgs) {
const searchTerm = new URL(request.url).searchParams.get('search')
if (!searchTerm) {
return json({ status: 'idle', searchResults: null } as const)
}

const like = `%${searchTerm ?? ''}%`

const rawSearchResults = await prisma.$queryRaw`
SELECT
m.id,
m.url,
m.title,
'CarModel' as type,
b.title as carBrandTitle
FROM CarModel m
JOIN CarBrand b ON m.carBrandId = b.id
WHERE m.title LIKE ${like} OR m.url LIKE ${like}
UNION
SELECT id, url, title, 'CarBrand' as type, NULL as carBrandTitle
FROM CarBrand
WHERE title LIKE ${like}
UNION
SELECT id, url, name as title, 'Dealer' as type, NULL as carBrandTitle
FROM Dealer
WHERE name LIKE ${like}
LIMIT 20
`

const result = SearchResultsSchema.safeParse(rawSearchResults)
if (!result.success) {
return json({ status: 'error', error: result.error.message } as const, {
status: 400,
})
}

return json({ status: 'idle', searchResults: result.data } as const)
}

export default function Index() {
const matches = useMatches()
const isOnSearchPage = matches.find(m => m.id === 'routes/users+/index')
const searchBar = isOnSearchPage ? null : (
<SearchBar status="idle" actionUrl={'search'} />
const data = useLoaderData<typeof loader>()

//TODO: implement search & query logic of load
const isPending = useDelayedIsPending({
formMethod: 'GET',
formAction: '/',
})
if (data.status === 'error') {
console.error(data.error)
}

const [searchParams] = useSearchParams()
const searchTerm = useMemo(
() => searchParams.get('search') ?? '',
[searchParams],
)

return (
Expand All @@ -33,7 +98,69 @@ export default function Index() {
Find Your Car
</Button>

<div className="mx-auto max-w-[400px] text-foreground">{searchBar}</div>
<div className="mx-auto mb-4 max-w-[400px] text-foreground">
<div className="w-full text-center max-md:px-4">
<div className="relative w-full max-w-[700px]">
<SearchBar
actionUrl=""
status={data.status}
// autoFocus
autoSubmit
/>

<div className="absolute w-full">
{data.status === 'idle' ? (
data.searchResults && data.searchResults.length ? (
<ul
className={cn(
'mt-2 flex w-full flex-col gap-2 rounded-xl bg-background p-4 delay-200',
{
'opacity-50': isPending,
},
)}
>
{data.searchResults.map(result => (
<li key={result.id}>
<Link
to={
'/' +
(result.type === 'CarBrand'
? 'brands'
: result.type === 'CarModel'
? 'brands/' + result.carBrandTitle
: result.type === 'Dealer'
? 'dealers'
: '') +
'/' +
result.url
}
className="flex flex-col items-center justify-center rounded-lg bg-muted px-5 py-8 transition duration-200 hover:bg-highlight hover:text-highlight-foreground"
>
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-center text-body-md">
{result.title}
</span>
</Link>
</li>
))}
</ul>
) : (
searchTerm !== '' && (
<div className="mt-2 flex w-full flex-col gap-2 rounded-xl bg-background p-4 delay-200">
<p className="px-5 py-8">
No results for "{searchTerm}" found.
</p>
</div>
)
)
) : data.status === 'error' ? (
<ErrorList
errors={['There was an error parsing the results']}
/>
) : null}
</div>
</div>
</div>
</div>

<h3 className="text-md capitalize">search, compare, find dealer</h3>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/routes/_base+/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const SearchResultSchema = z.object({

const SearchResultsSchema = z.array(SearchResultSchema)

export async function loader({ request, params }: DataFunctionArgs) {
export async function loader({ request }: DataFunctionArgs) {
const searchTerm = new URL(request.url).searchParams.get('search')
if (searchTerm === '') {
return redirect('/search')
Expand Down Expand Up @@ -67,7 +67,7 @@ export async function loader({ request, params }: DataFunctionArgs) {
return json({ status: 'idle', searchResults: result.data } as const)
}

export default function CarBrandUrlRoute() {
export default function GlobalSearch() {
const data = useLoaderData<typeof loader>()

//TODO: implement search & query logic of load
Expand Down
13 changes: 13 additions & 0 deletions app/routes/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,19 @@ export default function AdminRoute() {
routeName="translations"
icon="file-text"
/>
<SidebarNavLink
routeName="brands"
icon="file-text"
/>
<SidebarNavLink
routeName="models"
icon="file-text"
/>
<SidebarNavLink
routeName="dealers"
icon="file-text"
/>

<SidebarNavLink
routeName="pages"
icon="file-text"
Expand Down

0 comments on commit 45d0e09

Please sign in to comment.