Skip to content

Commit 6d23f31

Browse files
committed
soffiato
1 parent 76989f0 commit 6d23f31

26 files changed

+1057
-95
lines changed

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 right-0 -top-14">
9+
<Image
10+
unoptimized
11+
loader={grpahCMSImageLoader}
12+
alt={author.name}
13+
height="100px"
14+
width="100px"
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

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1-
import React from 'react'
1+
import React, { useState, useEffect } from 'react';
2+
import Link from 'next/link';
3+
4+
import { getCategories } from '../services';
25

36
const Categories = () => {
7+
const [categories, setCategories] = useState([]);
8+
9+
useEffect(() => {
10+
getCategories().then((newCategories) => {
11+
setCategories(newCategories);
12+
});
13+
}, []);
14+
415
return (
5-
<div>Categories</div>
6-
)
7-
}
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+
};
826

9-
export default Categories
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="30px"
18+
width="30px"
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

+27-22
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
1-
import React, {useContext} from 'react'
1+
import React, { useState, useEffect } from 'react';
22

3-
import Link from 'next/link'
4-
const Categories = [{name: 'React', slug: 'react'}, {name: 'Web Development', slug:'web-dev'}]
3+
import Link from 'next/link';
4+
import { getCategories } from '../services';
55

66
const Header = () => {
7+
const [categories, setCategories] = useState([]);
8+
9+
useEffect(() => {
10+
getCategories().then((newCategories) => {
11+
setCategories(newCategories);
12+
});
13+
}, []);
14+
715
return (
8-
<div className='container mx-auto px-10 mb-8'>
9-
<div className='border-b w-full inline-block border-blue-400 py-8'>
10-
<div className='md:float-left block'>
11-
<Link href='/'>
12-
<span className="cursor-pointer font-bold text-4xl text-white">Tomz Blog</span>
13-
</Link>
14-
</div>
15-
<div className='hidden md:float-left md:contents'>
16-
{Categories.map((category) => (
17-
<Link key = {category.slug} href={`/category/${category.slug}`}>
18-
<span className='md:float-right mt-2 align-middle text-white ml-4 font-semibold cursor-pointer'>
19-
{category.name}
20-
</span>
21-
</Link>
22-
))}
23-
</div>
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">Graph CMS</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+
))}
2427
</div>
28+
</div>
2529
</div>
26-
)
27-
}
30+
);
31+
};
2832

29-
export default Header
33+
export default Header;
34+

components/Layout.jsx

+9-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import React from 'react'
2-
import { Header } from '.'
1+
import React from 'react';
2+
import Header from './Header';
33

4-
function Layout({ children }) {
5-
return (
6-
<>
7-
<Header />
8-
{children}
9-
</>
10-
)
11-
}
4+
const Layout = ({ children }) => (
5+
<>
6+
<Header />
7+
{children}
8+
</>
9+
);
1210

13-
export default Layout
11+
export default Layout;

components/Loader.jsx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
3+
const Loader = () => (
4+
<div className="text-center">
5+
<button
6+
type="button"
7+
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"
8+
disabled=""
9+
>
10+
<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">
11+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
12+
<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" />
13+
</svg>
14+
Loading
15+
</button>
16+
</div>
17+
);
18+
19+
export default Loader;

0 commit comments

Comments
 (0)