-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hooks): add remix example (#404)
- Loading branch information
Showing
19 changed files
with
500 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], | ||
"rules": { | ||
"@typescript-eslint/naming-convention": "off", | ||
"spaced-comment": ["error", "always", { "markers": ["/"] }] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
node_modules | ||
|
||
/.cache | ||
/build | ||
/public/build | ||
.env | ||
|
||
/app/tailwind.css |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Welcome to Remix! | ||
|
||
- [Remix Docs](https://remix.run/docs) | ||
|
||
## Development | ||
|
||
From your terminal: | ||
|
||
```sh | ||
npm run dev | ||
``` | ||
|
||
This starts your app in development mode, rebuilding assets on file changes. | ||
|
||
## Deployment | ||
|
||
First, build your app for production: | ||
|
||
```sh | ||
npm run build | ||
``` | ||
|
||
Then run the app in production mode: | ||
|
||
```sh | ||
npm start | ||
``` | ||
|
||
Now you'll need to pick a host to deploy it to. | ||
|
||
### DIY | ||
|
||
If you're familiar with deploying node applications, the built-in Remix app server is production-ready. | ||
|
||
Make sure to deploy the output of `remix build` | ||
|
||
- `build/` | ||
- `public/build/` | ||
|
||
### Using a Template | ||
|
||
When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server. | ||
|
||
```sh | ||
cd .. | ||
# create a new project, and pick a pre-configured host | ||
npx create-remix@latest | ||
cd my-new-remix-app | ||
# remove the new project's app (not the old one!) | ||
rm -rf app | ||
# copy your app over | ||
cp -R ../my-old-remix-app/app app | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { RemixBrowser } from '@remix-run/react'; | ||
import { hydrateRoot } from 'react-dom/client'; | ||
|
||
hydrateRoot(document, <RemixBrowser />); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import type { EntryContext } from '@remix-run/node'; | ||
import { RemixServer } from '@remix-run/react'; | ||
import { renderToString } from 'react-dom/server'; | ||
|
||
export default function handleRequest( | ||
request: Request, | ||
responseStatusCode: number, | ||
responseHeaders: Headers, | ||
remixContext: EntryContext | ||
) { | ||
const markup = renderToString( | ||
<RemixServer context={remixContext} url={request.url} /> | ||
); | ||
|
||
responseHeaders.set('Content-Type', 'text/html'); | ||
|
||
return new Response(`<!DOCTYPE html>${markup}`, { | ||
status: responseStatusCode, | ||
headers: responseHeaders, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import type { MetaFunction } from '@remix-run/node'; | ||
import { | ||
Links, | ||
LiveReload, | ||
Meta, | ||
Outlet, | ||
Scripts, | ||
ScrollRestoration, | ||
} from '@remix-run/react'; | ||
|
||
export const meta: MetaFunction = () => ({ | ||
charset: 'utf-8', | ||
title: 'React InstantSearch Hooks - Remix', | ||
viewport: 'width=device-width,initial-scale=1', | ||
}); | ||
|
||
export default function App() { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body> | ||
<Outlet /> | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
<LiveReload /> | ||
</body> | ||
</html> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import algoliasearch from 'algoliasearch/lite'; | ||
import type { InstantSearchServerState } from 'react-instantsearch-hooks-web'; | ||
import { | ||
DynamicWidgets, | ||
Hits, | ||
InstantSearch, | ||
InstantSearchSSRProvider, | ||
Pagination, | ||
RefinementList, | ||
SearchBox, | ||
useInstantSearch, | ||
} from 'react-instantsearch-hooks-web'; | ||
import { getServerState } from 'react-instantsearch-hooks-server'; | ||
import { history } from 'instantsearch.js/cjs/lib/routers/index.js'; | ||
import instantSearchStyles from 'instantsearch.css/themes/satellite-min.css'; | ||
|
||
import type { LinksFunction, LoaderFunction } from '@remix-run/node'; | ||
import { json } from '@remix-run/node'; | ||
import { useLoaderData } from '@remix-run/react'; | ||
|
||
import { Hit } from '../../components/Hit'; | ||
import { Panel } from '../../components/Panel'; | ||
import { ScrollTo } from '../../components/ScrollTo'; | ||
import { NoResultsBoundary } from '../../components/NoResultsBoundary'; | ||
import { SearchErrorToast } from '../../components/SearchErrorToast'; | ||
|
||
import tailwindStyles from '../tailwind.css'; | ||
|
||
const searchClient = algoliasearch( | ||
'latency', | ||
'6be0576ff61c053d5f9a3225e2a90f76' | ||
); | ||
|
||
export const links: LinksFunction = () => [ | ||
{ rel: 'stylesheet', href: instantSearchStyles }, | ||
{ rel: 'stylesheet', href: tailwindStyles }, | ||
]; | ||
|
||
export const loader: LoaderFunction = async ({ request }) => { | ||
const serverUrl = request.url; | ||
const serverState = await getServerState(<Search serverUrl={serverUrl} />); | ||
|
||
return json({ | ||
serverState, | ||
serverUrl, | ||
}); | ||
}; | ||
|
||
type SearchProps = { | ||
serverState?: InstantSearchServerState; | ||
serverUrl?: string; | ||
}; | ||
|
||
function Search({ serverState, serverUrl }: SearchProps) { | ||
return ( | ||
<InstantSearchSSRProvider {...serverState}> | ||
<InstantSearch | ||
searchClient={searchClient} | ||
indexName="instant_search" | ||
routing={{ | ||
router: history({ | ||
getLocation() { | ||
if (typeof window === 'undefined') { | ||
return new URL(serverUrl!) as unknown as Location; | ||
} | ||
|
||
return window.location; | ||
}, | ||
}), | ||
}} | ||
> | ||
<SearchErrorToast /> | ||
|
||
<ScrollTo className="max-w-6xl p-4 flex gap-4 m-auto"> | ||
<div> | ||
<DynamicWidgets fallbackComponent={FallbackComponent} /> | ||
</div> | ||
|
||
<div className="flex flex-col w-full gap-8"> | ||
<SearchBox /> | ||
<NoResultsBoundary fallback={<NoResults />}> | ||
<Hits | ||
hitComponent={Hit} | ||
classNames={{ | ||
list: 'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4', | ||
item: 'p-2 w-full', | ||
}} | ||
/> | ||
<Pagination className="flex self-center" /> | ||
</NoResultsBoundary> | ||
</div> | ||
</ScrollTo> | ||
</InstantSearch> | ||
</InstantSearchSSRProvider> | ||
); | ||
} | ||
|
||
function FallbackComponent({ attribute }: { attribute: string }) { | ||
return ( | ||
<Panel header={attribute}> | ||
<RefinementList attribute={attribute} /> | ||
</Panel> | ||
); | ||
} | ||
|
||
function NoResults() { | ||
const { indexUiState } = useInstantSearch(); | ||
|
||
return ( | ||
<div> | ||
<p> | ||
No results for <q>{indexUiState.query}</q>. | ||
</p> | ||
</div> | ||
); | ||
} | ||
|
||
export default function HomePage() { | ||
const { serverState, serverUrl } = useLoaderData(); | ||
|
||
return <Search serverState={serverState} serverUrl={serverUrl} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import type { Hit as AlgoliaHit } from 'instantsearch.js'; | ||
import { Highlight } from 'react-instantsearch-hooks-web'; | ||
|
||
type HitProps = { | ||
hit: AlgoliaHit<{ | ||
name: string; | ||
price: number; | ||
image: string; | ||
brand: string; | ||
}>; | ||
}; | ||
|
||
export function Hit({ hit }: HitProps) { | ||
return ( | ||
<div className="group relative"> | ||
<div className="flex justify-center overflow-hidden"> | ||
<img | ||
src={hit.image} | ||
alt={hit.name} | ||
className="object-center object-cover" | ||
/> | ||
</div> | ||
<h3 className="mt-4 text-sm text-gray-700"> | ||
<span className="absolute inset-0" /> | ||
<Highlight hit={hit} attribute="name" /> | ||
</h3> | ||
<p className="mt-1 text-sm text-gray-500">{hit.brand}</p> | ||
<p className="mt-1 text-sm font-medium text-gray-900">${hit.price}</p> | ||
</div> | ||
); | ||
} |
27 changes: 27 additions & 0 deletions
27
react-instantsearch-hooks/remix/components/NoResultsBoundary.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import type { ReactNode } from 'react'; | ||
import { useInstantSearch } from 'react-instantsearch-hooks-web'; | ||
|
||
type NoResultsBoundaryProps = { | ||
children: ReactNode; | ||
fallback: ReactNode; | ||
}; | ||
|
||
export function NoResultsBoundary({ | ||
children, | ||
fallback, | ||
}: NoResultsBoundaryProps) { | ||
const { results } = useInstantSearch(); | ||
|
||
// The `__isArtificial` flag makes sure to not display the No Results message | ||
// when no hits have been returned yet. | ||
if (!results.__isArtificial && results.nbHits === 0) { | ||
return ( | ||
<> | ||
{fallback} | ||
<div hidden>{children}</div> | ||
</> | ||
); | ||
} | ||
|
||
return <>{children}</>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export function Panel({ | ||
children, | ||
header, | ||
footer, | ||
}: { | ||
children: React.ReactNode; | ||
header?: React.ReactNode; | ||
footer?: React.ReactNode; | ||
}) { | ||
return ( | ||
<div className="ais-Panel"> | ||
{header && <div className="ais-Panel-header">{header}</div>} | ||
<div className="ais-Panel-body">{children}</div> | ||
{footer && <div className="ais-Panel-footer">{footer}</div>} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import type { ComponentProps, ReactNode } from 'react'; | ||
import React, { useEffect, useRef } from 'react'; | ||
import { useInstantSearch } from 'react-instantsearch-hooks-web'; | ||
|
||
type ScrollToProps = ComponentProps<'div'> & { | ||
children: ReactNode; | ||
}; | ||
|
||
export function ScrollTo({ children, ...props }: ScrollToProps) { | ||
const { use } = useInstantSearch(); | ||
const containerRef = useRef<HTMLDivElement>(null); | ||
|
||
useEffect(() => { | ||
return use(() => { | ||
return { | ||
onStateChange() { | ||
const isFiltering = document.body.classList.contains('filtering'); | ||
const isTyping = | ||
document.activeElement?.tagName === 'INPUT' && | ||
document.activeElement?.getAttribute('type') === 'search'; | ||
|
||
if (isFiltering || isTyping) { | ||
return; | ||
} | ||
|
||
containerRef.current!.scrollIntoView(); | ||
}, | ||
}; | ||
}); | ||
}, [use]); | ||
|
||
return ( | ||
<div {...props} ref={containerRef}> | ||
{children} | ||
</div> | ||
); | ||
} |
Oops, something went wrong.