Skip to content

Commit

Permalink
Merge branch '1.x' into feature/SUM-49--favoritesPage
Browse files Browse the repository at this point in the history
  • Loading branch information
rebeccahongsf committed May 8, 2024
2 parents a42f27f + 5c7c1e2 commit 68c2859
Show file tree
Hide file tree
Showing 25 changed files with 1,928 additions and 1,029 deletions.
12 changes: 6 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import PageHeader from "@components/global/page-header";
import {Icon} from "next/dist/lib/metadata/types/metadata-types";
import { roboto, sourceSans3} from "../src/styles/fonts";
import DrupalWindowSync from "@components/elements/drupal-window-sync";
import {isPreviewMode} from "@lib/drupal/utils";
import UserAnalytics from "@components/elements/user-analytics";
import clsx from "clsx";
import Editori11y from "@components/tools/editorially";

const appleIcons: Icon[] = [60, 72, 76, 114, 120, 144, 152, 180].map(size => ({
url: `https://www-media.stanford.edu/assets/favicon/apple-touch-icon-${size}x${size}.png`,
Expand Down Expand Up @@ -44,14 +44,14 @@ export const metadata = {
export const revalidate = false;

const RootLayout = ({children, modal}: { children: React.ReactNode, modal: React.ReactNode }) => {
const isPreview = isPreviewMode();
const isDevMode = process.env.NODE_ENV === "development";

return (
<html lang="en" className={clsx(sourceSans3.className, roboto.variable)}>
{/* Add Google Analytics and SiteImprove when not in preview mode. */}
{!isPreview &&
<UserAnalytics/>
}
<UserAnalytics/>
<DrupalWindowSync/>
{isDevMode && <Editori11y/>}

<body>
<nav aria-label="Skip Links">
<a href="#main-content" className="skiplink">Skip to main content</a>
Expand Down
248 changes: 113 additions & 135 deletions app/search/algolia-search.tsx
Original file line number Diff line number Diff line change
@@ -1,178 +1,156 @@
"use client";

import algoliasearch from "algoliasearch/lite";
import {useHits, useSearchBox} from "react-instantsearch";
import {useHits, useSearchBox, usePagination} from "react-instantsearch";
import {InstantSearchNext} from "react-instantsearch-nextjs";
import Link from "@components/elements/link";
import {H2} from "@components/elements/headers";
import Image from "next/image";
import {useRef} from "react";
import {useEffect, useMemo, useRef} from "react";
import Button from "@components/elements/button";
import {UseSearchBoxProps} from "react-instantsearch";
import {useRouter, useSearchParams} from "next/navigation";
import {UseHitsProps} from "react-instantsearch-core/dist/es/connectors/useHits";
import {Hit as HitType} from "instantsearch.js";
import {IndexUiState} from "instantsearch.js/es/types/ui-state";
import {MagnifyingGlassIcon} from "@heroicons/react/20/solid";
import DefaultResult, {AlgoliaHit} from "@components/algolia-results/default";

type Props = {
appId: string
searchIndex: string
searchApiKey: string
initialUiState?: IndexUiState
}

const AlgoliaSearch = ({appId, searchIndex, searchApiKey}: Props) => {
const searchClient = algoliasearch(appId, searchApiKey);
const searchParams = useSearchParams();
const AlgoliaSearch = ({appId, searchIndex, searchApiKey, initialUiState = {}}: Props) => {
const searchClient = useMemo(() => algoliasearch(appId, searchApiKey), [appId, searchApiKey])

return (
<div>
<InstantSearchNext
indexName={searchIndex}
searchClient={searchClient}
initialUiState={{
[searchIndex]: {query: searchParams.get("q") || ""},
}}
initialUiState={{[searchIndex]: initialUiState}}
future={{preserveSharedStateOnUnmount: true}}
>
<div className="space-y-10">
<SearchBox/>
<HitList/>
</div>
<SearchForm/>
</InstantSearchNext>
</div>
)
}

const HitList = (props: UseHitsProps) => {
const {hits} = useHits(props);
if (hits.length === 0) {
return (
<p>No results for your search. Please try another search.</p>
)
}
const SearchForm = () => {

return (
<ul className="list-unstyled">
{hits.map(hit =>
<li key={hit.objectID} className="border-b border-gray-300 last:border-0">
<Hit hit={hit as unknown as AlgoliaHit}/>
</li>
)}
</ul>
)
}
const router = useRouter()
const searchParams = useSearchParams();

type AlgoliaHit = {
url: string
title: string
summary?: string
photo?: string
updated?: number
}
const inputRef = useRef<HTMLInputElement>(null);
const {query, refine} = useSearchBox({});

useEffect(() => {
const params = new URLSearchParams(searchParams.toString());
params.delete("q")

const Hit = ({hit}: { hit: AlgoliaHit }) => {
const hitUrl = new URL(hit.url);
// Keyword search.
if (query) params.set("q", query)
router.replace(`?${params.toString()}`, {scroll: false})
}, [router, searchParams, query]);

return (
<article className="@container flex justify-between gap-20 py-12">
<div>
<H2 className="text-m2">
<Link href={hit.url.replace(hitUrl.origin, "")}>
{hit.title}
</Link>
</H2>
<p>{hit.summary}</p>

{hit.updated &&
<div className="text-2xl">
Last Updated: {new Date(hit.updated * 1000).toLocaleDateString("en-us", {
month: "long",
day: "numeric",
year: "numeric"
})}
</div>
}
</div>

{hit.photo &&
<div className="hidden @6xl:block relative shrink-0 aspect-1 h-[150px] w-[150px]">
<Image
className="object-cover"
src={hit.photo.replace(hitUrl.origin, `${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}`)}
alt=""
fill
<div>
<form role="search" aria-labelledby="page-title" onSubmit={(e) => e.preventDefault()}>
<div className="max-w-6xl mx-auto mb-20 flex gap-5 items-center">
<label className="sr-only" htmlFor="search-input">
Keywords Search
</label>
<input
id="search-input"
className="flex-grow border-0 border-b border-black-30 text-m2"
ref={inputRef}
autoComplete="on"
autoCapitalize="off"
spellCheck={false}
maxLength={512}
type="search"
placeholder="Search"
defaultValue={query}
/>

<button
type="submit"
onClick={() => refine(inputRef.current?.value || "")}
>
<span className="sr-only">Submit Search</span>
<MagnifyingGlassIcon width={40} className="bg-cardinal-red text-white rounded-full p-3 block"/>
</button>

<Button
className="my-16"
centered
buttonElem
onClick={() => {
refine("")
if (inputRef.current) inputRef.current.value = ""
}}
>
Reset
</Button>
</div>
}
</article>
</form>
<HitList/>
</div>
)
}

const HitList = () => {
const {hits} = useHits<HitType<AlgoliaHit>>({});
const {currentRefinement: currentPage, pages, nbPages, nbHits, refine: goToPage} = usePagination({padding: 2})

const SearchBox = (props?: UseSearchBoxProps) => {
const router = useRouter();
const {query, refine} = useSearchBox(props);
const inputRef = useRef<HTMLInputElement>(null);

if (query) {
router.replace(`?q=${query}`, {scroll: false})
if (hits.length === 0) {
return (
<p>No results for your search. Please try another search.</p>
)
}

return (
<form
className="flex flex-col gap-10"
action=""
role="search"
noValidate
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
inputRef.current?.blur();
refine(inputRef.current?.value || "");
}}
onReset={(event) => {
event.preventDefault();
event.stopPropagation();
refine("");

if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.focus();
}
}}
>
<div className="flex flex-col">
<label className="font-bold" htmlFor="search-input">
Keywords<span className="sr-only">&nbsp;Search</span>
</label>
<input
id="search-input"
className="rounded-full hocus:shadow-2xl max-w-xl h-20 text-m1"
ref={inputRef}
autoComplete="on"
autoCorrect="on"
autoCapitalize="off"
spellCheck={false}
maxLength={512}
type="search"
required
defaultValue={query}
autoFocus
/>
</div>
<div className="flex gap-10">
<Button type="submit">
Submit
</Button>
<Button
secondary
type="reset"
className={query.length === 0 ? "hidden" : undefined}
>
Reset
</Button>
</div>
<div className="sr-only" aria-live="polite" aria-atomic>Showing results for {query}</div>
</form>
);
<div>
<div aria-live="polite">{nbHits} {nbHits > 1 ? "Results" : "Result"}</div>

<ul className="list-unstyled">
{hits.map(hit =>
<li key={hit.objectID} className="border-b border-gray-300 last:border-0">
<DefaultResult hit={hit}/>
</li>
)}
</ul>

{pages.length > 1 &&
<nav aria-label="Search results pager">
<ul className="list-unstyled flex justify-between">
{pages[0] > 0 &&
<li>
<button onClick={() => goToPage(0)}>
First
</button>
</li>
}

{pages.map(pageNum =>
<li key={`page-${pageNum}`} aria-current={currentPage === pageNum}>
<button onClick={() => goToPage(pageNum)}>
{pageNum + 1}
</button>
</li>
)}

{pages[pages.length - 1] !== nbPages &&
<li>
<button onClick={() => goToPage(nbPages - 1)}>
Last
</button>
</li>
}
</ul>
</nav>
}
</div>
)
}

export default AlgoliaSearch;
39 changes: 4 additions & 35 deletions app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {getSearchIndex} from "@lib/drupal/get-search-index";
import SearchResults, {SearchResult} from "./search-results";
import {H1} from "@components/elements/headers";
import {DrupalNode} from "next-drupal";
import {Suspense} from "react";
import {DrupalJsonApiParams} from "drupal-jsonapi-params";
import {getConfigPage} from "@lib/gql/gql-queries";
import {StanfordBasicSiteSetting} from "@lib/gql/__generated__/drupal.d";
import AlgoliaSearch from "./algolia-search";
import {IndexUiState} from "instantsearch.js/es/types/ui-state";

// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
export const revalidate = false;
Expand All @@ -23,46 +19,19 @@ export const metadata = {
const Page = async ({searchParams}: { searchParams?: { [_key: string]: string } }) => {

const siteSettingsConfig = await getConfigPage<StanfordBasicSiteSetting>("StanfordBasicSiteSetting")

const search = async (searchString: string): Promise<SearchResult[]> => {
"use server";

const params = new DrupalJsonApiParams();
params.addCustomParam({"filter[fulltext]": searchString})

// This still uses JSON API because GraphQL doesn"t have an easy way to search for content.
const searchResults: DrupalNode[] = await getSearchIndex("full_site_content", {params: params.getQueryObject()});

return searchResults.map(node => ({
id: node.id,
title: node.title,
path: node.path.alias,
changed: node.changed,
})).slice(0, 20)
}

const initialResults = await search(searchParams?.q || "");

const algoliaConfigured = siteSettingsConfig?.suSiteAlgolia &&
siteSettingsConfig?.suSiteAlgoliaId &&
siteSettingsConfig?.suSiteAlgoliaIndex &&
siteSettingsConfig?.suSiteAlgoliaSearch;
const initialState: IndexUiState = {}
if (searchParams?.q) initialState.query = searchParams.q as string

return (
<div className="centered mt-32">
<H1>Search</H1>

{!algoliaConfigured &&
<Suspense fallback={<></>}>
<SearchResults search={search} initialSearchString={searchParams?.q || ""} initialResults={initialResults}/>
</Suspense>
}

{(siteSettingsConfig?.suSiteAlgoliaId && siteSettingsConfig?.suSiteAlgoliaIndex && siteSettingsConfig?.suSiteAlgoliaSearch) &&
<AlgoliaSearch
appId={siteSettingsConfig.suSiteAlgoliaId}
searchIndex={siteSettingsConfig.suSiteAlgoliaIndex}
searchApiKey={siteSettingsConfig.suSiteAlgoliaSearch}
initialUiState={initialState}
/>
}
</div>
Expand Down
Loading

0 comments on commit 68c2859

Please sign in to comment.