Added search support
This commit is contained in:
parent
e7eb324cb1
commit
62a89706ec
|
@ -18,5 +18,9 @@ CACHE_CONTROL=false
|
|||
## OPEN EXCHANGE RATE
|
||||
OER_APP_ID=oerappid5e6ea45ef4e66eaa151612bdcb599df
|
||||
|
||||
## MEILI
|
||||
MEILISEARCH_URL=https://meilisearch.domain.com
|
||||
MEILISEARCH_MASTER_KEY=some_api_keyqs23d1qs6d54qs897qs3
|
||||
|
||||
## ANALYTICS
|
||||
ANALYTICS_URL=http://analytics.domain.com
|
|
@ -118,6 +118,7 @@ Read more:
|
|||
|
||||
- Smooth scrolling when using anchor links
|
||||
- On image pages (scans, gallery, image files), allow the user to navigate to the previous or next image using keyboard arrows.
|
||||
- On the search page, allow the user to navigate to the previous or next page using keyboard arrows.
|
||||
|
||||
- On media pages (scans, images, audios, videos), provide a download button. This way, the user doesn't have to right-click -> "save media as..."
|
||||
|
||||
|
|
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 400 KiB |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
|
@ -12,6 +12,7 @@ interface Props {
|
|||
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
|
||||
hideFooterLinks?: boolean;
|
||||
hideHomeButton?: boolean;
|
||||
hideSearchButton?: boolean;
|
||||
class?: string | undefined;
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,7 @@ const {
|
|||
backgroundImage,
|
||||
hideFooterLinks = false,
|
||||
hideHomeButton = false,
|
||||
hideSearchButton = false,
|
||||
...otherProps
|
||||
} = Astro.props;
|
||||
---
|
||||
|
@ -34,7 +36,11 @@ const {
|
|||
<Html openGraph={openGraph}>
|
||||
{backgroundImage && <AppLayoutBackgroundImg img={backgroundImage} />}
|
||||
<header>
|
||||
<Topbar parentPages={parentPages} hideHomeButton={hideHomeButton} />
|
||||
<Topbar
|
||||
parentPages={parentPages}
|
||||
hideHomeButton={hideHomeButton}
|
||||
hideSearchButton={hideSearchButton}
|
||||
/>
|
||||
</header>
|
||||
<main {...otherProps.class ? otherProps : {}}><slot /></main>
|
||||
<Footer withLinks={!hideFooterLinks} />
|
||||
|
|
|
@ -30,7 +30,11 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
|
|||
{
|
||||
withLinks && (
|
||||
<div id="socials" class="when-no-print">
|
||||
<a href={getLocalizedUrl("/discord")} class="pressable-label" aria-label={discordLabel} title={discordLabel}>
|
||||
<a
|
||||
href={getLocalizedUrl("/discord")}
|
||||
class="pressable-label"
|
||||
aria-label={discordLabel}
|
||||
title={discordLabel}>
|
||||
<Icon name="discord-brands" />
|
||||
<p class="font-s">{t("footer.socials.discord.title")}</p>
|
||||
</a>
|
||||
|
|
|
@ -11,9 +11,10 @@ import type { EndpointSource } from "src/shared/payload/payload-sdk";
|
|||
interface Props {
|
||||
parentPages?: EndpointSource[] | undefined;
|
||||
hideHomeButton?: boolean;
|
||||
hideSearchButton?: boolean;
|
||||
}
|
||||
|
||||
const { parentPages = [], hideHomeButton = false } = Astro.props;
|
||||
const { parentPages = [], hideHomeButton = false, hideSearchButton = false } = Astro.props;
|
||||
|
||||
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
||||
---
|
||||
|
@ -35,10 +36,16 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
|||
}
|
||||
|
||||
<div id="toolbar" class="hide-scrollbar">
|
||||
<a href={getLocalizedUrl("/search")} aria-label={t("header.topbar.search.tooltip")} hidden>
|
||||
<Button icon="material-symbols:search" />
|
||||
</a>
|
||||
<div class="separator" hidden></div>
|
||||
{
|
||||
!hideSearchButton && (
|
||||
<>
|
||||
<a href={getLocalizedUrl("/search")} title={t("header.topbar.search.tooltip")}>
|
||||
<Button icon="material-symbols:search" />
|
||||
</a>
|
||||
<div class="separator" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="when-no-js">
|
||||
<a href="/settings">
|
||||
|
|
|
@ -5,20 +5,21 @@ interface Props {
|
|||
id?: string;
|
||||
title?: string | undefined;
|
||||
icon?: string;
|
||||
class?: string;
|
||||
ariaLabel?: string;
|
||||
class?: string | undefined;
|
||||
}
|
||||
|
||||
const { title, icon, class: className, ariaLabel, id } = Astro.props;
|
||||
const { title, icon, ariaLabel, id, ...otherProps } = Astro.props;
|
||||
---
|
||||
|
||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||
|
||||
<button
|
||||
id={id}
|
||||
class:list={["pressable", { "with-title": !!title }, className]}
|
||||
class:list={["pressable", { "with-title": !!title }, otherProps.class]}
|
||||
aria-label={ariaLabel}
|
||||
title={ariaLabel}>
|
||||
title={ariaLabel}
|
||||
{...otherProps.class ? otherProps : {}}>
|
||||
{icon && <Icon name={icon} />}
|
||||
{title}
|
||||
</button>
|
||||
|
|
|
@ -22,7 +22,11 @@ const { title, description } = Astro.props;
|
|||
<p>{description}</p>
|
||||
) : (
|
||||
<p>
|
||||
Please contact <a href={getLocalizedUrl("/discord")} target="_blank">website technical administrator</a>.
|
||||
Please contact{" "}
|
||||
<a href={getLocalizedUrl("/discord")} target="_blank">
|
||||
website technical administrator
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,6 @@ const attributesWithMeta = [
|
|||
href={getLocalizedUrl(`/audios/${id}`)}
|
||||
attributes={attributesWithMeta}
|
||||
icon="material-symbols:music-note"
|
||||
iconHoverLabel={t("global.previewTypes.audio")}
|
||||
iconHoverLabel={t("global.collections.audios", { count: 1 })}
|
||||
smallTitle={title === filename}
|
||||
/>
|
||||
|
|
|
@ -65,6 +65,6 @@ if (price) {
|
|||
href={getLocalizedUrl(`/collectibles/${slug}`)}
|
||||
attributes={[...attributes, ...additionalAttributes]}
|
||||
icon="material-symbols:category"
|
||||
iconHoverLabel={t("global.previewTypes.collectible")}
|
||||
iconHoverLabel={t("global.collections.collectibles", { count: 1 })}
|
||||
disableRoundedTop
|
||||
/>
|
||||
|
|
|
@ -167,21 +167,21 @@ for (const attribute of attributes) {
|
|||
height: auto;
|
||||
|
||||
&.rounded-top {
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
border-top-left-radius: 14px;
|
||||
border-top-right-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
& > #icon-container {
|
||||
&.thumbnail-alt {
|
||||
margin: 0.4em;
|
||||
margin: 6px;
|
||||
margin-bottom: unset;
|
||||
aspect-ratio: 3/2;
|
||||
background-color: var(--color-elevation-2);
|
||||
color: var(--color-base-400);
|
||||
display: grid;
|
||||
place-content: center;
|
||||
border-radius: 0.7em;
|
||||
border-radius: 8px;
|
||||
|
||||
& > svg {
|
||||
width: 64px;
|
||||
|
@ -191,18 +191,18 @@ for (const attribute of attributes) {
|
|||
|
||||
&:not(.thumbnail-alt) {
|
||||
position: absolute;
|
||||
top: 0.4em;
|
||||
left: 0.4em;
|
||||
padding: 0.5em;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
padding: 8px;
|
||||
backdrop-filter: blur(5px);
|
||||
background-color: color-mix(in srgb, var(--color-elevation-2) 60%, transparent);
|
||||
}
|
||||
|
||||
&.rounded-top {
|
||||
border-radius: 0.7em;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
border-bottom-right-radius: 0.7em;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
& > #footer {
|
||||
|
|
|
@ -31,6 +31,6 @@ const { pretitle, title, subtitle, language } =
|
|||
href={getLocalizedUrl(`/images/${id}`)}
|
||||
attributes={attributes}
|
||||
icon="material-symbols:imagesmode"
|
||||
iconHoverLabel={t("global.previewTypes.image")}
|
||||
iconHoverLabel={t("global.collections.images", { count: 1 })}
|
||||
smallTitle={title === filename}
|
||||
/>
|
||||
|
|
|
@ -39,5 +39,5 @@ const metaAttributes: Attribute[] = [
|
|||
href={getLocalizedUrl(`/pages/${slug}`)}
|
||||
attributes={[...attributes, ...metaAttributes]}
|
||||
icon="material-symbols:docs"
|
||||
iconHoverLabel={t("global.previewTypes.page")}
|
||||
iconHoverLabel={t("global.collections.pages", { count: 1 })}
|
||||
/>
|
||||
|
|
|
@ -41,6 +41,6 @@ const attributesWithMeta = [
|
|||
href={getLocalizedUrl(`/videos/${id}`)}
|
||||
attributes={attributesWithMeta}
|
||||
icon="material-symbols:smart-display"
|
||||
iconHoverLabel={t("global.previewTypes.video")}
|
||||
iconHoverLabel={t("global.collections.videos", { count: 1 })}
|
||||
smallTitle={title === filename}
|
||||
/>
|
||||
|
|
|
@ -152,4 +152,25 @@ export type WordingKey =
|
|||
| "global.previewTypes.pdf"
|
||||
| "collectibles.files"
|
||||
| "pages.credits.translationLabel"
|
||||
| "pages.credits.currentLocale";
|
||||
| "pages.credits.currentLocale"
|
||||
| "global.media.attributes.resolution"
|
||||
| "search.searchBar.placeholder"
|
||||
| "global.collections.collectibles"
|
||||
| "global.collections.pages"
|
||||
| "global.collections.folders"
|
||||
| "global.collections.audios"
|
||||
| "global.collections.videos"
|
||||
| "global.collections.images"
|
||||
| "global.collections.files"
|
||||
| "global.collections.recorders"
|
||||
| "global.collections.chronologyEvents"
|
||||
| "global.collections.all"
|
||||
| "search.resultCount"
|
||||
| "search.searchBar.submitButton.tooltip"
|
||||
| "search.noResult.title"
|
||||
| "search.noResult.message"
|
||||
| "search.collectionFilter.tooltip"
|
||||
| "paginator.goFirstPageButton"
|
||||
| "paginator.goPreviousPageButton"
|
||||
| "paginator.goNextPageButton"
|
||||
| "paginator.goLastPageButton";
|
||||
|
|
|
@ -19,10 +19,12 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
|||
</a>
|
||||
</div>
|
||||
<div id="img-container">
|
||||
<img src={"/img/search-empty.webp"} width="1054" height="1100" />
|
||||
<img src={"/img/404-illustration.webp"} width="1054" height="1100" />
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||
|
||||
<style>
|
||||
.app {
|
||||
display: flex;
|
||||
|
|
|
@ -19,7 +19,11 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
|||
<div id="text-container">
|
||||
<h1 class="font-serif font-5xl">500</h1>
|
||||
<h2 class="font-3xl">Server Error</h2>
|
||||
<p>Please contact <a href={getLocalizedUrl("/discord")} target="_blank">website technical administrator</a>.</p>
|
||||
<p>
|
||||
Please contact <a href={getLocalizedUrl("/discord")} target="_blank">
|
||||
website technical administrator
|
||||
</a>.
|
||||
</p>
|
||||
<a href={getLocalizedUrl("/")}>
|
||||
<Button icon="material-symbols:home" title={t("home.title")} />
|
||||
</a>
|
||||
|
@ -40,6 +44,8 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
|||
}
|
||||
</AppLayout>
|
||||
|
||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||
|
||||
<style>
|
||||
.app {
|
||||
display: flex;
|
||||
|
|
|
@ -29,7 +29,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
|||
<section id="library">
|
||||
<h2 class="font-serif font-3xl">{t("home.librarySection.title")}</h2>
|
||||
<p set:html={t("home.librarySection.description")} />
|
||||
<a href={getLocalizedUrl("/search")} hidden>
|
||||
<a href={getLocalizedUrl("/search")}>
|
||||
<Button
|
||||
class="section-button"
|
||||
title={t("home.librarySection.button")}
|
||||
|
@ -152,6 +152,11 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
|
|||
}
|
||||
|
||||
&#library {
|
||||
& > a {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
& > .grid {
|
||||
@media (max-width: 40rem) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { getI18n } from "src/i18n/i18n";
|
||||
|
||||
interface Props {
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
getUrl: (newPage: number) => string;
|
||||
}
|
||||
|
||||
const { currentPage, getUrl, pageCount } = Astro.props;
|
||||
const { t, formatNumber } = await getI18n(Astro.locals.currentLocale);
|
||||
---
|
||||
|
||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||
|
||||
{
|
||||
pageCount > 1 && (
|
||||
<div class="pagination">
|
||||
<a
|
||||
id="first-page-button"
|
||||
class="pressable"
|
||||
aria-disabled={currentPage <= 1}
|
||||
href={getUrl(1)}
|
||||
title={t("paginator.goFirstPageButton")}>
|
||||
<Icon name="material-symbols:keyboard-double-arrow-left" />
|
||||
</a>
|
||||
<a
|
||||
id="previous-page-button"
|
||||
class="pressable"
|
||||
aria-disabled={currentPage <= 1}
|
||||
href={getUrl(currentPage - 1)}
|
||||
title={t("paginator.goPreviousPageButton")}>
|
||||
<Icon name="material-symbols:chevron-left" />
|
||||
</a>
|
||||
<p class="font-l">{`${formatNumber(currentPage)} / ${formatNumber(pageCount)}`}</p>
|
||||
<a
|
||||
id="next-page-button"
|
||||
class="pressable"
|
||||
aria-disabled={currentPage >= pageCount}
|
||||
href={getUrl(currentPage + 1)}
|
||||
title={t("paginator.goNextPageButton")}>
|
||||
<Icon name="material-symbols:chevron-right" />
|
||||
</a>
|
||||
<a
|
||||
id="last-page-button"
|
||||
class="pressable"
|
||||
aria-disabled={currentPage >= pageCount}
|
||||
href={getUrl(pageCount)}
|
||||
title={t("paginator.goLastPageButton")}>
|
||||
<Icon name="material-symbols:double-arrow-rounded" />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||
|
||||
<style>
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
place-items: center;
|
||||
|
||||
& > p {
|
||||
margin-inline: 12px;
|
||||
}
|
||||
|
||||
& > a {
|
||||
border: 2px solid var(--color-base-500);
|
||||
border-radius: 6px;
|
||||
padding: 0.5em;
|
||||
place-content: center;
|
||||
|
||||
> svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-elevation-2);
|
||||
border-color: var(--color-base-1000);
|
||||
}
|
||||
|
||||
&[aria-disabled] {
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
border-color: var(--color-base-1000);
|
||||
color: var(--color-base-1000);
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{/* ------------------------------------------- JS --------------------------------------------- */}
|
||||
|
||||
<script>
|
||||
document.addEventListener("keydown", async (e) => {
|
||||
const previousPageButton = document.getElementById("previous-page-button");
|
||||
const nextPageButton = document.getElementById("next-page-button");
|
||||
|
||||
if (!previousPageButton) return;
|
||||
if (!nextPageButton) return;
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
if (!previousPageButton.hasAttribute("aria-disabled")) previousPageButton.click();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
if (!nextPageButton.hasAttribute("aria-disabled")) nextPageButton.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,282 @@
|
|||
---
|
||||
import AppLayout from "components/AppLayout/AppLayout.astro";
|
||||
import ErrorMessage from "components/ErrorMessage.astro";
|
||||
import AudioPreview from "components/Previews/AudioPreview.astro";
|
||||
import FilePreview from "components/Previews/FilePreview.astro";
|
||||
import ImagePreview from "components/Previews/ImagePreview.astro";
|
||||
import PagePreview from "components/Previews/PagePreview.astro";
|
||||
import VideoPreview from "components/Previews/VideoPreview.astro";
|
||||
import { getI18n } from "src/i18n/i18n";
|
||||
import { Collections } from "src/shared/payload/payload-sdk";
|
||||
import { meilisearch } from "src/utils/meilisearch";
|
||||
import CollectiblePreview from "components/Previews/CollectiblePreview.astro";
|
||||
import Button from "components/Button.astro";
|
||||
import Paginator from "./_components/Paginator.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import HomeTitle from "../_components/HomeTitle.astro";
|
||||
|
||||
type State = {
|
||||
q: string;
|
||||
page: number;
|
||||
type?: string | undefined;
|
||||
};
|
||||
|
||||
const { t, getLocalizedUrl, formatNumber } = await getI18n(Astro.locals.currentLocale);
|
||||
|
||||
const reqUrl = new URL(Astro.request.url);
|
||||
|
||||
const state: State = {
|
||||
q: reqUrl.searchParams.get("q") ?? "",
|
||||
page: parseInt(reqUrl.searchParams.get("page") ?? "1"),
|
||||
type: reqUrl.searchParams.get("type") ?? undefined,
|
||||
};
|
||||
|
||||
const { hits, page, query, totalHits, totalPages } = await meilisearch.search({
|
||||
page: state.page,
|
||||
q: state.q,
|
||||
types: state.type,
|
||||
});
|
||||
|
||||
const types: { type: Collections; title: string }[] = [
|
||||
{
|
||||
type: Collections.Collectibles,
|
||||
title: t("global.collections.collectibles", { count: Infinity }),
|
||||
},
|
||||
{ type: Collections.Pages, title: t("global.collections.pages", { count: Infinity }) },
|
||||
{ type: Collections.Folders, title: t("global.collections.folders", { count: Infinity }) },
|
||||
{
|
||||
type: Collections.Audios,
|
||||
title: t("global.collections.audios", { count: Infinity }),
|
||||
},
|
||||
{
|
||||
type: Collections.Images,
|
||||
title: t("global.collections.images", { count: Infinity }),
|
||||
},
|
||||
{
|
||||
type: Collections.Videos,
|
||||
title: t("global.collections.videos", { count: Infinity }),
|
||||
},
|
||||
{
|
||||
type: Collections.Files,
|
||||
title: t("global.collections.files", { count: Infinity }),
|
||||
},
|
||||
{ type: Collections.Recorders, title: t("global.collections.recorders", { count: Infinity }) },
|
||||
{
|
||||
type: Collections.ChronologyEvents,
|
||||
title: t("global.collections.chronologyEvents"),
|
||||
},
|
||||
];
|
||||
|
||||
const getSearchUrl = (newState: State): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (newState.q !== "") searchParams.set("q", newState.q);
|
||||
if (newState.page !== 1) searchParams.set("page", newState.page.toString());
|
||||
if (newState.type) searchParams.set("type", newState.type);
|
||||
|
||||
return [getLocalizedUrl("/search"), searchParams.toString()].filter(Boolean).join("?");
|
||||
};
|
||||
---
|
||||
|
||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||
|
||||
<AppLayout hideSearchButton>
|
||||
<div class="center">
|
||||
<HomeTitle />
|
||||
|
||||
<form action={getLocalizedUrl("/search")} method="get">
|
||||
<div class="select-wrapper">
|
||||
<select name="type" title={t("search.collectionFilter.tooltip")}>
|
||||
<option value="" selected={state.type === undefined}>
|
||||
{t("global.collections.all")}
|
||||
</option>
|
||||
{
|
||||
types.map(({ title, type }) => (
|
||||
<option value={type} selected={type === state.type}>
|
||||
{title}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
<Icon name="material-symbols:keyboard-arrow-down" />
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
class="pressable"
|
||||
name="q"
|
||||
placeholder={t("search.searchBar.placeholder")}
|
||||
value={query}
|
||||
/>
|
||||
<Button
|
||||
class="submit"
|
||||
icon="material-symbols:search"
|
||||
ariaLabel={t("search.searchBar.submitButton.tooltip")}
|
||||
/>
|
||||
</form>
|
||||
<p id="advanced-link" class="font-s">
|
||||
<!-- Looking for more <a href={getLocalizedUrl("/search/advanced")}>advanced search options</a> -->
|
||||
</p>
|
||||
|
||||
{
|
||||
totalPages > 1 && (
|
||||
<>
|
||||
<Paginator
|
||||
currentPage={page}
|
||||
pageCount={totalPages}
|
||||
getUrl={(newPage) => getSearchUrl({ ...state, page: newPage })}
|
||||
/>
|
||||
<p class="font-s">
|
||||
{t("search.resultCount", {
|
||||
results: formatNumber(hits.length),
|
||||
totalResults: formatNumber(totalHits),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
hits.length === 0 && (
|
||||
<div class="center">
|
||||
<p class="font-2xl font-serif">{t("search.noResult.title")}</p>
|
||||
<p>{t("search.noResult.message", { query })}</p>
|
||||
<img id="no-result" src={"/img/search-no-results.webp"} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div id="results">
|
||||
{
|
||||
hits.map(({ type, data }) => {
|
||||
switch (type) {
|
||||
case Collections.Collectibles:
|
||||
return <CollectiblePreview collectible={data} />;
|
||||
|
||||
case Collections.Pages:
|
||||
return <PagePreview page={data} />;
|
||||
|
||||
case Collections.Audios:
|
||||
return <AudioPreview audio={data} />;
|
||||
|
||||
case Collections.Videos:
|
||||
return <VideoPreview video={data} />;
|
||||
|
||||
case Collections.Files:
|
||||
return <FilePreview file={data} />;
|
||||
|
||||
case Collections.Images:
|
||||
return <ImagePreview image={data} />;
|
||||
|
||||
case Collections.Recorders:
|
||||
case Collections.Folders:
|
||||
case Collections.ChronologyEvents:
|
||||
default:
|
||||
return <ErrorMessage title={`Unsupported result type: ${type}`} />;
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class="center">
|
||||
<Paginator
|
||||
currentPage={page}
|
||||
pageCount={totalPages}
|
||||
getUrl={(newPage) => getSearchUrl({ ...state, page: newPage })}
|
||||
/>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||
|
||||
<style>
|
||||
form {
|
||||
margin-top: 64px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 30rem;
|
||||
|
||||
& > * {
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
& > .select-wrapper {
|
||||
position: relative;
|
||||
|
||||
& > svg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
translate: 0 -50%;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
& > select {
|
||||
border-top-left-radius: 9999px;
|
||||
border-bottom-left-radius: 9999px;
|
||||
border: 2px solid var(--color-base-650);
|
||||
border-right: unset;
|
||||
padding-left: 24px;
|
||||
padding-right: 32px;
|
||||
background-color: var(--color-elevation-1);
|
||||
appearance: none;
|
||||
height: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
& > input[type="search"] {
|
||||
border-right: unset;
|
||||
padding-inline: 1em;
|
||||
padding-block: unset;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
background-color: var(--color-elevation-0);
|
||||
color: var(--color-base-800);
|
||||
|
||||
&:focus-visible {
|
||||
outline: 3px solid var(--color-base-1000);
|
||||
}
|
||||
}
|
||||
|
||||
& > .submit {
|
||||
border-top-left-radius: unset;
|
||||
border-bottom-left-radius: unset;
|
||||
padding-left: 0.75em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#advanced-link {
|
||||
margin-bottom: 98px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#results {
|
||||
margin-top: 5em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: clamp(6px, 2vmin, 16px);
|
||||
align-items: start;
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 24rem) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: grid;
|
||||
flex-direction: column;
|
||||
justify-items: center;
|
||||
gap: 12px;
|
||||
margin-block: 64px;
|
||||
}
|
||||
|
||||
#no-result {
|
||||
max-height: 70vh;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,115 @@
|
|||
import {
|
||||
Collections,
|
||||
type EndpointAudio,
|
||||
type EndpointChronologyEvent,
|
||||
type EndpointCollectible,
|
||||
type EndpointFile,
|
||||
type EndpointFolder,
|
||||
type EndpointImage,
|
||||
type EndpointPage,
|
||||
type EndpointRecorder,
|
||||
type EndpointVideo,
|
||||
} from "src/shared/payload/payload-sdk";
|
||||
|
||||
export enum Indexes {
|
||||
DOCUMENT = "DOCUMENT",
|
||||
}
|
||||
|
||||
export type MeiliDocument = {
|
||||
meilid: string;
|
||||
id: string;
|
||||
languages: string[];
|
||||
title?: string;
|
||||
content?: string;
|
||||
} & (
|
||||
| {
|
||||
type: Collections.Collectibles;
|
||||
data: EndpointCollectible;
|
||||
}
|
||||
| {
|
||||
type: Collections.Pages;
|
||||
data: EndpointPage;
|
||||
}
|
||||
| {
|
||||
type: Collections.Folders;
|
||||
data: EndpointFolder;
|
||||
}
|
||||
| {
|
||||
type: Collections.Videos;
|
||||
data: EndpointVideo;
|
||||
}
|
||||
| {
|
||||
type: Collections.Audios;
|
||||
data: EndpointAudio;
|
||||
}
|
||||
| {
|
||||
type: Collections.Images;
|
||||
data: EndpointImage;
|
||||
}
|
||||
| {
|
||||
type: Collections.Files;
|
||||
data: EndpointFile;
|
||||
}
|
||||
| {
|
||||
type: Collections.Recorders;
|
||||
data: EndpointRecorder;
|
||||
}
|
||||
| {
|
||||
type: Collections.ChronologyEvents;
|
||||
data: EndpointChronologyEvent;
|
||||
}
|
||||
);
|
||||
|
||||
export type SearchResponse<T> = {
|
||||
hits: T[];
|
||||
query: string;
|
||||
processingTimeMs: number;
|
||||
hitsPerPage: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
totalHits: number;
|
||||
facetDistribution: Record<"type" | "languages", Record<string, number>>;
|
||||
};
|
||||
|
||||
export type SearchRequest = {
|
||||
q: string;
|
||||
page: number;
|
||||
types?: string[] | string | undefined;
|
||||
};
|
||||
|
||||
export class Meilisearch {
|
||||
constructor(
|
||||
private readonly apiURL: string,
|
||||
private readonly bearer: string
|
||||
) {}
|
||||
|
||||
async search({ q, page, types }: SearchRequest): Promise<SearchResponse<MeiliDocument>> {
|
||||
const filter: string[] = [];
|
||||
|
||||
if (types) {
|
||||
if (typeof types === "string") {
|
||||
filter.push(`type = ${types}`);
|
||||
} else {
|
||||
filter.push(`type IN [${types.join(", ")}]`);
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
q,
|
||||
page,
|
||||
hitsPerPage: 25,
|
||||
filter,
|
||||
sort: ["updatedAt:desc"]
|
||||
};
|
||||
|
||||
const result = await fetch(`${this.apiURL}/indexes/DOCUMENT/search`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.bearer}`,
|
||||
},
|
||||
});
|
||||
return await result.json();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { Meilisearch } from "src/shared/meilisearch/meilisearch-sdk";
|
||||
|
||||
export const meilisearch = new Meilisearch(
|
||||
import.meta.env.MEILISEARCH_URL,
|
||||
import.meta.env.MEILISEARCH_MASTER_KEY
|
||||
);
|
Loading…
Reference in New Issue