Creating multiple dynamic paths with Strapi, with nested dynamic pages - reactjs

I'm using Strapi to call dynamic data into my website via an API GET request, and I want to generate paths for my dynamic pages. One level of dynamic pages works fine, but the second is a challenge.
My structure is as follows:
[category].js
[category]/[client].js
Both are dynamic, so I have, for example, a category "fashion" with multiple clients. The same goes for other categories like "products".
The first dynamic page works fine in building paths
[dynamic.js].
import CategoryCard from "../../../components/portfolio/categoryCard";
import { fetcher } from "../../../lib/api";
export const getStaticPaths = async () => {
const categoryPathResponse = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/categories`
);
const data = categoryPathResponse.data;
const paths = data.map((path) => {
return {
params: { category: path.attributes.path.toString().toLowerCase() },
};
});
return {
paths,
fallback: false,
};
};
export async function getStaticProps(context) {
const category = context.params.category;
const categoryPropsResponse = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/categories?filters[path][$eq]=${category}&?populate[0]=clients&populate[1]=clients.thumbnail`
);
return {
props: { category: categoryPropsResponse },
};
}
const CategoryOverviewPage = ({ category }) => {
const data = category.data;
const categoryTitle = data[0].attributes.Category;
return (
<>
{console.log('data for category before card', data)}
<div className="flex px-4 mt-24 lg:mt-12 lg:px-20">
<div>
<h1 className="[writing-mode:vertical-lr] [-webkit-writing-mode: vertical-lr] [-ms-writing-mode: vertical-lr] rotate-180 text-center">
{categoryTitle}
</h1>
</div>
<div className="grid grid-cols-[repeat(auto-fit,_minmax(150px,_250px))] gap-4 lg:gap-8 ml-4 lg:ml-32 max-w-[82vw]">
<CategoryCard data={data} />
</div>
</div>
</>
);
};
export default CategoryOverviewPage;
But the complexity comes with the second part, in which I have to create multiple paths per category. I tried and ended up with the following
[clients].js
export const getStaticPaths = async () => {
const categoryPathResponse = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/categories?populate=*`
);
const data = categoryPathResponse.data;
const paths = data.map((path) => {
const category = path.attributes.path.toString().toLowerCase()
const client = path.attributes.clients.map((client) => client.name).toString().toLowerCase().replace(/\s+/g, "-")
return {
params: {
category: category, client: client
},
};
});
return {
paths,
fallback: false,
};
};
export async function getStaticProps(context) {
const category = context.params.category;
const client = context.params.client;
const data = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL_BASE}/categories?filters[path][$eq]=${category}&?populate[clients][populate]=*&populate[clients][filters][name][$eq]=${client}`
);
return {
props: { client: data },
};
}
It seems to work for categories with only 1 item, which makes sense because a URL (path) is created like index/category/client.
But when there are multiple clients, it tries to create a path with 1 category and multiple clients attached to the same path, something like this category/client1client2.
This has to be separated, and for each client, there has to be a new path created like category1/client1, category1/client2, category2/client1, category2/client2, etc.
Any ideas?

In addition to mapping over the categories data, you also need to map over the clients array and generate a path entry for each.
Modify the code inside getStaticPaths in /[category]/[client].js as follows.
export const getStaticPaths = async () => {
// Existing code...
const paths = data.map((path) => {
const category = path.attributes.path.toString().toLowerCase()
return path.attributes.clients
.map((client) => {
const clientDetails = client.name.toLowerCase().replace(/\s+/g, "-")
return {
params: {
category: category, client: clientDetails
}
};
})
}).flat() // Flatten array to avoid nested arrays;
return {
paths,
fallback: false,
};
};

Related

Get value from promise using React/Typescript [duplicate]

This question already has answers here:
Using async/await inside a React functional component
(4 answers)
Closed 7 months ago.
I was given a snippet of a class named GithubService. It has a method getProfile, returning a promise result, that apparently contains an object that I need to reach in my page component Github.
GithubService.ts
class GithubService {
getProfile(login: string): Promise<GithubProfile> {
return fetch(`https://api.github.com/users/${login}`)
.then(res => res.json())
.then(({ avatar_url, name, login }) => ({
avatar: avatar_url as string,
name: name as string,
login: login as string,
}));
}
export type GithubProfile = {
avatar: string;
name: string;
login: string;
};
export const githubSerive = new GithubService();
The page component should look something like this:
import { githubSerive } from '~/app/github/github.service';
export const Github = () => {
let name = 'Joshua';
const profile = Promise.resolve(githubSerive.getProfile(name));
return (
<div className={styles.github}>
<p>
{//something like {profile.name}}
</p>
</div>
);
};
I'm pretty sure the Promise.resolve() method is out of place, but I really can't understand how do I put a GithubProfile object from promise into the profile variable.
I've seen in many tutorials they explicitly declare promise methods and set the return for all outcomes of a promise, but I can't change the source code.
as you are using React, consider making use of the useState and useEffect hooks.
Your Code could then look like below, here's a working sandBox as well, I 'mocked' the GitHub service to return a profile after 1s.
export default function Github() {
const [profile, setProfile] = useState();
useEffect(() => {
let name = "Joshua";
const init = async () => {
const _profile = await githubService.getProfile(name);
setProfile(_profile);
};
init();
}, []);
return (
<>
{profile ? (
<div>
<p>{`Avatar: ${profile.avatar}`}</p>
<p>{`name: ${profile.name}`}</p>
<p>{`login: ${profile.login}`}</p>
</div>
) : (
<p>loading...</p>
)}
</>
);
}
You should wait for the promise to be resolved by either using async/await or .then after the Promise.resolve.
const profile = await githubSerive.getProfile(name);
const profile = githubSerive.getProfile(name).then(data => data);
A solution would be:
import { githubSerive } from '~/app/github/github.service';
export async function Github() {
let name = 'Joshua';
const profile = await githubSerive.getProfile(name);
return (
<div className={styles.github}>
<p>
{profile.name}
</p>
</div>
);
}
But if you are using react, things would be a little different (since you have tagged reactjs in the question):
import { githubSerive } from '~/app/github/github.service';
import * as React from "react";
export const Github = () => {
let name = 'Joshua';
const [profile, setProfile] = React.useState();
React.useEffect(() => {
(async () => {
const profileData = await githubSerive.getProfile(name);
setProfile(profileData);
})();
}, [])
return (
<div className={styles.github}>
<p>
{profile?.name}
</p>
</div>
);
}

React Apollo Client: Pagination and fetch data on button click

I am new to Apollo Client and want to implement pagination. My code looks like this:
I am using RickandMorty endpoint for this (https://rickandmortyapi.com/graphql)
useCharacters.tsx
import { useQuery, gql } from '#apollo/client';
const GET_ALL_CHARACTERS = gql`
query GetCharacters($page: Int) {
characters(page: $page) {
info {
count
pages
}
results {
id
name
}
}
}
`;
export const useCharacters = (page: number = 1) => {
const { data, loading, error } = useQuery(GET_ALL_CHARACTERS, { variables: { page } });
return { data, loading, error };
};
App.tsx
export const App = () => {
const { data, loading, error } = useCharacters(1);
const nextPage = () => {
const { data, loading, error } = useCharacters(2);
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </div>
</>
);
};
It is fetching data properly the first time but I want to fetch new data when Next button is clicked on page 2.
I know I can't call useQuery() in a method like this as hooks cannot be called inside a block and also the data, error, and loading won't be accessible outside.
How can I fix this issue? I tried googling it but could not find any help related to this.
This might help other developers who are new to Apollo Client and will save them time.
fetchMore() can be used for pagination with Apollo Client.
useCharacters.tsx
export const useCharacters = (page: number = 1, name: string = '') => {
const { data, loading, error, fetchMore } = useQuery(GET_ALL_CHARACTERS, {
variables: { page, name },
notifyOnNetworkStatusChange: true, // to show loader
});
return { data, loading, error, fetchMore }; // returning fetchMore
};
App.tsx
export const App = () => {
const { data, loading, error, fetchMore } = useCharacters(1);
const nextPage = () => {
/* You can call the returned fetchMore() here and pass the next page number.
updateQuery() simply updates your data to the newly fetched records otherwise return previous records
*/
fetchMore({
variables: {
page: 2,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return fetchMoreResult;
},
});
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </div>
</>
);
};

How to make a folder static in react

I'm trying to get a picture from my folder with a path refer from my json and display it in FeaturedRooms.jsx.
...
src
components
FeaturedRooms.jsx
images
pages
Home.jsx
context.jsx
...
...
"images":[
{
"url":"../images/room-1.jpeg"
},
...
I'm using the context.jsx to handle and store the data.
const fetchUrl = async () => {
const resp = await fetch(url);
const respData = await resp.json();
const rooms = respData.rooms;
featured(rooms);
dispatch({ type: "ROOMS", payload: rooms });
};
useEffect(() => {
fetchUrl();
}, []);
const featured = (rooms) => {
let featuredRooms = rooms.filter((room) => room.room.featured === true);
dispatch({ type: "FEATURED_ROOMS", payload: featuredRooms });
};
And I'm trying to display it on the FeaturedRooms.jsx.
<div className="featured-wrapper">
{featuredRooms.map((rooms) => {
const { system, room } = rooms;
let images = room.images.map((image) => {
return image.url;
});
return (
<div className="room" key={system.id}>
<h2>{room.name}</h2>
<img src={images[0]} alt="featured-rooms" />
</div>
);
})}
</div>
But it's not working at all, is there a way for me to access it? And also, is there a way for me to make the images folder a static folder? Perhaps I will be accessing the images folder from a different folder.
edit: some more details.
Your folder structure is correct. What you need to do in your array is
...
"images":[
{
"url":require("../images/room-1.jpeg")
},
...
Then whenever you want to use this images array
images.map((item) =>(
<img src={item.url} />
))

Rendering multiple EditorJS components

I'm using EditorJS on a React page to allow people to write in a block-based editor. However, I also want to build sections, where a user can have multiple sections and each section can support an EditorJS component
I'm running into an issue when I add a new section, and want to render an empty EditorJS component for this new section (and keep the data from the old section and EditorJS instance). Instead of an empty instance, it copies over the information from the old instance and assigns it to the new Section. Type definitions are below
types.d.ts
interface User {
...
sections: Section[],
}
interface Section {
id: string,
name: string,
content: ContentBlock,
}
interface ContentBlock {
id: string,
threads: Thread[],
content: OutputData, //this is the EditorJS saved data
}
I'm wondering if EditorJS is keeping some sort of global state that it's applying to every instance of itself in my application. Does anyone have experience with spinning up multiple editorJS instances?
For reference, I have two components: Page.tsx and Section.tsx. Relevant code is below
//Page.tsx
const Page: React.FC = () => {
const [allSections, setAllSections] = useState<Section[]>([]);
const [currSectionID, setCurrSectionID] = useState("");
const addNewSection = (type: string) => {
const newID = uuidv4();
const newSection: Section = {
id: newID,
name: "",
content: emptyContentBlock,
};
setAllSections(arr => [...arr, newSection]);
setCurrSectionID(newID);
};
const updateContentForSection = (contentBlock: ContentBlock, sectionID: string) => {
const newSectionArray = [...allSections];
newSectionArray.forEach((section: Section) => {
if (section.id === sectionID) {
section.content = contentBlock
}
});
setAllSections(newSectionArray);
};
return (
<Section
sectionID={currSectionID}
sections={allSections}
pageActions = {{
addNewSection: addNewSection,
updateContentForSection: updateContentForSection,
}}
/>
)
}
//Section.tsx
const Section: React.FC<SectionInput> = (props) => {
const currSection = props.sections.filter(section => section.id === props.sectionID)[0];
const blocks = currSection? currSection.content.content : [];
const [editorInstance, setEditorInstance] = useState<EditorJS>();
const saveEditorData = async() => {
if (editorInstance) {
const savedData = await editorInstance.save();
console.log(`saving data to section ${props.sectionID}`, savedData);
props.pageActions.updateContentForSection({content: savedData, id: props.sectionID, threads: threads}, props.sectionID);
}
}
}
return (
<div>
<button
className={`absolute top-0 right-12 mt-2 focus:outline-none`}
onClick={() => {
props.pageActions.addNewSection()
}}
>
Add Section
</button>
<EditorJs
key="0"
holder="custom"
data={blocks}
autofocus={true}
instanceRef={(instance: EditorJS) => {
setEditorInstance(instance)
}}
onChange={saveEditorData}
tools={EDITOR_JS_TOOLS}
>
<div
id="custom"
>
</div>
</EditorJs>
</div>
)
So according to this github thread, the answer is actually straightforward. Use a unique ID for each editorJS ID for each editor you want to have in the DOM. The code, then, becomes something like this
<EditorJs
key={`${sectionID}`}
holder={`custom${sectionID}`}
data={blocks}
autofocus={true}
instanceRef={(instance: EditorJS) => {
setEditorInstance(instance)
}}
onChange={saveEditorData}
tools={EDITOR_JS_TOOLS}
>
<div
id={`custom${sectionID}`}
>
</div>
</EditorJs>

Multiple fetch requests to retrieve image urls

I am trying to fetch images by their ids. The architecture of backend is as follows: DB stores images in binary and there is another table that stores images ids.
I am using apollo client on front end to prefetch images ids and then send another set of fetch requests.
Unfortunately I get Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. Could anyone help me to
1) figure out why it happens. I see that there is bunch of pending promises in the stack.
and 2) how it can be refactored to better architecture.
import React, {useState} from 'react'
import {useQuery} from "#apollo/react-hooks";
import {gql} from 'apollo-boost';
const apiEndpoint = 'http://localhost:9211';
const getProductImage = function (id) {
return gql`
{
productById(id: "${id}") {
images {
imageId
}
}
}`
};
const fetchImage = (imageUrl, allImgsArr) => {
return fetch(imageUrl)
.then(res => res.blob())
.then(img => allImgsArr.push(URL.createObjectURL(img)))
};
const ItemPage = (props) => {
const [id] = useState(props.match.params.id);
const {data} = useQuery(getProductImage(id));
let imagesIds = [];
if (data) {
data.productById.images.forEach(image => {
imagesIds.push(image.imageId)
});
}
const [imagesUrls, setImagesUrl] = useState([]);
// MULTIPE FETCH RETRIEVALS START
for (let imId of imagesIds) {
setImagesUrl(imagesUrls => [...imagesUrls, fetchImage(`${apiEndpoint}/image/${imId}`, imagesUrls)]);
}
// MULTIPE FETCH RETRIEVALS END
return (
<>
<div>
<div>
<img src={imagesUrls[0] ? imagesUrls[0] : ''} alt="main item 1 photo"/>
</div>
<div>
<div>
<img src={imagesUrls[1] ? imagesUrls[1] : ''} alt="Additional item 1 photo"/>
</div>
</div>
</div>
</>
)
};
export default ItemPage;
your query should be a constant , not function.
const GET_PRODUCT_IMAGE = gql`
query getProduct($id:String!) {
productById(id: $id) {
images {
imageId
}
}
}
}`
// pass variables like this
const {data} = useQuery(GET_PRODUCT_IMAGE, { variables: { id },
});
More Info : https://www.apollographql.com/docs/react/data/queries/

Resources