#Can I filter a collection link this?

1 messages · Page 1 of 1 (latest)

solemn sandBOT
#

Hi @crude flower. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Yes, you can filter a collection like that, but there's an important consideration when using server-side rendering (SSR) that explains why your collection isn't being filtered as expected. Let me explain the issue and provide a solution.

The problem in your code is that you're using Astro.url.searchParams to get the category parameter, which is only available on the server side. In SSR mode, this code runs once when the server generates the initial HTML, but it doesn't automatically re-run when the client-side JavaScript updates the URL.

To make this work in SSR mode, you need to set up your page to re-render when the URL changes. Here's how you can modify your code to achieve this:

  1. First, change your page to a dynamic route. Rename your file to something like [...category].astro to catch all routes.

  2. Then, update your code to use Astro.params instead of Astro.url.searchParams:

---
import { getCollection } from "astro:content";

const { category = "workshops and food tours" } = Astro.params;

const experiences = await getCollection("experiences", ({ data }) => {
    return data.category.toLowerCase() === category.toLowerCase();
});
---

<section class="w-full max-w-[1200px]">
    <div
        class="flex justify-between gap-8 text-2xl font-bold text-woodsmoke/80"
    >
        <a href="/workshops-and-food-tours">WORKSHOPS AND FOOD TOURS</a>
        <a href="/aquatic">AQUATIC</a>
        <a href="/archeology">ARCHAEOLOGY</a>
        <a href="/adventure">ADVENTURE</a>
    </div>

    <div class="grid md:grid-cols-2 gap-8">
        {experiences.map((experience) => <span>{experience.data.title}</span>)}
    </div>
</section>
  1. Update your client-side JavaScript to use actual navigation instead of pushState:
#
<script>
    document.addEventListener("astro:page-load", () => {
        const links = document.querySelectorAll(
            "a[href]",
        ) as globalThis.NodeListOf<HTMLAnchorElement>;

        links.forEach((link) => {
            link.addEventListener("click", (event: MouseEvent) => {
                event.preventDefault();
                const href = link.getAttribute("href");
                window.location.href = href;
            });
        });
    });
</script>

This approach will cause a full page reload when changing categories, but it will ensure that the server re-renders the page with the correct filtered data each time.

If you want to avoid full page reloads and maintain a smoother user experience, you might want to consider using a client-side framework like React, Vue, or Svelte with Astro, or implement a more complex client-side rendering solution using Astro's partial hydration features.

For more information on SSR in Astro and how to handle dynamic routes, you can refer to the Astro documentation on Server-side Rendering.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@crude flower Kapa.ai is still learning and improving, please let me know how I did by reacting below.

crude flower
#

But I dont want that the page re-renders

crude flower
#

@solemn sand But I dont want that the page re-renders

solemn sandBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

I understand that you don't want the page to re-render. In this case, we need to approach the problem differently, using Astro's server-side rendering capabilities combined with client-side interactivity. Here's a solution that avoids page re-renders while still allowing you to filter the experiences:

  1. First, we'll use server-side rendering to fetch all experiences initially:
---
import { getCollection } from "astro:content";

const allExperiences = await getCollection("experiences");
---

<section class="w-full max-w-[1200px]">
    <div
        class="flex justify-between gap-8 text-2xl font-bold text-woodsmoke/80"
    >
        <button data-category="workshops and food tours">WORKSHOPS AND FOOD TOURS</button>
        <button data-category="aquatic">AQUATIC</button>
        <button data-category="archeology">ARCHAEOLOGY</button>
        <button data-category="adventure">ADVENTURE</button>
    </div>

    <div id="experiences-container" class="grid md:grid-cols-2 gap-8">
        {allExperiences.map((experience) => (
            <span data-category={experience.data.category.toLowerCase()}>{experience.data.title}</span>
        ))}
    </div>
</section>

<script>
    let currentCategory = "workshops and food tours";

    function filterExperiences(category) {
        const container = document.getElementById('experiences-container');
        const experiences = container.querySelectorAll('span');
        
        experiences.forEach(exp => {
            if (exp.dataset.category === category.toLowerCase()) {
                exp.style.display = '';
            } else {
                exp.style.display = 'none';
            }
        });
    }

    document.addEventListener("astro:page-load", () => {
        const buttons = document.querySelectorAll("button[data-category]");
#

buttons.forEach((button) => {
button.addEventListener("click", (event) => {
const category = event.currentTarget.getAttribute("data-category");
currentCategory = category;
filterExperiences(category);
history.pushState({}, "", ?category=${category});
});
});

    // Initial filter
    filterExperiences(currentCategory);
});

</script>


This solution uses Astro's [server-side rendering](<https://docs.astro.build/en/guides/server-side-rendering/>) to fetch all experiences on the initial page load. Then, it uses client-side JavaScript to filter the experiences without re-rendering the page.

Here's how it works:

1. We fetch all experiences on the server-side and render them into the DOM.
2. Each experience is given a `data-category` attribute for easy filtering.
3. The client-side JavaScript filters the experiences by showing/hiding them based on the selected category.
4. The URL is updated using `history.pushState()` for bookmarking purposes, but this doesn't trigger a page reload.

This approach gives you the best of both worlds:
- The initial page load benefits from server-side rendering, which is good for SEO and initial load performance.
- Subsequent category changes are handled entirely on the client-side, providing a smooth user experience without page reloads.

Remember to keep your `output: 'server'` setting in your `astro.config.mjs` file to enable server-side rendering.

[Server-side Rendering](<https://docs.astro.build/en/guides/server-side-rendering/>) in Astro allows you to combine static and dynamic content effectively, giving you control over how and when your content is rendered.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@crude flower Kapa.ai is still learning and improving, please let me know how I did by reacting below.

crude flower
#

@solemn sand There's no other way? Like passing the category as a prop to a component and the component gets only the experiences of the specific category, o directly pass to the component the experiences? Is this possible?

solemn sandBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, there are indeed other ways to achieve what you're looking for without page re-renders or client-side filtering. Let me explain two approaches that align with your suggestion:

  1. Passing the category as a prop to a component:

You can create a separate component that receives the category as a prop and fetches only the experiences for that specific category. Here's how you can do this:

---
// src/pages/index.astro
import { getCollection } from "astro:content";
import ExperiencesList from '../components/ExperiencesList.astro';

const categories = ["workshops and food tours", "aquatic", "archeology", "adventure"];
---

<section class="w-full max-w-[1200px]">
    <div class="flex justify-between gap-8 text-2xl font-bold text-woodsmoke/80">
        {categories.map(category => (
            <button data-category={category}>{category.toUpperCase()}</button>
        ))}
    </div>

    <div id="experiences-container">
        <ExperiencesList category={categories[0]} />
    </div>
</section>

<script>
    document.addEventListener("astro:page-load", () => {
        const container = document.getElementById('experiences-container');
        const buttons = document.querySelectorAll("button[data-category]");

        buttons.forEach((button) => {
            button.addEventListener("click", async (event) => {
                const category = event.currentTarget.getAttribute("data-category");
                const response = await fetch(`/api/experiences?category=${category}`);
                const html = await response.text();
                container.innerHTML = html;
                history.pushState({}, "", `?category=${category}`);
            });
        });
    });
</script>
---
// src/components/ExperiencesList.astro
import { getCollection } from "astro:content";

const { category } = Astro.props;
#

const experiences = await getCollection("experiences", ({ data }) => {
return data.category.toLowerCase() === category.toLowerCase();
});

<div class="grid md:grid-cols-2 gap-8">
{experiences.map((experience) => <span>{experience.data.title}</span>)}
</div>


```astro
---
// src/pages/api/experiences.astro
import ExperiencesList from '../../components/ExperiencesList.astro';

const category = Astro.url.searchParams.get('category') || 'workshops and food tours';

export const partial = true;
---

<ExperiencesList category={category} />

This approach uses Astro's API routes and partial rendering to fetch and render only the experiences for the selected category without a full page reload.

  1. Passing the filtered experiences directly to the component:

Alternatively, you can fetch all experiences on the server-side and pass the filtered experiences directly to the component:

---
// src/pages/index.astro
import { getCollection } from "astro:content";
import ExperiencesList from '../components/ExperiencesList.astro';

const allExperiences = await getCollection("experiences");
const categories = ["workshops and food tours", "aquatic", "archeology", "adventure"];

function getExperiencesByCategory(category) {
    return allExperiences.filter(exp => exp.data.category.toLowerCase() === category.toLowerCase());
}
---

<section class="w-full max-w-[1200px]">
    <div class="flex justify-between gap-8 text-2xl font-bold text-woodsmoke/80">
        {categories.map(category => (
            <button data-category={category}>{category.toUpperCase()}</button>
        ))}
    </div>
#

<div id="experiences-container">
<ExperiencesList experiences={getExperiencesByCategory(categories[0])} />
</div>
</section>

<script>
document.addEventListener("astro:page-load", () => {
const container = document.getElementById('experiences-container');
const buttons = document.querySelectorAll("button[data-category]");

    buttons.forEach((button) => {
        button.addEventListener("click", async (event) => {
            const category = event.currentTarget.getAttribute("data-category");
            const response = await fetch(`/api/experiences?category=${category}`);
            const html = await response.text();
            container.innerHTML = html;
            history.pushState({}, "", `?category=${category}`);
        });
    });
});

</script>


```astro
---
// src/components/ExperiencesList.astro
const { experiences } = Astro.props;
---

<div class="grid md:grid-cols-2 gap-8">
    {experiences.map((experience) => <span>{experience.data.title}</span>)}
</div>
---
// src/pages/api/experiences.astro
import { getCollection } from "astro:content";
import ExperiencesList from '../../components/ExperiencesList.astro';

const category = Astro.url.searchParams.get('category') || 'workshops and food tours';
const allExperiences = await getCollection("experiences");
const filteredExperiences = allExperiences.filter(exp => exp.data.category.toLowerCase() === category.toLowerCase());

export const partial = true;
---

<ExperiencesList experiences={filteredExperiences} />

Both of these approaches allow you to fetch and display only the experiences for the selected category without full page reloads or client-side filtering of all experiences. They leverage Astro's server-side rendering capabilities while providing a smooth user experience.

#

@crude flower Kapa.ai is still learning and improving, please let me know how I did by reacting below.

crude flower
#

@solemn sand And how can I use client side rendering?

solemn sandBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

To use client-side rendering in Astro, you can leverage Astro's framework components and client directives. This approach allows you to create interactive components that render on the client side. Here's how you can implement client-side rendering for your experiences list:

  1. First, create a framework component (e.g., React, Vue, Svelte) to handle the experiences list. Let's use React as an example:
// src/components/ExperiencesListReact.jsx
import React, { useState, useEffect } from 'react';

const ExperiencesListReact = ({ initialExperiences, categories }) => {
  const [experiences, setExperiences] = useState(initialExperiences);
  const [category, setCategory] = useState(categories[0]);

  useEffect(() => {
    const fetchExperiences = async () => {
      const response = await fetch(`/api/experiences?category=${category}`);
      const data = await response.json();
      setExperiences(data);
    };
    fetchExperiences();
  }, [category]);

  return (
    <div>
      <div className="flex justify-between gap-8 text-2xl font-bold text-woodsmoke/80">
        {categories.map((cat) => (
          <button key={cat} onClick={() => setCategory(cat)}>
            {cat.toUpperCase()}
          </button>
        ))}
      </div>
      <div className="grid md:grid-cols-2 gap-8">
        {experiences.map((experience) => (
          <span key={experience.id}>{experience.data.title}</span>
        ))}
      </div>
    </div>
  );
};

export default ExperiencesListReact;
  1. Now, use this component in your Astro page with a client directive:
---
// src/pages/index.astro
import { getCollection } from "astro:content";
import ExperiencesListReact from '../components/ExperiencesListReact';
#

const allExperiences = await getCollection("experiences");
const categories = ["workshops and food tours", "aquatic", "archeology", "adventure"];
const initialExperiences = allExperiences.filter(exp => exp.data.category.toLowerCase() === categories[0].toLowerCase());

<section class="w-full max-w-[1200px]">
<ExperiencesListReact client:load initialExperiences={initialExperiences} categories={categories} />
</section>


3. Create an API route to fetch experiences by category:

```astro
---
// src/pages/api/experiences.astro
import { getCollection } from "astro:content";

const category = Astro.url.searchParams.get('category') || 'workshops and food tours';
const allExperiences = await getCollection("experiences");
const filteredExperiences = allExperiences.filter(exp => exp.data.category.toLowerCase() === category.toLowerCase());

return new Response(JSON.stringify(filteredExperiences), {
  status: 200,
  headers: {
    "Content-Type": "application/json"
  }
});
---

In this setup:

  1. The ExperiencesListReact component is rendered on the client side, thanks to the client:load directive. This directive tells Astro to send the component's JavaScript to the browser and hydrate it on page load.

  2. The initial experiences are passed from the server to provide an initial render, improving perceived performance and SEO.

  3. Subsequent category changes are handled entirely on the client side, fetching new data from the API route without a full page reload.

This approach combines the benefits of server-side rendering (initial load performance and SEO) with the interactivity of client-side rendering.

#

@crude flower Kapa.ai is still learning and improving, please let me know how I did by reacting below.

crude flower
#

@solemn sand What is astro:page-load for?

solemn sandBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The astro:page-load event is an important lifecycle event in Astro's View Transitions API. Here's what it's used for:

  1. Timing: The astro:page-load event fires at the end of page navigation, after the new page is visible to the user and blocking styles and scripts are loaded View Transitions Guide.

  2. Execution: This event is fired both on initial page navigation for a pre-rendered page and on any subsequent navigation, either forwards or backwards View Transitions Guide.

  3. Use cases: You can use this event to run code on every page navigation. For example, it's useful for setting up event listeners that would otherwise be lost during navigation View Transitions Guide.

  4. Replacement for DOMContentLoaded: When view transitions are enabled, code that would normally execute on DOMContentLoaded should be changed to execute on this event View Transitions Router API Reference.

Here's an example of how to use the astro:page-load event:

document.addEventListener('astro:page-load', () => {
  // This runs on first page load and after every navigation.
  setupStuff(); // e.g. add event listeners
});

This event is particularly useful when you're using Astro's View Transitions feature, as it ensures your JavaScript runs at the appropriate time in the navigation cycle, maintaining interactivity across page transitions.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@crude flower Kapa.ai is still learning and improving, please let me know how I did by reacting below.

crude flower
#

document.addEventListener("astro:page-load", () => {
let currentCategory = "workshops and food tours";

    const experienceContainer = document.getElementById(
        "experience-container",
    );
    const experiences = experienceContainer?.querySelectorAll("span");
    const buttons = document.querySelectorAll(
        "button[data-category]",
    ) as globalThis.NodeListOf<HTMLButtonElement>;

    // Function that filters experiences by category
    const filterExperiences = (category: string) => {
        experiences?.forEach((experience) => {
            if (experience.dataset.category === category.toLowerCase()) {
                experience.style.display = "";
            } else {
                experience.style.display = "none";
            }
        });
    };

    // Adds the event listener to the buttons
    buttons.forEach((button) => {
        button.addEventListener("click", (event: MouseEvent) => {
            const currentTarget = event.currentTarget as HTMLElement;
            const category = currentTarget.dataset.category;

            if (category) {
                currentCategory = category;
                filterExperiences(category);
            }
        });
    });

    filterExperiences(currentCategory);

Now, how can I add text-primary-500 class to the button that has the current category in its data-set?

#

@solemn sand document.addEventListener("astro:page-load", () => {
let currentCategory = "workshops and food tours";

    const experienceContainer = document.getElementById(
        "experience-container",
    );
    const experiences = experienceContainer?.querySelectorAll("span");
    const buttons = document.querySelectorAll(
        "button[data-category]",
    ) as globalThis.NodeListOf<HTMLButtonElement>;

    // Function that filters experiences by category
    const filterExperiences = (category: string) => {
        experiences?.forEach((experience) => {
            if (experience.dataset.category === category.toLowerCase()) {
                experience.style.display = "";
            } else {
                experience.style.display = "none";
            }
        });
    };

    // Adds the event listener to the buttons
    buttons.forEach((button) => {
        button.addEventListener("click", (event: MouseEvent) => {
            const currentTarget = event.currentTarget as HTMLElement;
            const category = currentTarget.dataset.category;

            if (category) {
                currentCategory = category;
                filterExperiences(category);
            }
        });
    });

    filterExperiences(currentCategory);

Now, how can I add text-primary-500 class to the button that has the current category in its data-set?

solemn sandBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

To add the text-primary-500 class to the button that has the current category in its data-set, you can modify your code to update the button styles when filtering experiences. Here's how you can achieve this:

  1. First, let's create a function to update the button styles:
const updateButtonStyles = (category: string) => {
    buttons.forEach((button) => {
        if (button.dataset.category === category.toLowerCase()) {
            button.classList.add('text-primary-500');
        } else {
            button.classList.remove('text-primary-500');
        }
    });
};
  1. Now, let's modify your existing code to use this function:
document.addEventListener("astro:page-load", () => {
    let currentCategory = "workshops and food tours";

    const experienceContainer = document.getElementById(
        "experience-container",
    );
    const experiences = experienceContainer?.querySelectorAll("span");
    const buttons = document.querySelectorAll(
        "button[data-category]",
    ) as globalThis.NodeListOf<HTMLButtonElement>;

    // Function that filters experiences by category
    const filterExperiences = (category: string) => {
        experiences?.forEach((experience) => {
            if (experience.dataset.category === category.toLowerCase()) {
                experience.style.display = "";
            } else {
                experience.style.display = "none";
            }
        });
        updateButtonStyles(category);
    };

    // Function to update button styles
    const updateButtonStyles = (category: string) => {
        buttons.forEach((button) => {
            if (button.dataset.category === category.toLowerCase()) {
                button.classList.add('text-primary-500');
            } else {
                button.classList.remove('text-primary-500');
            }
        });
    };
#

// Adds the event listener to the buttons
buttons.forEach((button) => {
button.addEventListener("click", (event: MouseEvent) => {
const currentTarget = event.currentTarget as HTMLElement;
const category = currentTarget.dataset.category;

        if (category) {
            currentCategory = category;
            filterExperiences(category);
        }
    });
});

filterExperiences(currentCategory);

});


In this updated code:

1. We've added a new function `updateButtonStyles` that adds the `text-primary-500` class to the button matching the current category and removes it from all other buttons.

2. We call `updateButtonStyles` inside the `filterExperiences` function, so the button styles are updated every time the experiences are filtered.

3. We also call `filterExperiences(currentCategory)` at the end of the event listener, which will set the initial styles for the buttons based on the default category.

This approach ensures that the `text-primary-500` class is always applied to the button corresponding to the current category, and removed from all other buttons.

Remember to make sure that the `text-primary-500` class is defined in your CSS or Tailwind configuration to see the desired styling effect.

[Astro's client-side scripts](<https://docs.astro.build/en/guides/client-side-scripts/>) allow you to add interactivity to your pages, and the `astro:page-load` event ensures that your script runs after each navigation when using Astro's View Transitions.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------