add some basic search functionality

This commit is contained in:
2024-07-15 04:30:05 +02:00
parent 55257f3bb4
commit c935314c4f
16 changed files with 226 additions and 5 deletions

14
web/package-lock.json generated
View File

@ -23,7 +23,8 @@
"react-intersection-observer": "^9.13.0",
"sass": "^1.77.8",
"swiper": "^11.1.4",
"urql": "^4.1.0"
"urql": "^4.1.0",
"use-debounce": "^10.0.1"
},
"devDependencies": {
"@types/node": "^20",
@ -8306,6 +8307,17 @@
"react": ">= 16.8.0"
}
},
"node_modules/use-debounce": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.1.tgz",
"integrity": "sha512-0uUXjOfm44e6z4LZ/woZvkM8FwV1wiuoB6xnrrOmeAEjRDDzTLQNRFtYHvqUsJdrz1X37j0rVGIVp144GLHGKg==",
"engines": {
"node": ">= 16.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -25,7 +25,8 @@
"react-intersection-observer": "^9.13.0",
"sass": "^1.77.8",
"swiper": "^11.1.4",
"urql": "^4.1.0"
"urql": "^4.1.0",
"use-debounce": "^10.0.1"
},
"devDependencies": {
"@types/node": "^20",

60
web/src/app/sok/page.tsx Normal file
View File

@ -0,0 +1,60 @@
import { graphql } from "@/gql";
import { getClient } from "@/app/client";
import { SearchContainer } from "@/components/search/SearchContainer";
import { Suspense } from "react";
export default async function Page({
searchParams,
}: {
searchParams?: {
q?: string;
};
}) {
const { q: query } = searchParams ?? {};
let results = [];
if (query) {
const searchQuery = graphql(`
query search($query: String) {
results: search(query: $query) {
__typename
... on NewsPage {
id
title
}
... on EventPage {
id
title
}
... on GenericPage {
id
title
}
... on VenuePage {
id
title
}
... on AssociationPage {
id
title
associationType
}
}
}
`);
const { data, error } = await getClient().query(searchQuery, {
query: query,
});
results = (data?.results ?? []) as any;
}
return (
<main className="site-main" id="main">
<Suspense key={query}>
<SearchContainer query={query ?? ""} results={results} />
</Suspense>
</main>
);
}

View File

@ -1,17 +1,19 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
import { useState, useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import Link from "next/link";
import styles from "./header.module.scss";
import { Logo, LogoIcon } from "@/components/general/Logo";
import Icon from "../general/Icon";
import { useInView } from "react-intersection-observer";
import { getSearchPath } from "@/lib/common";
export const Header = () => {
const { ref: observer, inView: isInView } = useInView({
triggerOnce: false,
initialInView: true,
});
const { replace } = useRouter();
const [showMenu, setShowMenu] = useState(false);
function toggleMenu() {
@ -42,6 +44,17 @@ export const Header = () => {
undefined
);
const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key != "Enter") {
return;
}
const query = e.currentTarget.value;
if (query) {
setShowMenu(false);
replace(getSearchPath(query));
}
};
return (
<>
<header
@ -144,7 +157,7 @@ export const Header = () => {
<li className={styles.search}>
<label>
<p>Søk</p>
<input type="text" />
<input type="text" onKeyDown={handleSearch} />
</label>
</li>
<li className={styles.galtinn}>

View File

@ -0,0 +1,74 @@
"use client";
import { useDebouncedCallback } from "use-debounce";
import { PageHeader } from "../general/PageHeader";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { getSearchPath } from "@/lib/common";
export function SearchContainer({
query,
results,
}: {
query: string;
results: any;
}) {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
const onQueryChange = useDebouncedCallback((query) => {
replace(getSearchPath(query));
}, 500);
return (
<div>
<PageHeader heading="Søk" />
<input
name="query"
type="text"
autoFocus
defaultValue={query ?? ""}
onChange={(e) => {
onQueryChange(e.target.value);
}}
/>
{query && <SearchResults results={results} />}
</div>
);
}
function capitalizeFirstLetter(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
const PAGE_TYPES: Record<string, string> = {
NewsPage: "Nyhet",
EventPage: "Arrangement",
GenericPage: "Underside",
VenuePage: "Lokale",
AssociationPage: "Forening",
};
function SearchResults({ results }: { results: any }) {
if (!results.length) {
return <div>Ingen resultater 😔</div>;
}
const supportedResults = results.filter(
(result: any) =>
!!result?.id && Object.keys(PAGE_TYPES).includes(result.__typename)
);
return (
<div>
{supportedResults.map((result: any) => {
let resultType = PAGE_TYPES[result.__typename] ?? "";
if (result.__typename === "AssociationPage") {
resultType = capitalizeFirstLetter(result?.associationType);
}
return (
<div key={result.id}>
<span>{resultType}</span>
<span>{result.title}</span>
</div>
);
})}
</div>
);
}

View File

@ -34,6 +34,7 @@ const documents = {
"\n query venueIndex {\n index: venueIndex {\n ... on VenueIndex {\n ...VenueIndex\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n ...Venue\n }\n }\n }\n ": types.VenueIndexDocument,
"\n fragment Home on HomePage {\n ... on HomePage {\n featuredEvents {\n id\n }\n }\n }\n": types.HomeFragmentDoc,
"\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n home: page(contentType: \"home.HomePage\", urlPath: \"/home/\") {\n ... on HomePage {\n ...Home\n }\n }\n news: pages(contentType: \"news.newsPage\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...News\n }\n }\n }\n ": types.HomeDocument,
"\n query search($query: String) {\n results: search(query: $query) {\n __typename\n ... on NewsPage {\n id\n title\n }\n ... on EventPage {\n id\n title\n }\n ... on GenericPage {\n id\n title\n }\n ... on VenuePage {\n id\n title\n }\n ... on AssociationPage {\n id\n title\n associationType\n }\n }\n }\n ": types.SearchDocument,
"\n fragment VenueRentalIndex on VenueRentalIndex {\n ... on VenueRentalIndex {\n title\n lead\n body {\n ...Blocks\n }\n }\n }\n": types.VenueRentalIndexFragmentDoc,
"\n query venueRentalIndex {\n index: venueRentalIndex {\n ... on VenueRentalIndex {\n ...VenueRentalIndex\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n ...Venue\n }\n }\n }\n ": types.VenueRentalIndexDocument,
"\n fragment OneLevelOfBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n }\n": types.OneLevelOfBlocksFragmentDoc,
@ -149,6 +150,10 @@ export function graphql(source: "\n fragment Home on HomePage {\n ... on Hom
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n home: page(contentType: \"home.HomePage\", urlPath: \"/home/\") {\n ... on HomePage {\n ...Home\n }\n }\n news: pages(contentType: \"news.newsPage\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...News\n }\n }\n }\n "): (typeof documents)["\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n home: page(contentType: \"home.HomePage\", urlPath: \"/home/\") {\n ... on HomePage {\n ...Home\n }\n }\n news: pages(contentType: \"news.newsPage\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...News\n }\n }\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query search($query: String) {\n results: search(query: $query) {\n __typename\n ... on NewsPage {\n id\n title\n }\n ... on EventPage {\n id\n title\n }\n ... on GenericPage {\n id\n title\n }\n ... on VenuePage {\n id\n title\n }\n ... on AssociationPage {\n id\n title\n associationType\n }\n }\n }\n "): (typeof documents)["\n query search($query: String) {\n results: search(query: $query) {\n __typename\n ... on NewsPage {\n id\n title\n }\n ... on EventPage {\n id\n title\n }\n ... on GenericPage {\n id\n title\n }\n ... on VenuePage {\n id\n title\n }\n ... on AssociationPage {\n id\n title\n associationType\n }\n }\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

File diff suppressed because one or more lines are too long

View File

@ -27,6 +27,16 @@ export const PIG_NAMES = [
"peek",
];
export function getSearchPath(query: string): string {
const params = new URLSearchParams();
if (query) {
params.set("q", query);
} else {
params.delete("q");
}
return `/sok?${params.toString()}`;
}
export function randomElement(array: any[]): any | undefined {
return array.length
? array[Math.floor(Math.random() * array.length)]