Skip to content

Commit 1efeb22

Browse files
Add files via upload
1 parent 2abe683 commit 1efeb22

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+7905
-2
lines changed

README.md

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,32 @@
1-
# 50-Projects-In-50-Days---HTML-CSS-JavaScript
2-
50 Projects In 50 Days - HTML, CSS & JavaScript, by Packt Publishing
1+
# DevSpace Blog
2+
3+
> Static Next.js blog that uses Markdown for posts. Includes pagination, categories and search
4+
5+
This project is part of my [Next.js Udemy course](https://www.udemy.com/course/nextjs-dev-to-deployment)
6+
7+
![DevSpace Blog](/public/images/screen.png 'DevSpace Blog')
8+
9+
[VIEW DEMO](https://devspace-blog-pearl.vercel.app)
10+
11+
## Usage
12+
13+
### Install Dependencies
14+
```bash
15+
npm install
16+
```
17+
18+
### Run Dev Server (http://localhost:3000)
19+
```bash
20+
npm run dev
21+
```
22+
23+
### Creating posts
24+
25+
* Create a markdown file in the "posts" folder and name it whatever you want as the slug
26+
* Add the frontmatter/fields at the top and then the post body. See an example in the "posts" folder of this repo
27+
* Add your cover image and author image in the public/images folder
28+
* For category color coding, edit the "Components/CategoryLabel.js" file
29+
30+
### Caching
31+
32+
Husky is used to run a cache script on git commit. Caching is used for the search api route/serverless function

cache/data.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/CategoryLabel.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Link from 'next/link'
2+
3+
export default function CategoryLabel({ children }) {
4+
const colorKey = {
5+
JavaScript: 'yellow',
6+
CSS: 'blue',
7+
Python: 'green',
8+
PHP: 'purple',
9+
Ruby: 'red',
10+
}
11+
12+
return (
13+
<div
14+
className={`px-2 py-1 bg-${colorKey[children]}-600 text-gray-100 font-bold rounded`}
15+
>
16+
<Link href={`/blog/category/${children.toLowerCase()}`}>{children}</Link>
17+
</div>
18+
)
19+
}

components/CategoryList.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Link from 'next/link'
2+
3+
export default function CategoryList({ categories }) {
4+
return (
5+
<div className='w-full p-5 bg-white rounded-lg shadow-md mt-6'>
6+
<h3 className='text-2xl bg-gray-800 text-white p-3 rounded'>
7+
Blog Categories
8+
</h3>
9+
<ul className='divide-y divide-gray-300'>
10+
{categories.map((category, index) => (
11+
<Link key={index} href={`/blog/category/${category.toLowerCase()}`}>
12+
<li className='p-4 cursor-pointer hover:bg-gray-50'>{category}</li>
13+
</Link>
14+
))}
15+
</ul>
16+
</div>
17+
)
18+
}

components/Header.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Link from 'next/link'
2+
import Image from 'next/image'
3+
4+
export default function Header() {
5+
return (
6+
<header className='bg-gray-900 text-gray-100 shadow w-full'>
7+
<div className='container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center'>
8+
<Link href='/'>
9+
<a className='flex md:w-1/5 title-font font-medium items-center md:justify-start mb-4 md:mb-0'>
10+
<Image src='/images/logo.png' width={40} height={40} alt='logo' />
11+
<span className='ml-3 text-xl'>DevSpace</span>
12+
</a>
13+
</Link>
14+
<nav className='flex flex-wrap md:w-4/5 items-center justify-end text-base md:ml-auto'>
15+
<Link href='/blog'>
16+
<a className='mx-5 cursor-pointer uppercase hover:text-indigo-300'>
17+
Blog
18+
</a>
19+
</Link>
20+
<Link href='/about'>
21+
<a className='mx-5 cursor-pointer uppercase hover:text-indigo-300'>
22+
About
23+
</a>
24+
</Link>
25+
</nav>
26+
</div>
27+
</header>
28+
)
29+
}

components/Layout.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Head from 'next/head'
2+
import Header from './Header'
3+
import Search from './Search'
4+
5+
export default function Layout({ title, keywords, description, children }) {
6+
return (
7+
<div>
8+
<Head>
9+
<title>{title}</title>
10+
<meta name='keywords' content={keywords} />
11+
<meta name='description' content={description} />
12+
<link rel='icon' href='/favicon.ico' />
13+
</Head>
14+
15+
<Header />
16+
<Search />
17+
<main className='container mx-auto my-7'>{children}</main>
18+
</div>
19+
)
20+
}
21+
22+
Layout.defaultProps = {
23+
title: 'Welcome to DevSpace',
24+
keywords: 'development, coding, programming',
25+
description: 'The best info and news in development',
26+
}

components/Pagination.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Link from 'next/link'
2+
3+
export default function Pagination({ currentPage, numPages }) {
4+
const isFirst = currentPage === 1
5+
const isLast = currentPage === numPages
6+
const prevPage = `/blog/page/${currentPage - 1}`
7+
const nextPage = `/blog/page/${currentPage + 1}`
8+
9+
if (numPages === 1) return <></>
10+
11+
return (
12+
<div className='mt-6'>
13+
<ul className='flex pl-0 list-none my-2'>
14+
{!isFirst && (
15+
<Link href={prevPage}>
16+
<li className='relative block py-2 px-3 leading-tight bg-white border border-gray-300 text-gray-800 mr-1 hover:bg-gray-200 cursor-pointer'>
17+
Previous
18+
</li>
19+
</Link>
20+
)}
21+
{Array.from({ length: numPages }, (_, i) => (
22+
<Link href={`/blog/page/${i + 1}`}>
23+
<li className='relative block py-2 px-3 leading-tight bg-white border border-gray-300 text-gray-800 mr-1 hover:bg-gray-200 cursor-pointer'>
24+
{i + 1}
25+
</li>
26+
</Link>
27+
))}
28+
29+
{!isLast && (
30+
<Link href={nextPage}>
31+
<li className='relative block py-2 px-3 leading-tight bg-white border border-gray-300 text-gray-800 mr-1 hover:bg-gray-200 cursor-pointer'>
32+
Next
33+
</li>
34+
</Link>
35+
)}
36+
</ul>
37+
</div>
38+
)
39+
}

components/Post.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Link from 'next/link'
2+
import Image from 'next/image'
3+
import CategoryLabel from './CategoryLabel'
4+
5+
export default function Post({ post, compact }) {
6+
return (
7+
<div className='w-full px-10 py-6 bg-white rounded-lg shadow-md mt-6'>
8+
{!compact && (
9+
<Image
10+
src={post.frontmatter.cover_image}
11+
alt=''
12+
height={420}
13+
width={600}
14+
className='mb-4 rounded'
15+
/>
16+
)}
17+
<div className='flex justify-between items-center'>
18+
<span className='font-light text-gray-600'>
19+
{post.frontmatter.date}
20+
</span>
21+
<CategoryLabel>{post.frontmatter.category}</CategoryLabel>
22+
</div>
23+
24+
<div className='mt-2'>
25+
<Link href={`/blog/${post.slug}`}>
26+
<a className='text-2xl text-gray-700 font-bold hover:underline'>
27+
{post.frontmatter.title}
28+
</a>
29+
</Link>
30+
<p className='mt-2 text-gray-600'>{post.frontmatter.excerpt}</p>
31+
</div>
32+
33+
{!compact && (
34+
<div className='flex justify-between items-center mt-6'>
35+
<Link href={`/blog/${post.slug}`}>
36+
<a className='text-gray-900 hover:text-blue-600'>Read More</a>
37+
</Link>
38+
<div className='flex items-center'>
39+
<img
40+
src={post.frontmatter.author_image}
41+
alt=''
42+
className='mx-4 w-10 h-10 object-cover rounded-full hidden sm:block'
43+
/>
44+
<h3 className='text-gray-700 font-bold'>
45+
{post.frontmatter.author}
46+
</h3>
47+
</div>
48+
</div>
49+
)}
50+
</div>
51+
)
52+
}

components/Search.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useState, useEffect } from 'react'
2+
import { FaSearch } from 'react-icons/fa'
3+
import SearchResults from './SearchResults'
4+
5+
export default function Search() {
6+
const [searchTerm, setSearchTerm] = useState('')
7+
const [searchResults, setSearchResults] = useState([])
8+
9+
useEffect(() => {
10+
const getResults = async () => {
11+
if (searchTerm === '') {
12+
setSearchResults([])
13+
} else {
14+
const res = await fetch(`/api/search?q=${searchTerm}`)
15+
const { results } = await res.json()
16+
setSearchResults(results)
17+
}
18+
}
19+
20+
getResults()
21+
}, [searchTerm])
22+
23+
return (
24+
<div className='relative bg-gray-600 p-4'>
25+
<div className='container mx-auto flex items-center justify-center md:justify-end'>
26+
<div className='relative text-gray-600 w-72'>
27+
<form>
28+
<input
29+
type='search'
30+
name='search'
31+
id='search'
32+
className='bg-white h-10 px-5 pr-10 rounded-full text-sm focus:outline-none w-72'
33+
value={searchTerm}
34+
onChange={(e) => setSearchTerm(e.target.value)}
35+
placeholder='Search Posts...'
36+
/>
37+
38+
<FaSearch className='absolute top-0 right-0 text-black mt-3 mr-4' />
39+
</form>
40+
</div>
41+
</div>
42+
43+
<SearchResults results={searchResults} />
44+
</div>
45+
)
46+
}

components/SearchResults.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Post from './Post'
2+
3+
export default function SearchResults({ results }) {
4+
if (results.length === 0) return <></>
5+
6+
return (
7+
<div className='absolute top-20 right-0 md:right-10 z-10 border-4 border-gray-500 bg-white text-black w-full md:w-6/12 rounded-2xl'>
8+
<div className='p-10'>
9+
<h2 className='text-3xl mb-3'>{results.length} Results</h2>
10+
{results.map((result, index) => (
11+
<Post key={index} post={result} compact={true} />
12+
))}
13+
</div>
14+
</div>
15+
)
16+
}

config/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const POSTS_PER_PAGE = 6

jsconfig.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"paths": {
5+
"@/components/*": ["components/*"],
6+
"@/config/*": ["config/*"],
7+
"@/utils/*": ["utils/*"],
8+
"@/lib/*": ["lib/*"]
9+
}
10+
}
11+
}

lib/posts.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import matter from 'gray-matter'
4+
import { sortByDate } from '@/utils/index'
5+
6+
const files = fs.readdirSync(path.join('posts'))
7+
8+
export function getPosts() {
9+
const posts = files.map((filename) => {
10+
const slug = filename.replace('.md', '')
11+
12+
const markdownWithMeta = fs.readFileSync(
13+
path.join('posts', filename),
14+
'utf-8'
15+
)
16+
17+
const { data: frontmatter } = matter(markdownWithMeta)
18+
19+
return {
20+
slug,
21+
frontmatter,
22+
}
23+
})
24+
25+
return posts.sort(sortByDate)
26+
}

0 commit comments

Comments
 (0)