I'm getting an error while fetching blogs from sanity to my template, I'm creating a next.js website
Error: Error: Unknown block type "undefined", please specify a
serializer for it in the serializers.types prop
<PortableText
// Pass in block content straight from Sanity.io
content={blogs[0].content}
projectId="oeqragbg"
dataset="production"
// Optionally override marks, decorators, blocks, etc. in a flat
// structure without doing any gymnastics
serializers = {{
h1: (props) => <h1 style={{ color: "red" }} {...props} />,
li: ({ children }) => <li className="special-list-item">{children}</li>,
}}
/>
export async function getServerSideProps(context) {
const client = createClient({
projectId: 'oeqragbg',
dataset: 'production',
useCdn: false
});
const query = '*[_type == "blog"]';
const blogs = await client.fetch(query);
return {
props: {
blogs
}
}
}
Are you using the #portabletext/react package?
This is how you would use it:
import {PortableText} from '#portabletext/react'
const myPortableTextComponents = {
block: {
h1: ({children}) => <h1 style={{ color: "red" }}">{children}</h1>
},
listItem: {
bullet: ({children}) => <li className="special-list-item">{children}</li>,
},
}
<PortableText
value={blogs[0].content}
components={myPortableTextComponents}
/>
Related
I am a beginner who wants to build a blog using Notion API, Next.js and Tailwind CSS. I learned the following code from here: https://egghead.io/lessons/next-js-request-notion-database-data-from-the-api-with-next-js.
The following code works fine in /post/index.js, but I get this error when I put the following code into /components/PostTest.js and import it in /index.js.
How do I solve this problem?
Error information
error screenshot
Server Error
TypeError: Cannot read properties of undefined (reading 'map')
#line 9
return posts.map((posts) => (
Source Code
import Head from "next/head";
import Link from "next/link";
import { Client } from "#notionhq/client";
import { useState } from "react";
export const PostPage = ({ posts }) => {
const [post] = useState(null);
return posts.map((posts) => (
<div className="bg-[#F5F5F7] dark:bg-black px-4 py-2 md:py-4">
<div className="bg-[#FFFFFF] dark:bg-[#141414] max-w-sm rounded-xl overflow-hidden shadow-sm container mx-auto">
<img
className="aspect-[16/9] bg-cover bg-center"
src={posts.coverImage}
alt="Post Banner"
/>
<div className="px-6 py-4">
<p className="text-[12px] md:text-[14px] dark:text-[#888888] leading-5 font-[700] pt-2 uppercase tracking-normal mb-[8px]">
{posts.Category}
</p>
<Link href={`/post/${posts.PID}`}>
<div className="text-lg md:text-xl text-[#1d1d1f] dark:text-[#F5F5F7] leading-snug font-[700]">
{posts.Title}
</div>
</Link>
<p className="text-[14px] text-[#6e6e73] dark:text-[#888888] leading-5 font-[600] pt-2">
{new Date(posts.Date).toLocaleDateString()}
</p>
</div>
</div>
</div>
));
};
export const getStaticProps = async () => {
const notion = new Client({
auth: process.env.NOTION_TOKEN,
});
// get posts more than 100 pages.
let results = [];
let data = await notion.databases.query({
database_id: process.env.NOTION_POST_DATABASE_ID,
filter: {
property: "Status",
select: {
equals: "Published",
},
},
sorts: [
{
property: "Date",
direction: "descending",
},
],
});
results = [...data.results];
while (data.has_more) {
data = await notion.databases.query({
database_id: process.env.NOTION_POST_DATABASE_ID,
filter: {
property: "Status",
select: {
equals: "Published",
},
},
start_cursor: data.next_cursor,
});
results = [...results, ...data.results];
}
const posts = results.map((post) => ({
id: post.id,
Title: post.properties.Title.title[0].text.content,
Category: post.properties.Category.select.name,
category_color: post.properties.Category.select.color,
Date: post.properties.Date.date.start,
Tags: post.properties.Tags.multi_select.map((Tags) => Tags.name),
Tags_color: post.properties.Tags.multi_select.map((TagsColor) => TagsColor.color),
PID: post.properties.PID.rich_text[0].text.content,
Author: post.properties.Author.people.map((people) => people.name),
Author_avatar_url: post.properties.Author.people.map((people) => people.avatar_url),
coverImage:
post.cover.file?.url ||
post.cover.external?.url,
}));
return {
props: {
posts,
},
revalidate: 1,
};
};
export default PostPage;
First of all, post should be inited with an empty array:
const [post, ] = useState([]);
Secondly, you cannot return an array of JSX, so wrap it in a Fragment or <>.
return (
<>
posts.map((posts) => (
...
)
</>
)
if posts is null or it's not an array, you'll get that error.
Try this fix
return (
posts?.map((posts) => (
...
)
)
I don't speak English very well. Please be understanding!
First, please check my code!
export default function DriveFolder() {
const [clickFolderPk, setClickFolderPk] = useState(1);
const viewFolder = async () => {
const url = `/api/store/drive/view-folder?folderId=${clickFolderPk}`;
await get(url)
.then((res) => {
console.log(res);
setMainFolder(res.directChildrenFolders);
})
.catch((error) => {
console.log(error);
});
};
useEffect(() => {
viewFolder();
}, [clickFolderPk]);
return (
<div className={classes.driveFolder}>
{mainFolder.map((main, key) => (
<TreeView>
<TreeItem
onClick={() => setClickFolderPk(main.FOLDER_PK)}>
<TreeItem nodeId='10' label='OSS' />
<TreeItem nodeId='6' label='Material-UI'>
<TreeItem nodeId='7' label='src'>
<TreeItem nodeId='8' label='index.js' />
<TreeItem nodeId='9' label='tree-view.js' />
</TreeItem>
</TreeItem>
</TreeItem>
</TreeView>
))}
</div>
);
}
I edited some code to make it clear. (might misspelled)
With this code, on the first rendering, since 'clickFolderPk' value is 1, I get the right data from DB.
However, since I have subfolders within folders from 'clickFolderPk' value 1, I have to request another GET REQUEST to see my subfolders from root folders.
Here is the simple image that you can understand my situation better.
this is what I get from 'clickFolderPk' value 1.
However, when I press 'kikiki', GET request functions and render like this.
.
This is not the way I want to render things.
I want every data from DB, however they don't disappear whenever I use different GET request with different PK number.
I want them stay on the screen and get the subfolders within them.
I'm struggling with this issue for quite a time.
Your help will be really appreciated!!!!!
It's all about Nesting: Folders have sub-folders, etc and it goes on...
Note: To break things down, I will answer from a React point of view disregarding how your backend api is structured or returns data.
Basically there are two main approaches,
Approach #1:
The global state is a single source of truth for all the folders think of it like this:
const [allFolders, setAllFolders] = useState([
{
id: "1",
name: "a-1",
folders: [
{
name: "a-subfolder-1",
folders: [{ name: "a-subfolder-subfolder-1" }],
},
{ name: "subfolder-2" },
],
},
]);
The problem is that any small update requires to mutate the entire state. So I will focus more on Approach #2
Approach #2:
There is the main tree that has child components, child components can expand and have children too:
import { useEffect, useState } from "react";
import "./styles.css";
export default function DriveFolder() {
const [folders, setFolders] = useState([
{ id: "1", name: "folder-a" },
{ id: "2", name: "folder-b" },
{ id: "3", name: "folder-c" }
]);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
{folders.map((folder) => {
return <Folder key={folder.id} folder={folder} />;
})}
</div>
);
}
const Folder = ({ parent = undefined, folder }) => {
const [subfolders, setSubfolders] = useState([]);
const [isOpened, setOpened] = useState(false);
const hasSubfolders = subfolders.length > 0;
useEffect(() => {
// send request to your backend to fetch sub-folders
// --------------- to ease stuff I will hard code it
// with this you can limit the example of nest you wish
const maxNestsCount = 5;
const subfolderParent = parent || folder;
const subfolder = {
id: subfolderParent.id + "-sub",
name: "subfolder-of-" + subfolderParent.name
};
const currentNestCount = subfolder.name.split("-sub").length;
setSubfolders(currentNestCount < maxNestsCount ? [subfolder] : []);
// -----------------------------
}, []);
const handleToggleShowSubFolders = (e) => {
e.stopPropagation();
if (!hasSubfolders) {
return;
}
setOpened(!isOpened);
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
paddingHorizontal: 5,
marginTop: 10,
marginLeft: parent ? 20 : 0,
backgroundColor: "#1678F230",
cursor: hasSubfolders ? "pointer" : undefined
}}
onClick={handleToggleShowSubFolders}
>
{folder.name}
<div style={{ display: isOpened ? "block" : "none" }}>
{subfolders.map((subfolder) => (
<Folder key={subfolder.id} parent={folder} folder={subfolder} />
))}
</div>
</div>
);
};
Try it out:
Here is the output of the sample code above:
I wish to place a list of posts on my home page instead of having to create a seperate dynamic page. This is my gatsby-node.js file
// DYNAMICALLY CREATE PAGES FOR EACH POST
module.exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions;
const postTemplate = path.resolve('src/templates/news.js');
const postResult = await graphql(`
query {
allContentfulPost {
edges {
node {
slug
}
}
}
}
`);
// Handle errors
if (postResult.errors) {
reporter.panicOnBuild('Error while running GraphQL query.');
return;
}
// Create the pages for each markdown file
postResult.data.allContentfulPost.edges.forEach(({ node }) => {
createPage({
component: postTemplate,
path: `/news/${node.slug}`,
context: {
slug: node.slug,
},
});
});
// PAGINATION FOR BLOG POSTS
const postsResult = await graphql(`
{
allContentfulPost(sort: { fields: date, order: DESC }, limit: 1000) {
edges {
node {
slug
}
}
}
}
`);
if (postsResult.errors) {
reporter.panicOnBuild('Error while running GraphQL query.');
return;
}
// Create blog-list pages
const posts = postsResult.data.allContentfulPost.edges;
const postsPerPage = 12;
const postNumPages = Math.ceil(posts.length / postsPerPage);
Array.from({ length: postNumPages }).forEach((_, i) => {
createPage({
path: i === 0 ? '/' : `/news/${i + 1}`,
component: path.resolve('./src/templates/news-list.js'),
context: {
limit: postsPerPage,
skip: i * postsPerPage,
postNumPages,
currentPage: i + 1,
},
});
});
};
And this is my news-list.js file
import React from 'react';
import { Link, graphql } from 'gatsby';
import Layout from '../components/layout';
import SEO from '../components/seo';
export const query = graphql`
query ($skip: Int!, $limit: Int!) {
allContentfulPost(sort: { fields: date, order: DESC }, limit: $limit, skip: $skip) {
edges {
node {
title
slug
date(formatString: "MMMM Do, YYYY")
}
}
}
}
`;
const NewList = (props) => {
// const { postNumPages } = props.pageContext;
const posts = props.data.allContentfulPost.edges;
return (
<Layout>
<SEO title='News' />
{posts.map(({ node }) => {
const title = node.title || node.slug;
return (
<div className='container mx-auto prose prose-lg'>
<div className='mb-2'>
<Link to={`/posts/${node.slug}`}>
<h3 className='underline font-sans mb-1'>{title}</h3>
</Link>
<div className='flex items-center justify-between'>
<span className='font-mono text-sm'>{node.date}</span>
</div>
</div>
</div>
);
})}
</Layout>
);
};
export default NewList;
I have tried to import the above news-list.js as component from my templates folder into my index.js folder. However I am getting the Error:
TypeError: Cannot read property 'allContentfulPost' of undefined
But if i add path: i === 0 ? '/news' : /news/${i + 1}, into my node file and go to localhost/news i get the list of posts.
But I want them on the home page.. So i thought If I was to just have / it would work turns out no.
How can i get the posts that are listed at LH/news to be displayed on my homepage instead.
Update
New Component after latest answer
import React from 'react';
import { useStaticQuery, graphql, Link } from 'gatsby';
import Layout from '../components/layout';
// import News from '../components/news';
// import NewsList from '../templates/news-list';
export const query = graphql`
{
allContentfulPost(sort: { fields: date, order: DESC }, limit: 1000) {
edges {
node {
title
slug
date(formatString: "MMMM Do, YYYY")
}
}
}
}
`;
const Index = ({ data }) => {
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
companyname
}
}
}
`
);
return (
<Layout>
<section className='c-mt-10'>
<div className=''>
<div className='font-mono md:flex md:justify-between'>
<div className='mb-5'>
<a href={`mailto:hello#${site.siteMetadata.companyname}.co.uk`}>
hello#pfb{site.siteMetadata.companyname}.co.uk
</a>
<br />
<br />
<tel>+44 020 3925 6054</tel>
</div>
<a
href='https://www.google.com/maps/place/Warnford+Court,+29+Throgmorton+St,+London+EC2N+2AT/#51.5154096,-0.0890419,17z/data=!3m1!4b1!4m5!3m4!1s0x48761cacb440b98d:0x9742679143333ff!8m2!3d51.5154096!4d-0.0868479'
target='_blank'
rel='noreferrer'>
<address className='text-right'>
Warnford Court
<br />
29 Throgmorton Street
<br /> London, EC2N 2AT
</address>
</a>
</div>
</div>
<div>
<h2>Company News</h2>
<ul>
{data.allContentfulPost.edges.map(({ node }) => (
<li key={node.title}>
<Link to={node.slug}>{node.title}</Link>
</li>
))}
</ul>
</div>
</section>
</Layout>
);
};
export default Index;
I think you are mixing a lot of concepts.
One thing is the gatsby-node.js queries, useful to create dynamic pages based on dynamic data (from Contentful CMS in your case) based on a parameter (slug in your case).
Another thing is page queries, a way of retrieving data in a top-level components (pages or templates, not components).
If you want to list all your post in your homepage, you just need to create a GraphQL query and loop through the results just like:
const IndexPage = ({ data }) => {
return <Layout>
<ul>
{data.allContentfulPost.edges.map(({node})=> <li key={node.title}><Link to={node.slug}>{title}</Link></li>)}
</ul>
</Layout>
}
export const query = graphql`
{
allContentfulPost(sort: { fields: date, order: DESC }, limit: 1000) {
edges {
node {
title
slug
date(formatString: "MMMM Do, YYYY")
}
}
}
}
`;
When using page queries, your data is stored inside props.data so you can destructure them directly into data.
In your case, you were importing a template inside a page, which doesn't make much sense because you don't have, among other things, the query.
If I make this call but the pokemon I've entered doesn't have a second type I get this error message:
Is it possible to make an if statement within the useState hook that I've named setPokemon?
If so, how can I do that or how can I get through this?
import Axios from "axios";
import React, { useState } from "react";
import "./SearchPokemon.css";
function PK() {
const api = Axios.create({
baseURL: "https://pokeapi.co/api/v2/",
});
const [pokemon, setPokemon] = useState({});
const [pokemonDescription, fetchDescription] = useState({});
const [evolution, pokemonEvolution] = useState({});
const searchPokemon = () => {
api.get(`pokemon/charmander`).then((response) => {
setPokemon({
name: response.data.name,
height: response.data.height,
weight: response.data.weight,
img: response.data.sprites.front_default,
id: response.data.id,
type: response.data.types[0].type.name,
type2: response.data.types[1].type.name,
});
api.get(`pokemon-species/${response.data.id}/`).then((response) => {
fetchDescription({
entry: response.data.flavor_text_entries[0].flavor_text,
evolution: response.data.evolution_chain.url,
});
api.get(`${response.data.evolution_chain.url}`).then((response) => {
pokemonEvolution({
evolution: response.data.chain.evolves_to[0].species.name,
});
});
});
});
};
return (
<div>
<div className="main">
<h1 style={{ textTransform: "capitalize" }}>{pokemon.name}</h1>
<h1>No. {pokemon.id}</h1>
<img src={pokemon.img} alt="" />
</div>
<div className="info">
<h3 style={{ textTransform: "capitalize" }}>
Type: {pokemon.type} {pokemon.type2}
</h3>
<h3>Height: {pokemon.height * 10} Cm</h3>
<h3>Weight: {pokemon.weight / 10} Kg</h3>
</div>
<div className="desc">
<div className="desc-info">
<h3 style={{ textTransform: "capitalize" }}>
{pokemonDescription.entry}
</h3>
</div>
</div>
<h1 style={{ textTransform: "capitalize" }}>
Evolution: {evolution.evolution}
</h1>
<button onClick={searchPokemon}>Click me</button>
</div>
);
}
export default PK;
If we first look at your error, the index 1 of your types array from your api response data is not defined. Therefore, when you try to access, it throws.
When you are not certain of the response of your api, you can use a combination of optional chaining and setting default values for that property.
This way, your code won’t break.
In your example, I believe you could do something like:
const response = {
data: {
types: []
}
};
console.log(response.data.types[1]?.type.name ?? "pick a value or leave an empty string");
// type2: response.data.types[1]?.type.name ?? ""
Notice the question mark I’ve added right after the index 1 of your expected types array. This symbol allows for optional chaining.
Then we use Nullish coalescing operator (??).
I am kind of new to react & TypeScript.
I am trying to modify some functionality but having hard time doing that.
Currently we show all the subscriptions and all the transactions on the same page instead what I want to do is that based on the subscription selection i want show all the transactions (Currently both subscriptions and transactions are siblings).
I have created an on click event on subscription which is calling handleViewTransaction
The problem is that I am not able to re-render TransactionsTable from the handleViewTransaction.
Do I need to make it parent-child or there is a way to achieve it without much code refactoring?
import * as React from 'react'
import {RouteComponentProps} from 'react-router-dom'
import * as _ from 'lodash'
import * as cx from 'classnames'
import {Accordion, AccordionItem} from 'react-light-accordion'
import * as dcbApi from '../../../components/dcbApi'
import {Page} from '../../components/page'
import {Loader} from '../../components/loader'
import {Modal} from '../../components/modal'
import {SubscriptionStore, SubscriptionStoreState, useSubscriptionStore} from '../../../stores/subscription'
import {TransactionsTable} from '../../components/tables/transactions'
import {SubscriptionsTable} from '../../components/tables/subscriptions'
import {TransactionsPage} from './transactions'
import {BokuSubscription, BokuTransaction} from '../../../types'
export interface ResultPageProps extends RouteComponentProps<{}> {
userId?: string
subscriptions?: BokuSubscription[]
transactions?: BokuTransaction[]
isLoading?: boolean
onRefetch?: () => any
}
export const ResultPage = ({ isLoading: isLoadingParent, history, userId, subscriptions, transactions, onRefetch }: ResultPageProps) => {
const [isLoading, setIsLoading] = React.useState(false);
const [subscriptionCancellationSent, setSubscriptionCancellationSent] = React.useState(false);
const [transactionCancellationSent, setTransactionCancellationSent] = React.useState(false);
const [selectedTransaction, setSelectedTransaction] = React.useState(null);
const hasTransactions = false;
const navigateHome = () =>
history.push('/');
const handleViewTransaction = async (daznUserId: string, subscriptionId: string, aggregator: string) => {
// TODO: Show subscription specific transactions
};
const handleSubsctiptionCancel = async (userId: string, subscriptionId: string) => {
try {
setIsLoading(true);
const response = await dcbApi.cancelSubscription(userId, 'boku', subscriptionId);
setIsLoading(false);
setSubscriptionCancellationSent(true);
Modal.info({
title: `Your cancellation request has been submited for ${userId}`,
okText: 'OK',
content: (
<div>
<span>Transaction Id:</span> {response.transactionId}<br/>
<span>Subscription Id:</span> {response.subscriptionId}<br/>
<span>Aggregator:</span> {response.aggregator}<br/>
<span>Status:</span> {response.status}<br/>
</div>
),
})
//this.setState({ cancellationSent: true })
} catch (err) {
setIsLoading(false);
console.error(err);
Modal.info({
title: `Your cancellation request for ${userId} has failed`,
okText: 'OK',
content: (
<div>
{_.get(err, 'message') && <div>Error message: {err.message}</div>}
<Accordion atomic={true}>
<AccordionItem title='Toggle detailed error'>
<pre>{JSON.stringify(err, null, 2)}</pre>
</AccordionItem>
</Accordion>
</div>
),
})
} finally {
if (typeof onRefetch === 'function') {
onRefetch()
}
}
};
const handleChargeRefund = async (userId: string, chargeId: string, reasonCode: number) => {
try {
setIsLoading(true);
const response = await dcbApi.refund(userId, 'boku', chargeId, reasonCode)
setIsLoading(false);
setTransactionCancellationSent(true);
Modal.info({
title: `Your refund request has been submited`,
okText: 'OK',
content: (
<div>
<span>Charge Id:</span> {response.chargeId}<br/>
<span>Country:</span> {response.country}<br/>
<span>Refund status:</span> {response.refundStatus}<br/>
<span>Refund Id:</span> {response.refundId}<br/>
</div>
),
})
//this.setState({ cancellationSent: true })
} catch (err) {
setIsLoading(false);
console.error(err);
Modal.info({
title: `Your refund request has failed`,
okText: 'OK',
content: (
<div>
{_.get(err, 'message') && <div>Error message: {err.message}</div>}
<Accordion atomic={true}>
<AccordionItem title='Toggle detailed error'>
<pre>{JSON.stringify(err, null, 2)}</pre>
</AccordionItem>
</Accordion>
</div>
),
})
} finally {
if (typeof onRefetch === 'function') {
onRefetch()
}
}
};
return (
<Page
theme='dark' // light|dark
title='Back to home'
onLogoClick={navigateHome}
onTitleClick={navigateHome}
overlay={(!isLoading && !isLoadingParent) ? undefined :
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(130,131,134,.34)'
}}>
<div style={{
textAlign: 'center',
position: 'relative',
top: '50%',
left: '50%',
transform: 'translate(-50%,-50%)'
}}>
<Loader/><br/><br/>
<span style={{
color: 'white',
fontSize: 22,
fontFamily: 'Trim',
textAlign: 'center'
}}>Loading</span>
</div>
</div>}
>
<div style={{width: '100%'}}>
<div
style={{
textAlign: 'center',
marginBottom: 24,
fontFamily: 'Trim',
fontWeight: 500,
fontSize: '26px'
}}
>Subscription statuses for user: {userId}</div>
<SubscriptionsTable
userId={userId}
data={subscriptions}
cancellationSent={subscriptionCancellationSent}
onCancelSubscription={handleSubsctiptionCancel}
onViewTransaction={handleViewTransaction}
/>
<div id="transactions">
<div
style={{
textAlign: 'center',
marginTop: 24,
marginBottom: 24,
fontFamily: 'Trim',
fontWeight: 500,
fontSize: '26px',
}}
>
{hasTransactions ?
`Transactions for user: ${userId}` :
<i>This user does not have transactions linked to their subscription</i>}
</div>
{hasTransactions &&
<TransactionsTable
userId={userId}
data={transactions}
cancellationSent={transactionCancellationSent}
onRefundCharge={handleChargeRefund}
/>}
</div>
</div>
</Page>
)
};
What if you add a filtered state inside ResultPage and pass this to TransactionsTable? handleViewTransaction will do the filtering depending on subscriptionId. Some "pseudo code":
First, set default with all transactions.
const [filteredTransactions, setFilteredTransactions] = useState(transactions);
In the handleViewTransactions, do something like:
const filteredData = transactions.filter((item) => item.subscriptionId === subscriptionId);
setFilteredTransactions(filteredData);
The TransactionsTable component will use the filtered transactions instead:
<TransactionsTable
...
data={filteredTransactions}
...
/>
The only way to make a component rerender is to have its state or its props change, or an ancestor rerenders. With that in mind, if you want the sibling of a component to rerender when something happens, the best solution is to make the common parent update its state, and pass some value into the sibling.
For your type of scenario, you probably want to hold the selection state in the parent, and pass the value and setter functions down into the children as necessary