-
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.