Skip to content

Commit 148f93a

Browse files
committed
Blog App Using NEXTjs and GraphQl
0 parents  commit 148f93a

40 files changed

+3212
-0
lines changed

.env

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NEXT_PUBLIC_YOURBLOG_ENDPOINT = https://api-ap-south-1.hygraph.com/v2/clg4xmjpw02wj01un7cpleb91/master
2+
YOURBLOG_TOKEN = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdjbXMtbWFpbi1wcm9kdWN0aW9uIn0.eyJ2ZXJzaW9uIjozLCJpYXQiOjE2ODE5NjQ4NDcsImF1ZCI6WyJodHRwczovL2FwaS1hcC1zb3V0aC0xLmh5Z3JhcGguY29tL3YyL2NsZzR4bWpwdzAyd2owMXVuN2NwbGViOTEvbWFzdGVyIiwibWFuYWdlbWVudC1uZXh0LmdyYXBoY21zLmNvbSJdLCJpc3MiOiJodHRwczovL21hbmFnZW1lbnQuZ3JhcGhjbXMuY29tLyIsInN1YiI6IjI2ZDJjYWE3LWFjMTQtNDU5Ny1iZWZmLTI5OWM4ZDQ5M2FjYiIsImp0aSI6ImNsZ29tZnkweTZ2ZXkwMXQ1OWx6ODVmbTIifQ.ip8DoFM7sZtvxAc7j24-k3myF-5eCcnNuizYVB_emTcSPfINIZGAjh-gYYByNqzha81tSxryT7vOj-d3epg0kYZ6-uC9RwBEueSymnvsYVuf5Kbza8Fmj3bfCmVoMf8mCzEb2Zj2tgaopvLvrlMj3x35JfHBn0QPJyLx23bp5HZAXAuU1nUXjRQvw0WAr8KBDu_t1uM2zsEM4zQJA8G8GfmukI41ab7Z0uQnW188lhftu1a07oP6nfjQig0OPToffgP8TqWvZYJiz6Hedv8tsZ6CnKETnJO-94BrrXNpVXOr_6Rfp0jbDpRRBsyHNdtxwxLYRffx2oEHi8PUk0d4sN4ZSkdoZ7RqKxhrE2coIfjBP1BdhwkhOYcBbgFlA1i_5WATFFC83lcK1hdhzLJ7uksraPLwG4Y7mv0QXhFE5UhX0WuuatXFfo1FyyYyXbNLIOBbkJnQvfHfSqjPfHnFMcIRoMeP44yU3bhhyotW8y2QarNcQwfjHEpFCnnx0tmp57akk4YtrCaOwtJLGVwg_UtQ3m36cza5dgDd5WeWtbdNOhUHoTTJcbqfTwV-dnXYdzkt8YRKtZsqsBmtLfOLAwfNzvcc_nBg_swr0IgIkC6VJo5bVlDyxNa6lHAIGDSkj1WL1WcRwtGyNyUkDiJ2cudnB7uWa7bOmJOznyKyIF0'

.gitignore

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
```
14+
15+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16+
17+
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
18+
19+
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
20+
21+
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
22+
23+
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
24+
25+
## Learn More
26+
27+
To learn more about Next.js, take a look at the following resources:
28+
29+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
30+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
31+
32+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
33+
34+
## Deploy on Vercel
35+
36+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
37+
38+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

components/AdjacentPostCard.jsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
import moment from 'moment';
3+
import Link from 'next/link';
4+
5+
const AdjacentPostCard = ({ post, position }) => (
6+
<>
7+
<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}')` }} />
8+
<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" />
9+
<div className="flex flex-col rounded-lg p-4 items-center justify-center absolute w-full h-full">
10+
<p className="text-white text-shadow font-semibold text-xs">{moment(post.createdAt).format('MMM DD, YYYY')}</p>
11+
<p className="text-white text-shadow font-semibold text-2xl text-center">{post.title}</p>
12+
</div>
13+
<Link href={`/post/${post.slug}`}><span className="z-10 cursor-pointer absolute w-full h-full" /></Link>
14+
{position === 'LEFT' && (
15+
<div className="absolute arrow-btn bottom-5 text-center py-3 cursor-pointer bg-pink-600 left-4 rounded-full">
16+
<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">
17+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
18+
</svg>
19+
</div>
20+
)}
21+
{position === 'RIGHT' && (
22+
<div className="absolute arrow-btn bottom-5 text-center py-3 cursor-pointer bg-pink-600 right-4 rounded-full">
23+
<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">
24+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
25+
</svg>
26+
</div>
27+
)}
28+
</>
29+
);
30+
31+
export default AdjacentPostCard;

components/Author.jsx

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import Image from 'next/image';
3+
4+
import { grpahCMSImageLoader } from '../util';
5+
6+
const Author = ({ author }) => (
7+
<div className="text-center mt-20 mb-8 p-12 relative rounded-lg bg-black bg-opacity-20">
8+
<div className="absolute left-0 flex justify-center right-0 -top-14">
9+
<Image
10+
unoptimized
11+
loader={grpahCMSImageLoader}
12+
alt={author.name}
13+
height={100}
14+
width={100}
15+
className="align-middle rounded-full"
16+
src={author.photo.url}
17+
/>
18+
</div>
19+
<h3 className="text-white mt-4 mb-4 text-xl font-bold">{author.name}</h3>
20+
<p className="text-white text-ls">{author.bio}</p>
21+
</div>
22+
);
23+
24+
export default Author;

components/Categories.jsx

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { useState, useEffect } from 'react';
2+
import Link from 'next/link';
3+
4+
import { getCategories } from '../services';
5+
6+
const Categories = () => {
7+
const [categories, setCategories] = useState([]);
8+
9+
useEffect(() => {
10+
getCategories().then((newCategories) => {
11+
setCategories(newCategories);
12+
});
13+
}, []);
14+
15+
return (
16+
<div className="bg-white shadow-lg rounded-lg p-8 pb-12 mb-8">
17+
<h3 className="text-xl mb-8 font-semibold border-b pb-4">Categories</h3>
18+
{categories.map((category, index) => (
19+
<Link key={index} href={`/category/${category.slug}`}>
20+
<span className={`cursor-pointer block ${(index === categories.length - 1) ? 'border-b-0' : 'border-b'} pb-3 mb-3`}>{category.name}</span>
21+
</Link>
22+
))}
23+
</div>
24+
);
25+
};
26+
27+
export default Categories;

components/Comments.jsx

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { useEffect, useState } from 'react';
2+
import moment from 'moment';
3+
import parse from 'html-react-parser';
4+
5+
import { getComments } from '../services';
6+
7+
const Comments = ({ slug }) => {
8+
const [comments, setComments] = useState([]);
9+
10+
useEffect(() => {
11+
getComments(slug).then((result) => {
12+
setComments(result);
13+
});
14+
}, []);
15+
16+
return (
17+
<>
18+
{comments.length > 0 && (
19+
<div className="bg-white shadow-lg rounded-lg p-8 pb-12 mb-8">
20+
<h3 className="text-xl mb-8 font-semibold border-b pb-4">
21+
{comments.length}
22+
{' '}
23+
Comments
24+
</h3>
25+
{comments.map((comment, index) => (
26+
<div key={index} className="border-b border-gray-100 mb-4 pb-4">
27+
<p className="mb-4">
28+
<span className="font-semibold">{comment.name}</span>
29+
{' '}
30+
on
31+
{' '}
32+
{moment(comment.createdAt).format('MMM DD, YYYY')}
33+
</p>
34+
<p className="whitespace-pre-line text-gray-600 w-full">{parse(comment.comment)}</p>
35+
</div>
36+
))}
37+
</div>
38+
)}
39+
</>
40+
);
41+
};
42+
43+
export default Comments;

components/CommentsForm.jsx

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { submitComment } from '../services';
3+
4+
const CommentsForm = ({ slug }) => {
5+
const [error, setError] = useState(false);
6+
const [localStorage, setLocalStorage] = useState(null);
7+
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
8+
const [formData, setFormData] = useState({ name: null, email: null, comment: null, storeData: false });
9+
10+
useEffect(() => {
11+
setLocalStorage(window.localStorage);
12+
const initalFormData = {
13+
name: window.localStorage.getItem('name'),
14+
email: window.localStorage.getItem('email'),
15+
storeData: window.localStorage.getItem('name') || window.localStorage.getItem('email'),
16+
};
17+
setFormData(initalFormData);
18+
}, []);
19+
20+
const onInputChange = (e) => {
21+
const { target } = e;
22+
if (target.type === 'checkbox') {
23+
setFormData((prevState) => ({
24+
...prevState,
25+
[target.name]: target.checked,
26+
}));
27+
} else {
28+
setFormData((prevState) => ({
29+
...prevState,
30+
[target.name]: target.value,
31+
}));
32+
}
33+
};
34+
35+
const handlePostSubmission = () => {
36+
setError(false);
37+
const { name, email, comment, storeData } = formData;
38+
if (!name || !email || !comment) {
39+
setError(true);
40+
return;
41+
}
42+
const commentObj = {
43+
name,
44+
email,
45+
comment,
46+
slug,
47+
};
48+
49+
if (storeData) {
50+
localStorage.setItem('name', name);
51+
localStorage.setItem('email', email);
52+
} else {
53+
localStorage.removeItem('name');
54+
localStorage.removeItem('email');
55+
}
56+
57+
submitComment(commentObj)
58+
.then((res) => {
59+
if (res.createComment) {
60+
if (!storeData) {
61+
formData.name = '';
62+
formData.email = '';
63+
}
64+
formData.comment = '';
65+
setFormData((prevState) => ({
66+
...prevState,
67+
...formData,
68+
}));
69+
setShowSuccessMessage(true);
70+
setTimeout(() => {
71+
setShowSuccessMessage(false);
72+
}, 3000);
73+
}
74+
});
75+
};
76+
77+
return (
78+
<div className="bg-white shadow-lg rounded-lg p-8 pb-12 mb-8">
79+
<h3 className="text-xl mb-8 font-semibold border-b pb-4">Leave a Reply</h3>
80+
<div className="grid grid-cols-1 gap-4 mb-4">
81+
<textarea value={formData.comment} onChange={onInputChange} className="p-4 outline-none w-full rounded-lg h-40 focus:ring-2 focus:ring-gray-200 bg-gray-100 text-gray-700" name="comment" placeholder="Comment" />
82+
</div>
83+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
84+
<input type="text" value={formData.name} onChange={onInputChange} className="py-2 px-4 outline-none w-full rounded-lg focus:ring-2 focus:ring-gray-200 bg-gray-100 text-gray-700" placeholder="Name" name="name" />
85+
<input type="email" value={formData.email} onChange={onInputChange} className="py-2 px-4 outline-none w-full rounded-lg focus:ring-2 focus:ring-gray-200 bg-gray-100 text-gray-700" placeholder="Email" name="email" />
86+
</div>
87+
<div className="grid grid-cols-1 gap-4 mb-4">
88+
<div>
89+
<input checked={formData.storeData} onChange={onInputChange} type="checkbox" id="storeData" name="storeData" value="true" />
90+
<label className="text-gray-500 cursor-pointer" htmlFor="storeData"> Save my name, email in this browser for the next time I comment.</label>
91+
</div>
92+
</div>
93+
{error && <p className="text-xs text-red-500">All fields are mandatory</p>}
94+
<div className="mt-8">
95+
<button type="button" onClick={handlePostSubmission} className="transition duration-500 ease hover:bg-indigo-900 inline-block bg-pink-600 text-lg font-medium rounded-full text-white px-8 py-3 cursor-pointer">Post Comment</button>
96+
{showSuccessMessage && <span className="text-xl float-right font-semibold mt-3 text-green-500">Comment submitted for review</span>}
97+
</div>
98+
</div>
99+
);
100+
};
101+
102+
export default CommentsForm;

components/FeaturedPostCard.jsx

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import moment from 'moment';
3+
import Image from 'next/image';
4+
import Link from 'next/link';
5+
6+
const FeaturedPostCard = ({ post }) => (
7+
<div className="relative h-72">
8+
<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}')` }} />
9+
<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" />
10+
<div className="flex flex-col rounded-lg p-4 items-center justify-center absolute w-full h-full">
11+
<p className="text-white mb-4 text-shadow font-semibold text-xs">{moment(post.createdAt).format('MMM DD, YYYY')}</p>
12+
<p className="text-white mb-4 text-shadow font-semibold text-2xl text-center">{post.title}</p>
13+
<div className="flex items-center absolute bottom-5 w-full justify-center">
14+
<Image
15+
unoptimized
16+
alt={post.author.name}
17+
height={30}
18+
width={30}
19+
className="align-middle drop-shadow-lg rounded-full"
20+
src={post.author.photo.url}
21+
/>
22+
<p className="inline align-middle text-white text-shadow ml-2 font-medium">{post.author.name}</p>
23+
</div>
24+
</div>
25+
<Link href={`/post/${post.slug}`}><span className="cursor-pointer absolute w-full h-full" /></Link>
26+
</div>
27+
);
28+
29+
export default FeaturedPostCard;

components/Header.jsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { useState, useEffect } from 'react';
2+
3+
import Link from 'next/link';
4+
import { getCategories } from '../services';
5+
6+
const Header = () => {
7+
const [categories, setCategories] = useState([]);
8+
9+
useEffect(() => {
10+
getCategories().then((newCategories) => {
11+
setCategories(newCategories);
12+
});
13+
}, []);
14+
15+
return (
16+
<div className="container mx-auto px-10 mb-8">
17+
<div className="border-b w-full inline-block border-blue-400 py-8">
18+
<div className="md:float-left block">
19+
<Link href="/">
20+
<span className="cursor-pointer font-bold text-4xl text-white">Your Blog</span>
21+
</Link>
22+
</div>
23+
<div className="hidden md:float-left md:contents">
24+
{categories.map((category, index) => (
25+
<Link key={index} href={`/category/${category.slug}`}><span className="md:float-right mt-2 align-middle text-white ml-4 font-semibold cursor-pointer">{category.name}</span></Link>
26+
))}
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
};
32+
33+
export default Header;

components/Layout.jsx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react';
2+
import Header from './Header';
3+
4+
const Layout = ({ children }) => (
5+
<>
6+
<Header />
7+
{children}
8+
</>
9+
);
10+
11+
export default Layout;

0 commit comments

Comments
 (0)