-
Star
(143)
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.
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; | |
}; |
really Great Job
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
Great Job!
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
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.
You are always my number 1 teacher!!!
I am really grateful to you.
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
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.
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.