feat: initial client side searching for resources
All checks were successful
Deploy to SFTP Server / build (push) Successful in 14m30s
All checks were successful
Deploy to SFTP Server / build (push) Successful in 14m30s
This commit is contained in:
43
src/components/ClientSearch.svelte
Normal file
43
src/components/ClientSearch.svelte
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let searchTerm = "";
|
||||||
|
let staticContainer: HTMLElement | null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
staticContainer = document.getElementById("resource-list-static");
|
||||||
|
});
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
if (!staticContainer) return;
|
||||||
|
|
||||||
|
const lowerCaseSearchTerm = searchTerm.toLowerCase().trim();
|
||||||
|
|
||||||
|
if (!lowerCaseSearchTerm) {
|
||||||
|
[...staticContainer?.children].forEach((element: HTMLElement) => {
|
||||||
|
element.style.display = "";
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of staticContainer?.children) {
|
||||||
|
const el = element as HTMLElement;
|
||||||
|
const isMatch = el.dataset.searchTerm?.includes(lowerCaseSearchTerm);
|
||||||
|
if (isMatch) {
|
||||||
|
el.style.display = "";
|
||||||
|
} else {
|
||||||
|
el.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: search(searchTerm);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="search-wrapper my-4 noise relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={searchTerm}
|
||||||
|
placeholder={`Search...`}
|
||||||
|
class="w-full p-2 gradient border-1 border-neutral rounded noise" />
|
||||||
|
</div>
|
||||||
@@ -70,7 +70,7 @@ const definedSizes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const sizes = thumbnail
|
const sizes = thumbnail
|
||||||
? [definedSizes[0]]
|
? [definedSizes[1]]
|
||||||
: definedSizes.filter((size) => !maxWidth || size.width <= maxWidth);
|
: definedSizes.filter((size) => !maxWidth || size.width <= maxWidth);
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
typeof d === "string" ? new Date(d) : d;
|
typeof d === "string" ? new Date(d) : d;
|
||||||
|
|
||||||
const iso = (d: string | Date) => {
|
const iso = (d: string | Date) => {
|
||||||
if(!d) return ""
|
if (!d) return "";
|
||||||
const v = toDate(d);
|
const v = toDate(d);
|
||||||
if(!v?.getTime) return ""
|
if (!v?.getTime) return "";
|
||||||
return isNaN(v.getTime()) ? "" : v.toISOString();
|
return isNaN(v.getTime()) ? "" : v.toISOString();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,12 +50,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrapper > * {
|
.wrapper > * {
|
||||||
padding: 2px 11px;
|
padding: 2px 11px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ const { resource } = Astro.props;
|
|||||||
resource?.content?.image && (
|
resource?.content?.image && (
|
||||||
<Image
|
<Image
|
||||||
hash
|
hash
|
||||||
src={{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata}
|
src={
|
||||||
|
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
|
||||||
|
}
|
||||||
alt="Cover for {resource?.content?.name}"
|
alt="Cover for {resource?.content?.name}"
|
||||||
class="rounded-2xl overflow-hidden"
|
class="rounded-2xl overflow-hidden"
|
||||||
pictureClass="rounded-2xl"
|
pictureClass="rounded-2xl"
|
||||||
@@ -21,4 +23,7 @@ const { resource } = Astro.props;
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div set:html={markdownToHtml(resource?.content?.articleBody)} />
|
<div
|
||||||
|
class="flex flex-col gap-4"
|
||||||
|
set:html={markdownToHtml(resource?.content?.articleBody)}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -4,18 +4,30 @@ import { markdownToHtml } from "@helpers/markdown";
|
|||||||
import Image from "@components/Image.astro";
|
import Image from "@components/Image.astro";
|
||||||
import type { ImageMetadata } from "astro";
|
import type { ImageMetadata } from "astro";
|
||||||
|
|
||||||
const { resource } = Astro.props
|
const { resource } = Astro.props;
|
||||||
const ingredients = resource?.content?.recipeIngredient || [];
|
const ingredients = resource?.content?.recipeIngredient || [];
|
||||||
const instructions = resource?.content?.recipeInstructions || [];
|
const instructions = resource?.content?.recipeInstructions || [];
|
||||||
---
|
---
|
||||||
|
|
||||||
<h1 class="text-4xl">{resource?.content?.name}</h1>
|
<h1 class="text-4xl">{resource?.content?.name}</h1>
|
||||||
<div>
|
<div>
|
||||||
{resource?.content?.image && <Image hash src={{src: memorium.getImageUrl(resource.content.image)} as ImageMetadata} alt="Cover for {resource?.content?.name}" class="rounded-2xl overflow-hidden" pictureClass="rounded-2xl" />}
|
{
|
||||||
|
resource?.content?.image && (
|
||||||
|
<Image
|
||||||
|
hash
|
||||||
|
src={
|
||||||
|
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
|
||||||
|
}
|
||||||
|
alt="Cover for {resource?.content?.name}"
|
||||||
|
class="rounded-2xl overflow-hidden"
|
||||||
|
pictureClass="rounded-2xl"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<p>{resource?.content?.description}</p>
|
<main class="flex flex-col gap-4" set:html={markdownToHtml(resource?.content?.description ?? "")} />
|
||||||
<h2 class="text-2xl">Ingredients</h2>
|
<h2 class="text-2xl">Ingredients</h2>
|
||||||
<ul>
|
<ul class="list-disc">
|
||||||
{
|
{
|
||||||
ingredients.map((ingredient: string) => (
|
ingredients.map((ingredient: string) => (
|
||||||
<li set:html={markdownToHtml(ingredient)} />
|
<li set:html={markdownToHtml(ingredient)} />
|
||||||
@@ -24,7 +36,7 @@ const instructions = resource?.content?.recipeInstructions || [];
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 class="text-2xl">Steps</h2>
|
<h2 class="text-2xl">Steps</h2>
|
||||||
<ol>
|
<ol class="list-decimal">
|
||||||
{
|
{
|
||||||
instructions.map((ingredient: string) => (
|
instructions.map((ingredient: string) => (
|
||||||
<li set:html={markdownToHtml(ingredient)} />
|
<li set:html={markdownToHtml(ingredient)} />
|
||||||
|
|||||||
@@ -16,16 +16,13 @@ import ImageSlider from "@components/ImageSlider.svelte"
|
|||||||
|
|
||||||
<ImageGallery client:load/>
|
<ImageGallery client:load/>
|
||||||
|
|
||||||
|
|
||||||
## May 27th
|
## May 27th
|
||||||
|
|
||||||
First day, arrival at the sea.
|
|
||||||
|
|
||||||
import image1 from "images/20250527_125228.jpg"
|
import image1 from "images/20250527_125228.jpg"
|
||||||
import image16 from "images/PXL_20250527_101057540.MP.jpg"
|
import image16 from "images/PXL_20250527_101057540.MP.jpg"
|
||||||
import image17 from "images/PXL_20250527_100728883.jpg"
|
import image17 from "images/PXL_20250527_100728883.jpg"
|
||||||
|
|
||||||
<ImageSlider title="Hike" client:load>
|
<ImageSlider title="First day, arrival at the sea." client:load>
|
||||||
<Image src={image1} alt="A cow on a meadow in front of the sea"/>
|
<Image src={image1} alt="A cow on a meadow in front of the sea"/>
|
||||||
<Image src={image17} alt="A person on flysch rock in front of a cave at the sea"/>
|
<Image src={image17} alt="A person on flysch rock in front of a cave at the sea"/>
|
||||||
<Image src={image16} alt="A person in a cave from which you can see the sea"/>
|
<Image src={image16} alt="A person in a cave from which you can see the sea"/>
|
||||||
@@ -33,12 +30,10 @@ import image17 from "images/PXL_20250527_100728883.jpg"
|
|||||||
|
|
||||||
## May 28th
|
## May 28th
|
||||||
|
|
||||||
First day of hiking
|
|
||||||
|
|
||||||
import image15 from "images/PXL_20250528_121633744.MP.jpg"
|
import image15 from "images/PXL_20250528_121633744.MP.jpg"
|
||||||
import image19 from "images/20250528_164715.jpg"
|
import image19 from "images/20250528_164715.jpg"
|
||||||
|
|
||||||
<ImageSlider title="Hike" client:load>
|
<ImageSlider title="First day of hiking" client:load>
|
||||||
<Image src={image15} alt="Us after the first 300 meters"/>
|
<Image src={image15} alt="Us after the first 300 meters"/>
|
||||||
<Image src={image19} alt="Many mountain goats that like to lick the salt from the skin"/>
|
<Image src={image19} alt="Many mountain goats that like to lick the salt from the skin"/>
|
||||||
</ImageSlider>
|
</ImageSlider>
|
||||||
@@ -127,5 +122,5 @@ Final-descent
|
|||||||
|
|
||||||
import image27 from "images/PXL_20250605_100650266.jpg"
|
import image27 from "images/PXL_20250605_100650266.jpg"
|
||||||
|
|
||||||
<Image alt="" src={image27} alt="27"/>
|
<Image alt="" src={image27} alt="The last walk down the mountain"/>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import ImageSlider from "@components/ImageSlider.svelte"
|
|||||||
|
|
||||||
<ImageGallery client:load/>
|
<ImageGallery client:load/>
|
||||||
|
|
||||||
|
|
||||||
## 27. May
|
## 27. May
|
||||||
|
|
||||||
Erster Tag, Ankunft am Meer.
|
Erster Tag, Ankunft am Meer.
|
||||||
@@ -25,7 +24,7 @@ import image1 from "images/20250527_125228.jpg"
|
|||||||
import image16 from "images/PXL_20250527_101057540.MP.jpg"
|
import image16 from "images/PXL_20250527_101057540.MP.jpg"
|
||||||
import image17 from "images/PXL_20250527_100728883.jpg"
|
import image17 from "images/PXL_20250527_100728883.jpg"
|
||||||
|
|
||||||
<ImageSlider title="Wanderung" client:load>
|
<ImageSlider client:load>
|
||||||
<Image src={image1} alt="Bild einer Kuh auf einer Wiese vor dem Meer"/>
|
<Image src={image1} alt="Bild einer Kuh auf einer Wiese vor dem Meer"/>
|
||||||
<Image src={image17} alt="Person auf flysch gestein vor einer Höhle am Meer"/>
|
<Image src={image17} alt="Person auf flysch gestein vor einer Höhle am Meer"/>
|
||||||
<Image src={image16} alt="Bild von Person in einer Höhle aus der man das Meer sieht"/>
|
<Image src={image16} alt="Bild von Person in einer Höhle aus der man das Meer sieht"/>
|
||||||
@@ -38,7 +37,7 @@ Erster Wandertag
|
|||||||
import image15 from "images/PXL_20250528_121633744.MP.jpg"
|
import image15 from "images/PXL_20250528_121633744.MP.jpg"
|
||||||
import image19 from "images/20250528_164715.jpg"
|
import image19 from "images/20250528_164715.jpg"
|
||||||
|
|
||||||
<ImageSlider title="Wanderung" client:load>
|
<ImageSlider client:load>
|
||||||
<Image src={image15} alt="Wir nach den ersten 300 Metern"/>
|
<Image src={image15} alt="Wir nach den ersten 300 Metern"/>
|
||||||
<Image src={image19} alt="Viele Bergziegen die gerne das Salz von der Haut lecken"/>
|
<Image src={image19} alt="Viele Bergziegen die gerne das Salz von der Haut lecken"/>
|
||||||
</ImageSlider>
|
</ImageSlider>
|
||||||
@@ -56,8 +55,6 @@ import image13 from "images/PXL_20250529_201559403.jpg"
|
|||||||
|
|
||||||
## 30-31. May
|
## 30-31. May
|
||||||
|
|
||||||
Gewitter und Restday
|
|
||||||
|
|
||||||
import image11 from "images/PXL_20250530_110041174.jpg"
|
import image11 from "images/PXL_20250530_110041174.jpg"
|
||||||
import image10 from "images/PXL_20250530_135631186.MP.jpg"
|
import image10 from "images/PXL_20250530_135631186.MP.jpg"
|
||||||
import image8 from "images/PXL_20250530_170114907.MP.jpg"
|
import image8 from "images/PXL_20250530_170114907.MP.jpg"
|
||||||
@@ -127,4 +124,4 @@ Finaler Abstieg
|
|||||||
|
|
||||||
import image27 from "images/PXL_20250605_100650266.jpg"
|
import image27 from "images/PXL_20250605_100650266.jpg"
|
||||||
|
|
||||||
<Image alt="" src={image27} alt="27"/>
|
<Image alt="" src={image27} alt="The last walk down the mountain"/>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export async function listResource(
|
|||||||
id: string,
|
id: string,
|
||||||
): Promise<MemoriumEntry | undefined> {
|
): Promise<MemoriumEntry | undefined> {
|
||||||
const url = `${SERVER_URL}/resources/${id}`;
|
const url = `${SERVER_URL}/resources/${id}`;
|
||||||
|
console.log(url);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export async function getStaticPaths() {
|
|||||||
const paths = await Promise.all(
|
const paths = await Promise.all(
|
||||||
resourceTypes.map(async (resourceType) => {
|
resourceTypes.map(async (resourceType) => {
|
||||||
const resources = await memorium.listResource(resourceType.id);
|
const resources = await memorium.listResource(resourceType.id);
|
||||||
console.log({resources:resources?.content[0]})
|
|
||||||
return resources?.content?.map((res: any) => {
|
return resources?.content?.map((res: any) => {
|
||||||
return {
|
return {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import HeroCard from "@components/HeroCard.astro";
|
|||||||
import * as memorium from "@helpers/memorium";
|
import * as memorium from "@helpers/memorium";
|
||||||
import { resources as resourceTypes } from "../resources.ts";
|
import { resources as resourceTypes } from "../resources.ts";
|
||||||
import { useTranslations } from "@i18n/utils";
|
import { useTranslations } from "@i18n/utils";
|
||||||
|
import ClientSearch from "@components/ClientSearch.svelte";
|
||||||
|
|
||||||
const { resourceType } = Astro.params;
|
const { resourceType } = Astro.params;
|
||||||
|
|
||||||
@@ -32,15 +33,31 @@ export async function getStaticPaths() {
|
|||||||
function isValidResource(res: any) {
|
function isValidResource(res: any) {
|
||||||
return !!res?.content?._type;
|
return !!res?.content?._type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSearchTerm(res:any){
|
||||||
|
if(!res) return "";
|
||||||
|
return Object.keys(res).map((key) => {
|
||||||
|
if(key.startsWith("_")) return;
|
||||||
|
const value = res[key];
|
||||||
|
if(Array.isArray(value)) return value.join(" ");
|
||||||
|
if(typeof value === "object") return buildSearchTerm(value);
|
||||||
|
return value
|
||||||
|
}).filter(s => !!s?.length).join(" ").toLowerCase();
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Max Richter">
|
<Layout title="Max Richter">
|
||||||
<h1 class="text-4xl mb-4">{t(resourceType)}</h1>
|
<h1 class="text-4xl mb-4">{t(resourceType)}</h1>
|
||||||
<p>{t(`${resourceType as "articles"}.description`)}</p>
|
<p>{t(`${resourceType as "articles"}.description`)}</p>
|
||||||
|
|
||||||
|
<ClientSearch resourceType={resourceType} client:load />
|
||||||
|
|
||||||
|
<div id="resource-list-static"class="flex flex-col gap-6">
|
||||||
{
|
{
|
||||||
resources.content
|
resources?.content
|
||||||
.filter((res: any) => isValidResource(res))
|
.filter((res: any) => isValidResource(res))
|
||||||
.map((resource: any) => (
|
.map((resource: any) => (
|
||||||
|
<div data-search-term={buildSearchTerm(resource?.content)}>
|
||||||
<HeroCard
|
<HeroCard
|
||||||
post={{
|
post={{
|
||||||
collection: "resources/" + resourceType,
|
collection: "resources/" + resourceType,
|
||||||
@@ -59,6 +76,8 @@ function isValidResource(res: any) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -3,16 +3,47 @@ import Layout from "@layouts/Layout.astro";
|
|||||||
import HeroCard from "@components/HeroCard.astro";
|
import HeroCard from "@components/HeroCard.astro";
|
||||||
import { resources } from "./resources.ts";
|
import { resources } from "./resources.ts";
|
||||||
import { useTranslations } from "@i18n/utils";
|
import { useTranslations } from "@i18n/utils";
|
||||||
|
import * as memorium from "@helpers/memorium";
|
||||||
|
|
||||||
const t = useTranslations(Astro.url);
|
const t = useTranslations(Astro.url);
|
||||||
|
|
||||||
|
async function getCoverImage(resourceName:string){
|
||||||
|
const resources = await memorium.listResource(resourceName);
|
||||||
|
if(!resources?.content) return "";
|
||||||
|
let amount = 0;
|
||||||
|
while (true){
|
||||||
|
amount++;
|
||||||
|
const randomResource = resources?.content[Math.floor(Math.random() * resources?.content.length)];
|
||||||
|
const cover = randomResource?.content?.cover || randomResource?.content?.image;
|
||||||
|
if(cover){
|
||||||
|
if(cover.startsWith("https://") || cover.startsWith("http://")){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return `https://marka.max-richter.dev/${cover}`;
|
||||||
|
}
|
||||||
|
if(amount > 50) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Max Richter">
|
<Layout title="Max Richter">
|
||||||
{resources.map((resource) =>
|
{
|
||||||
<HeroCard post={{
|
await Promise.all(resources.map(async (resource) => {
|
||||||
|
const cover = await getCoverImage(resource.id);
|
||||||
|
console.log({cover})
|
||||||
|
return <HeroCard
|
||||||
|
post={{
|
||||||
...resource,
|
...resource,
|
||||||
body: t(`${resource.id}.description`),
|
body: t(`${resource.id}.description`),
|
||||||
data: {
|
data: {
|
||||||
cover:{src:resource.cover}, title: t(resource.id)}}} />
|
cover: { src: cover },
|
||||||
)}
|
title: t(resource.id),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}))
|
||||||
|
}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user