Skip to content

Instantly share code, notes, and snippets.

@adrianhajdin
Created November 4, 2021 08:13
Show Gist options
  • Save adrianhajdin/2b2e8509a48229baf9bb9b53d4a31c91 to your computer and use it in GitHub Desktop.
Save adrianhajdin/2b2e8509a48229baf9bb9b53d4a31c91 to your computer and use it in GitHub Desktop.
GraphCMS Blog
import React from 'react';
import { useRouter } from 'next/router';
import { getCategories, getCategoryPost } from '../../services';
import { PostCard, Categories, Loader } from '../../components';
const CategoryPost = ({ posts }) => {
const router = useRouter();
if (router.isFallback) {
return <Loader />;
}
return (
<div className="container mx-auto px-10 mb-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
<div className="col-span-1 lg:col-span-8">
{posts.map((post, index) => (
<PostCard key={index} post={post.node} />
))}
</div>
<div className="col-span-1 lg:col-span-4">
<div className="relative lg:sticky top-8">
<Categories />
</div>
</div>
</div>
</div>
);
};
export default CategoryPost;
// Fetch data at build time
export async function getStaticProps({ params }) {
const posts = await getCategoryPost(params.slug);
return {
props: { posts },
};
}
// Specify dynamic routes to pre-render pages based on data.
// The HTML is generated at build time and will be reused on each request.
export async function getStaticPaths() {
const categories = await getCategories();
return {
paths: categories.map(({ slug }) => ({ params: { slug } })),
fallback: true,
};
}
import React from 'react';
import moment from 'moment';
import Image from 'next/image';
import Link from 'next/link';
const FeaturedPostCard = ({ post }) => (
<div className="relative h-72">
<div className="absolute rounded-lg bg-center bg-no-repeat bg-cover shadow-md inline-block w-full h-72" style={{ backgroundImage: `url('${post.featuredImage.url}')` }} />
<div className="absolute rounded-lg bg-center bg-gradient-to-b opacity-50 from-gray-400 via-gray-700 to-black w-full h-72" />
<div className="flex flex-col rounded-lg p-4 items-center justify-center absolute w-full h-full">
<p className="text-white mb-4 text-shadow font-semibold text-xs">{moment(post.createdAt).format('MMM DD, YYYY')}</p>
<p className="text-white mb-4 text-shadow font-semibold text-2xl text-center">{post.title}</p>
<div className="flex items-center absolute bottom-5 w-full justify-center">
<Image
unoptimized
alt={post.author.name}
height="30px"
width="30px"
className="align-middle drop-shadow-lg rounded-full"
src={post.author.photo.url}
/>
<p className="inline align-middle text-white text-shadow ml-2 font-medium">{post.author.name}</p>
</div>
</div>
<Link href={`/post/${post.slug}`}><span className="cursor-pointer absolute w-full h-full" /></Link>
</div>
);
export default FeaturedPostCard;
import React, { useState, useEffect } from 'react';
import Carousel from 'react-multi-carousel';
import 'react-multi-carousel/lib/styles.css';
import { FeaturedPostCard } from '../components';
import { getFeaturedPosts } from '../services';
const responsive = {
superLargeDesktop: {
breakpoint: { max: 4000, min: 1024 },
items: 5,
},
desktop: {
breakpoint: { max: 1024, min: 768 },
items: 3,
},
tablet: {
breakpoint: { max: 768, min: 640 },
items: 2,
},
mobile: {
breakpoint: { max: 640, min: 0 },
items: 1,
},
};
const FeaturedPosts = () => {
const [featuredPosts, setFeaturedPosts] = useState([]);
const [dataLoaded, setDataLoaded] = useState(false);
useEffect(() => {
getFeaturedPosts().then((result) => {
setFeaturedPosts(result);
setDataLoaded(true);
});
}, []);
const customLeftArrow = (
<div className="absolute arrow-btn left-0 text-center py-3 cursor-pointer bg-pink-600 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white w-full" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</div>
);
const customRightArrow = (
<div className="absolute arrow-btn right-0 text-center py-3 cursor-pointer bg-pink-600 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white w-full" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</div>
);
return (
<div className="mb-8">
<Carousel infinite customLeftArrow={customLeftArrow} customRightArrow={customRightArrow} responsive={responsive} itemClass="px-4">
{dataLoaded && featuredPosts.map((post, index) => (
<FeaturedPostCard key={index} post={post} />
))}
</Carousel>
</div>
);
};
export default FeaturedPosts;
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;700&display=swap');
html,
body {
padding: 0;
margin: 0;
font-family: 'Montserrat', sans-serif;
&:before{
content:'';
content: "";
width: 100%;
height: 100vh;
//background: linear-gradient(to right bottom, #6d327c, #485DA6, #00a1ba, #01b18e, #32b37b);
// background: #D3D3D3;
background-image: url("../public/bg.jpg");
position: fixed;
left: 0;
top: 0;
z-index: -1;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: cover;
}
}
.text-shadow{
text-shadow: 0px 2px 0px rgb(0 0 0 / 30%);
}
.adjacent-post{
& .arrow-btn{
transition: width 300ms ease;
width: 50px;
}
&:hover{
& .arrow-btn{
width: 60px;
}
}
}
.react-multi-carousel-list {
& .arrow-btn{
transition: width 300ms ease;
width: 50px;
&:hover{
width: 60px;
}
}
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
import React from 'react';
const Loader = () => (
<div className="text-center">
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-rose-600 hover:bg-rose-500 focus:border-rose-700 active:bg-rose-700 transition ease-in-out duration-150 cursor-not-allowed"
disabled=""
>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Loading
</button>
</div>
);
export default Loader;
import React from 'react';
import moment from 'moment';
const PostDetail = ({ post }) => {
const getContentFragment = (index, text, obj, type) => {
let modifiedText = text;
if (obj) {
if (obj.bold) {
modifiedText = (<b key={index}>{text}</b>);
}
if (obj.italic) {
modifiedText = (<em key={index}>{text}</em>);
}
if (obj.underline) {
modifiedText = (<u key={index}>{text}</u>);
}
}
switch (type) {
case 'heading-three':
return <h3 key={index} className="text-xl font-semibold mb-4">{modifiedText.map((item, i) => <React.Fragment key={i}>{item}</React.Fragment>)}</h3>;
case 'paragraph':
return <p key={index} className="mb-8">{modifiedText.map((item, i) => <React.Fragment key={i}>{item}</React.Fragment>)}</p>;
case 'heading-four':
return <h4 key={index} className="text-md font-semibold mb-4">{modifiedText.map((item, i) => <React.Fragment key={i}>{item}</React.Fragment>)}</h4>;
case 'image':
return (
<img
key={index}
alt={obj.title}
height={obj.height}
width={obj.width}
src={obj.src}
/>
);
default:
return modifiedText;
}
};
return (
<>
<div className="bg-white shadow-lg rounded-lg lg:p-8 pb-12 mb-8">
<div className="relative overflow-hidden shadow-md mb-6">
<img src={post.featuredImage.url} alt="" className="object-top h-full w-full object-cover shadow-lg rounded-t-lg lg:rounded-lg" />
</div>
<div className="px-4 lg:px-0">
<div className="flex items-center mb-8 w-full">
<div className="hidden md:flex items-center justify-center lg:mb-0 lg:w-auto mr-8 items-center">
<img
alt={post.author.name}
height="30px"
width="30px"
className="align-middle rounded-full"
src={post.author.photo.url}
/>
<p className="inline align-middle text-gray-700 ml-2 font-medium text-lg">{post.author.name}</p>
</div>
<div className="font-medium text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 inline mr-2 text-pink-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span className="align-middle">{moment(post.createdAt).format('MMM DD, YYYY')}</span>
</div>
</div>
<h1 className="mb-8 text-3xl font-semibold">{post.title}</h1>
{post.content.raw.children.map((typeObj, index) => {
const children = typeObj.children.map((item, itemindex) => getContentFragment(itemindex, item.text, item));
return getContentFragment(index, children, typeObj, typeObj.type);
})}
</div>
</div>
</>
);
};
export default PostDetail;
import { request, gql } from 'graphql-request';
const graphqlAPI = process.env.NEXT_PUBLIC_GRAPHCMS_ENDPOINT;
export const getPosts = async () => {
const query = gql`
query MyQuery {
postsConnection {
edges {
cursor
node {
author {
bio
name
id
photo {
url
}
}
createdAt
slug
title
excerpt
featuredImage {
url
}
categories {
name
slug
}
}
}
}
}
`;
const result = await request(graphqlAPI, query);
return result.postsConnection.edges;
};
export const getCategories = async () => {
const query = gql`
query GetGategories {
categories {
name
slug
}
}
`;
const result = await request(graphqlAPI, query);
return result.categories;
};
export const getPostDetails = async (slug) => {
const query = gql`
query GetPostDetails($slug : String!) {
post(where: {slug: $slug}) {
title
excerpt
featuredImage {
url
}
author{
name
bio
photo {
url
}
}
createdAt
slug
content {
raw
}
categories {
name
slug
}
}
}
`;
const result = await request(graphqlAPI, query, { slug });
return result.post;
};
export const getSimilarPosts = async (categories, slug) => {
const query = gql`
query GetPostDetails($slug: String!, $categories: [String!]) {
posts(
where: {slug_not: $slug, AND: {categories_some: {slug_in: $categories}}}
last: 3
) {
title
featuredImage {
url
}
createdAt
slug
}
}
`;
const result = await request(graphqlAPI, query, { slug, categories });
return result.posts;
};
export const getAdjacentPosts = async (createdAt, slug) => {
const query = gql`
query GetAdjacentPosts($createdAt: DateTime!,$slug:String!) {
next:posts(
first: 1
orderBy: createdAt_ASC
where: {slug_not: $slug, AND: {createdAt_gte: $createdAt}}
) {
title
featuredImage {
url
}
createdAt
slug
}
previous:posts(
first: 1
orderBy: createdAt_DESC
where: {slug_not: $slug, AND: {createdAt_lte: $createdAt}}
) {
title
featuredImage {
url
}
createdAt
slug
}
}
`;
const result = await request(graphqlAPI, query, { slug, createdAt });
return { next: result.next[0], previous: result.previous[0] };
};
export const getCategoryPost = async (slug) => {
const query = gql`
query GetCategoryPost($slug: String!) {
postsConnection(where: {categories_some: {slug: $slug}}) {
edges {
cursor
node {
author {
bio
name
id
photo {
url
}
}
createdAt
slug
title
excerpt
featuredImage {
url
}
categories {
name
slug
}
}
}
}
}
`;
const result = await request(graphqlAPI, query, { slug });
return result.postsConnection.edges;
};
export const getFeaturedPosts = async () => {
const query = gql`
query GetCategoryPost() {
posts(where: {featuredPost: true}) {
author {
name
photo {
url
}
}
featuredImage {
url
}
title
slug
createdAt
}
}
`;
const result = await request(graphqlAPI, query);
return result.posts;
};
export const submitComment = async (obj) => {
const result = await fetch('/api/comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(obj),
});
return result.json();
};
export const getComments = async (slug) => {
const query = gql`
query GetComments($slug:String!) {
comments(where: {post: {slug:$slug}}){
name
createdAt
comment
}
}
`;
const result = await request(graphqlAPI, query, { slug });
return result.comments;
};
export const getRecentPosts = async () => {
const query = gql`
query GetPostDetails() {
posts(
orderBy: createdAt_ASC
last: 3
) {
title
featuredImage {
url
}
createdAt
slug
}
}
`;
const result = await request(graphqlAPI, query);
return result.posts;
};
@ni6hant
Copy link

ni6hant commented Jun 5, 2022

I am going to try making my own blog with the accompanying Youtube Tutorial Build and Deploy THE BEST Modern Blog App with React | GraphQL, NextJS, Tailwind CSS and make my own notes that I will post here or if big will put a link for it.

@onovogodwinprosperity
Copy link

really Great Job

@madeleine68
Copy link

Is there a way to include a hyperlink in the body of the post?should we use @graphcms/rich-text-react-renderer? How can we implement it?
Thanks

@Malingozilla
Copy link

Great Job!

@gggg1983
Copy link

gggg1983 commented Sep 8, 2022

@gggg1983
Copy link

gggg1983 commented Sep 8, 2022

@Elipilgrim
Copy link

Hmmm.... This is great stuff would need to make some tweaks to the globals.scss pageI forgot to write down the issues I saw but they (3) are simple walls

@Chura33
Copy link

Chura33 commented Dec 7, 2022

This is a great tutorial I have had some errors but I have been able to overcome them so far. Loving the challenge and I don't know when I would be able to create a tutorial like this.

@AbrsheYotor
Copy link

You are always my number 1 teacher!!!

@jisan25
Copy link

jisan25 commented Jun 1, 2023

I am really grateful to you.

@Geeky-eddie
Copy link

i am currently trying to build this, i will come back to update on my experience once i am done, Great tutorial by the way

@sixtusdeveloper
Copy link

Really good, though I'm using TypeScript so had to adjust most of the syntax.

@sourabhCCSD
Copy link

sourabhCCSD commented Sep 30, 2024

Started this long back in 2022 and then got a job that occupied me so left the tutorial in between. Now after having some free time from my project I am back here again to complete and say, Hi Javascript Mastery.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment