konstantinmuenster / gatsby-theme-portfolio-minimal

A Gatsby Theme to create modern one-page portfolios with a clean yet expressive design.
MIT License
161 stars 90 forks source link

Cannot read properties of undefined (reading 'articles') #13

Closed rolas978 closed 2 years ago

rolas978 commented 2 years ago

Hi there! I've been using this theme for some time now and love it. One issue I'm running into while tweaking it comes when I attempt to create a new section using ArticlesListingTemplate. I have this in my index.js:

import React from 'react';
import {
    HeroSection,
    Page,
    Seo,
} from 'gatsby-theme-portfolio-minimal';
import ArticleListingTemplate from 'gatsby-theme-portfolio-minimal/src/templates/ArticleListing';

export default function IndexPage() {
    return (
        <>
    <Page>
            <Seo title="Gatsby Theme Portfolio Minimal" />
                <HeroSection sectionId="hero" />
                <ArticleListingTemplate heading="Explore Collections" />
            </Page>
        </>
    );
}

but end up with this error:

Cannot read properties of undefined (reading 'articles')

  23 | export default function ArticleListingTemplate(props: ArticleListingTemplateProps): React.ReactElement {
  24 |     const ARTICLES_PER_PAGE = 9;
> 25 |     const articles = props.pageContext.articles;
     |                                       ^
  26 |     const [filterOptions, setFilterOptions] = React.useState<FilterOption[]>(extractFilterOptions(articles));
  27 |     const [shownArticlesNumber, setShownArticlesNumber] = React.useState<number>(ARTICLES_PER_PAGE);
  28 |

articles is defined earlier in the file through an interface, so not sure why I'm getting this error. What might be going wrong here? Appreciate any help!

rolas978 commented 2 years ago

Here's the code for the full ArticleListing file:


import React from 'react';
import { Page } from '../../components/Page';
import { Section } from '../../components/Section';
import { Seo } from '../../components/Seo';
import { Slider } from '../../components/Slider';
import { ArticleCard } from '../../components/ArticleCard';
import { Button, ButtonType } from '../../components/Button';
import ArticleTemplateData from '../Article/data';
import * as classes from './style.module.css';

interface ArticleListingTemplateProps {
    pageContext: {
        articles: ArticleTemplateData[];
    };
}

interface FilterOption {
    label: string;
    selected: boolean;
    relatedArticleIds: string[];
}

export default function ArticleListingTemplate(props: ArticleListingTemplateProps): React.ReactElement {
    const ARTICLES_PER_PAGE = 9;
    const articles = props.pageContext.articles;
    const [filterOptions, setFilterOptions] = React.useState<FilterOption[]>(extractFilterOptions(articles));
    const [shownArticlesNumber, setShownArticlesNumber] = React.useState<number>(ARTICLES_PER_PAGE);

    function handleFilterOptionClick(optionLabel: string): void {
        const updatedFilterOptions = [...filterOptions];
        const selectedOptionIndex = updatedFilterOptions.map((o) => o.label).indexOf(optionLabel);
        updatedFilterOptions[selectedOptionIndex].selected = !updatedFilterOptions[selectedOptionIndex].selected;
        setFilterOptions(updatedFilterOptions);
    }

    function handleLoadMoreButtonClick(articlesNumber: number, selectedArticlesNumber?: number): void {
        const incrementedArticleNumber = shownArticlesNumber + 3;
        if (selectedArticlesNumber && selectedArticlesNumber >= incrementedArticleNumber) {
            setShownArticlesNumber(incrementedArticleNumber);
        } else if (!selectedArticlesNumber && articlesNumber >= incrementedArticleNumber) {
            setShownArticlesNumber(incrementedArticleNumber);
        }
    }

    // Check if at least one filter option is selected. If so, create an array of all article ids that
    // are selected based on the current filter option selection. We use this later on to easily check
    // which articles to show.
    let selectedArticleIds: string[] = [];
    const filterSelected = filterOptions.map((o) => o.selected).indexOf(true) !== -1;
    if (filterSelected) {
        selectedArticleIds = filterOptions
            .filter((option) => option.selected) // Filter only for selected options
            .map((option) => option.relatedArticleIds) // Create an array of article ids arrays
            .flat(1) // Flatten the array to a string[]
            .filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicate article ids
    }

    return (
        <>
            <Seo title="NFT Collection" useTitleTemplate={true} />
            <Page>
                <Section anchor="articleListing" heading="Explore Collections">
                    <div className={classes.Filter}>
                        Sort by type
                        <Slider additionalClasses={[classes.Options]}>
                            {filterOptions.map((option, key) => {
                                return (
                                    <div
                                        key={key}
                                        role="button"
                                        onClick={() => handleFilterOptionClick(option.label)}
                                        className={[
                                            classes.Option,
                                            option.selected === true ? classes.Selected : null,
                                        ].join(' ')}
                                    >
                                        {option.label} ({option.relatedArticleIds.length})
                                    </div>
                                );
                            })}
                        </Slider>
                    </div>  
                                  <div className={classes.Listing}>
                        {articles
                            .filter((article) => !filterSelected || selectedArticleIds.includes(article.id))
                            .slice(0, shownArticlesNumber)
                            .map((article, key) => {
                                return (
                                    <ArticleCard
                                        key={key}
                                        showBanner={true}
                                        data={{
                                            title: article.title,
                                            image: article.banner,
                                            link: article.slug,
                                        }}
                                    />
                                );
                            })}
                    </div>
                </Section>
            </Page>
        </>
    );
}

// Helper function to calculate a sorted array of filter options based on the given articles
// We use the helper function before we initialize the state so that it can happen on the server.
function extractFilterOptions(articles: ArticleTemplateData[]): FilterOption[] {
    const filterOptions: FilterOption[] = [];
    const categoryList: string[] = [];
    articles.forEach((article) => {
        article.categories.forEach((category) => {
            if (!categoryList.includes(category)) {
                filterOptions.push({ label: category, selected: false, relatedArticleIds: [article.id] });
                categoryList.push(category);
            } else {
                const optionIndex = filterOptions.map((o) => o.label).indexOf(category);
                filterOptions[optionIndex].relatedArticleIds.push(article.id);
            }
        });
    });
    return filterOptions.sort((a, b) => (a.relatedArticleIds.length > b.relatedArticleIds.length ? -1 : 1));
}
konstantinmuenster commented 2 years ago

Hi @rolas978! Usually, the ArticleListingTemplate is used to automatically generate the blog listing page. You find the code here.

If you want to reuse the template for a custom section, you need to pass in the required props which you find here. So basically, your index.js should look like:

export default function IndexPage() {
   const pageContext = { articles: YourArrayOfArticles };
   return (
       <>
   <Page>
           <Seo title="Gatsby Theme Portfolio Minimal" />
               <HeroSection sectionId="hero" />
            <ArticleListingTemplate heading="Explore Collections" pageContext={pageContext} />
           </Page>
       </>
   );
}

But your YourArrayOfArticles needs to have the correct shape, otherwise it won't work. Perhaps, you can also look into the latest version I released since it contains the Section and Animation component. This allows you building better custom sections.

Let me know if this helps you 😄