Created
November 4, 2021 08:13
-
Star
(142)
You must be signed in to star a gist -
Fork
(16)
You must be signed in to fork a gist
-
-
Save adrianhajdin/2b2e8509a48229baf9bb9b53d4a31c91 to your computer and use it in GitHub Desktop.
GraphCMS Blog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
Really good, though I'm using TypeScript so had to adjust most of the syntax.
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
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