Skip to content

Commit 145a5b5

Browse files
authored
Merge pull request #133 from codebayu/feature-revamp-blog
Feature - Revamp Blog Page
2 parents e8cd983 + bb97bda commit 145a5b5

File tree

11 files changed

+187
-122
lines changed

11 files changed

+187
-122
lines changed

.DS_Store

0 Bytes
Binary file not shown.

app/blog/[slug]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default async function BlogDetailPage({ params, searchParams }: Props) {
4040
return (
4141
<>
4242
<Container data-aos="fade-left">
43-
<BackButton url="/blog" />
43+
<BackButton url="/blog?category=home" />
4444
<ReaderPage content={blog} pageViewCount={pageViewCount} comments={comments} />
4545
</Container>
4646
</>

app/blog/page.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Metadata } from 'next'
2+
import { revalidatePath } from 'next/cache'
23

34
import Container from '@/components/elements/Container'
4-
import PageHeading from '@/components/elements/PageHeading'
5+
import { getBlogData } from '@/services/blog'
56

7+
import { BLOG_LINK } from '@/common/constant/menu'
68
import { METADATA } from '@/common/constant/metadata'
9+
import { BlogItem } from '@/common/types/blog'
710

811
import Blog from '@/modules/blog'
912

@@ -16,16 +19,22 @@ export const metadata: Metadata = {
1619
}
1720
}
1821

19-
const PAGE_TITLE = 'Blog'
20-
const PAGE_DESCRIPTION = 'Exploring the world of code, creativity, and constant learning.'
21-
22-
export default async function BlogPage() {
22+
export default async function BlogPage({ searchParams }: { searchParams: { category: string } }) {
23+
const blogs = await getBlog(searchParams.category)
2324
return (
24-
<>
25-
<Container data-aos="fade-left">
26-
<PageHeading title={PAGE_TITLE} description={PAGE_DESCRIPTION} />
27-
<Blog />
28-
</Container>
29-
</>
25+
<Container data-aos="fade-left">
26+
<Blog blogs={blogs} />
27+
</Container>
3028
)
3129
}
30+
31+
async function getBlog(category: string) {
32+
revalidatePath('/blog')
33+
const blogs = await getBlogData()
34+
35+
const data: BlogItem[] = blogs?.filter((blog: BlogItem) => {
36+
const activeId = BLOG_LINK.find(link => link.value === category)?.id
37+
return blog.collection_id === activeId
38+
})
39+
return data
40+
}

common/constant/menu.tsx

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,35 @@ export const MENU_ITEMS: MenuItemProps[] = [
3535
eventName: 'Pages: Home'
3636
},
3737
{
38-
title: 'Projects',
39-
href: '/projects',
40-
icon: <ProjectIcon size={iconSize} />,
38+
title: 'About',
39+
href: '/about',
40+
icon: <ProfileIcon size={iconSize} />,
4141
isShow: true,
4242
isExternal: false,
43-
eventName: 'Pages: Projects'
43+
eventName: 'Pages: About'
4444
},
4545
{
4646
title: 'Blog',
47-
href: '/blog',
47+
href: '/blog?category=home',
4848
icon: <BlogIcon size={iconSize} />,
4949
isShow: true,
5050
isExternal: false,
5151
eventName: 'Pages: Blog'
5252
},
53+
{
54+
title: 'Projects',
55+
href: '/projects',
56+
icon: <ProjectIcon size={iconSize} />,
57+
isShow: true,
58+
isExternal: false,
59+
eventName: 'Pages: Projects'
60+
},
61+
5362
{
5463
title: 'Learn',
5564
href: '/learn',
5665
icon: <LearnIcon size={iconSize} />,
57-
isShow: true,
66+
isShow: false,
5867
isExternal: false,
5968
eventName: 'Pages: Learn'
6069
},
@@ -82,14 +91,6 @@ export const MENU_ITEMS: MenuItemProps[] = [
8291
isExternal: false,
8392
eventName: 'Pages: Chat Room'
8493
},
85-
{
86-
title: 'About',
87-
href: '/about',
88-
icon: <ProfileIcon size={iconSize} />,
89-
isShow: true,
90-
isExternal: false,
91-
eventName: 'Pages: About'
92-
},
9394
{
9495
title: 'Contact',
9596
href: '/contact',
@@ -189,3 +190,30 @@ export const SOCIAL_MEDIA: MenuItemProps[] = [
189190
backgroundColor: 'bg-black'
190191
}
191192
]
193+
194+
export const BLOG_LINK = [
195+
{
196+
id: null,
197+
href: '?category=home',
198+
label: 'Home',
199+
value: 'home'
200+
},
201+
{
202+
id: 24593,
203+
href: '?category=nextjs',
204+
label: 'Next.js',
205+
value: 'nextjs'
206+
},
207+
{
208+
id: 24596,
209+
href: '?category=typescript',
210+
label: 'TypeScript',
211+
value: 'typescript'
212+
},
213+
{
214+
id: null,
215+
href: '/roadmap?tribe=frontend-developer',
216+
label: 'Roadmap',
217+
value: 'roadmap'
218+
}
219+
]

components/elements/ShootingStar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export default function ShootingStar() {
22
return (
3-
<div className="absolute left-40 top-20">
3+
<div className="absolute left-40 top-20 z-30">
44
<div className="night">
55
<div className="star"></div>
66
<div className="star"></div>

modules/blog/components/Blog.tsx

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,24 @@
1-
'use client'
2-
3-
import EmptyState from '@/components/elements/EmptyState'
4-
import LoadingCard from '@/components/elements/LoadingCard'
5-
import { fetcher } from '@/services/fetcher'
6-
import clsx from 'clsx'
7-
import { motion } from 'framer-motion'
8-
import useSWR from 'swr'
9-
10-
import { DEVTO_BLOG_API } from '@/common/constant'
111
import { BlogItem } from '@/common/types/blog'
122

13-
import { useBlogView } from '@/stores/blog-view'
14-
15-
import useIsMobile from '@/hooks/useIsMobile'
16-
173
import BlogCard from './BlogCard'
18-
import BlogListHeader from './BlogListHeader'
19-
20-
export default function Blog() {
21-
const isMobile = useIsMobile()
22-
const { viewOption, setViewOption } = useBlogView()
23-
const { data, isLoading } = useSWR(DEVTO_BLOG_API, fetcher, {
24-
revalidateOnMount: true
25-
})
4+
import BlogHeader from './BlogHeader'
5+
import BlogThumbnail from './BlogThumbnail'
266

27-
const blogs: BlogItem[] = data?.filter((blog: BlogItem) => blog.collection_id === null)
28-
29-
if (isLoading)
30-
return (
31-
<div
32-
className={clsx(
33-
'gap-5 sm:gap-4',
34-
viewOption === 'list' || isMobile ? 'flex flex-col' : 'grid grid-cols-2 sm:!gap-5'
35-
)}
36-
>
37-
{[1, 2].map(item => (
38-
<LoadingCard key={item} view={viewOption} />
39-
))}
40-
</div>
41-
)
42-
43-
if (blogs.length === 0 && !isLoading) {
44-
return <EmptyState message="No Data" />
45-
}
7+
interface BlogProps {
8+
blogs: BlogItem[]
9+
}
4610

11+
export default function Blog({ blogs }: BlogProps) {
4712
return (
4813
<>
49-
{!isMobile && <BlogListHeader viewOption={viewOption} setViewOption={setViewOption} />}
50-
<div className={clsx('gap-5 sm:gap-4', viewOption === 'list' || isMobile ? 'flex flex-col' : 'grid grid-cols-2')}>
51-
{blogs?.map((item: BlogItem, index: number) => (
52-
<motion.div
53-
key={index}
54-
initial={{ opacity: 0, scale: 0.8 }}
55-
animate={{ opacity: 1, scale: 1 }}
56-
transition={{ duration: 0.3, delay: index * 0.1 }}
57-
>
58-
<BlogCard view={viewOption} {...item} />
59-
</motion.div>
60-
))}
14+
<BlogHeader />
15+
<BlogThumbnail newestBlog={blogs[0]} />
16+
<h3 className="mb-4 mt-10 hidden font-semibold text-neutral-700 transition-all duration-300 dark:text-neutral-200 md:block md:text-xl">
17+
Related Articles
18+
</h3>
19+
20+
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
21+
{blogs?.slice(1).map((item: BlogItem, index: number) => <BlogCard key={index} {...item} />)}
6122
</div>
6223
</>
6324
)

modules/blog/components/BlogCard.tsx

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1+
'use client'
2+
13
import { usePathname, useRouter } from 'next/navigation'
24

3-
import Card from '@/components/elements/Card'
45
import Image from '@/components/elements/Image'
5-
import { useEffect, useState } from 'react'
66
import { TbMessage2 as CommentIcon } from 'react-icons/tb'
77

88
import { PLACEHOLDER_URL } from '@/common/constant'
99
import { formatBlogSlug, formatDate } from '@/common/helpers'
10-
import { cn } from '@/common/libs/cn'
1110
import { sendDataLayer } from '@/common/libs/gtm'
1211
import { BlogItem } from '@/common/types/blog'
1312

14-
import useIsMobile from '@/hooks/useIsMobile'
15-
1613
interface BlogCardProps extends BlogItem {
1714
view?: string
1815
isExcerpt?: boolean
@@ -27,26 +24,16 @@ export default function BlogCard({
2724
description,
2825
slug,
2926
comments_count,
30-
view,
31-
isExcerpt = true,
32-
isCarousel = false,
3327
collection_id
3428
}: BlogCardProps) {
35-
const [viewOption, setViewOption] = useState<string>()
36-
const isMobile = useIsMobile()
3729
const router = useRouter()
3830
const pathname = usePathname()
3931

4032
const newSlug = formatBlogSlug(slug)
4133

42-
const trimmedTitle = viewOption === 'grid' ? title.slice(0, 70) + (title.length > 70 ? '...' : '') : title
34+
const trimmedTitle = title.slice(0, 70) + (title.length > 70 ? '...' : '')
4335
const trimmedContent = description.slice(0, 100) + (description.length > 100 ? '...' : '')
4436

45-
const contentContainerClasses = cn(
46-
'flex flex-col self-center w-full sm:w-4/5 flex-grow space-y-3 px-5 sm:p-0 mb-5 sm:mb-0',
47-
view === 'grid' ? 'sm:w-full sm:!p-6' : ''
48-
)
49-
5037
function handleCardClick() {
5138
sendDataLayer({
5239
event: 'article_clicked',
@@ -58,33 +45,18 @@ export default function BlogCard({
5845
router.push(`/blog/${newSlug}?id=${id}&read-mode=true`)
5946
}
6047

61-
useEffect(() => {
62-
isMobile ? setViewOption('grid') : setViewOption(view)
63-
}, [isMobile, view])
64-
6548
return (
66-
<Card
67-
onClick={handleCardClick}
68-
className={cn(
69-
'flex w-full cursor-pointer items-center gap-6 border border-neutral-300 dark:border-neutral-800 dark:bg-neutral-800 sm:flex-row lg:hover:scale-[102%]',
70-
viewOption === 'grid' ? 'w-full !flex-col sm:h-full' : '!flex-row sm:p-5 sm:px-6',
71-
isCarousel && 'min-w-[350px]',
72-
!isExcerpt && 'sm:h-[320px]'
73-
)}
74-
>
75-
<div className={`${viewOption === 'grid' ? 'w-full' : 'w-fit'}`}>
49+
<div className="flex w-full cursor-pointer flex-col text-start" onClick={handleCardClick}>
50+
<div className="overflow-hidden rounded-lg">
7651
<Image
7752
src={cover_image || PLACEHOLDER_URL}
78-
width={isMobile || viewOption === 'grid' ? 400 : 240}
53+
width={200}
7954
height={100}
8055
alt={title}
81-
className={cn(
82-
'w-full object-cover sm:h-[8.5rem] sm:rounded-xl',
83-
viewOption === 'grid' ? '!h-48 !rounded-b-none !rounded-t-xl' : ''
84-
)}
56+
className="!h-48 w-full object-cover"
8557
/>
8658
</div>
87-
<article className={contentContainerClasses}>
59+
<article className="mt-4 flex w-full flex-grow flex-col space-y-3 self-center">
8860
<h2 className="font-medium text-neutral-600 transition-all duration-300 dark:text-neutral-200 dark:hover:text-teal-400 md:text-[17px] lg:hover:text-teal-800">
8961
{trimmedTitle}
9062
</h2>
@@ -102,12 +74,10 @@ export default function BlogCard({
10274
</span>
10375
</div>
10476
</div>
105-
{isExcerpt && (
106-
<p className="hidden text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 sm:block">
107-
{trimmedContent}
108-
</p>
109-
)}
77+
<p className="hidden text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 sm:block">
78+
{trimmedContent}
79+
</p>
11080
</article>
111-
</Card>
81+
</div>
11282
)
11383
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client'
2+
3+
import Image from 'next/image'
4+
import Link from 'next/link'
5+
import { useSearchParams } from 'next/navigation'
6+
7+
import { useTheme } from 'next-themes'
8+
9+
import { BLOG_LINK } from '@/common/constant/menu'
10+
import { cn } from '@/common/libs/cn'
11+
12+
export default function BlogHeader() {
13+
const { resolvedTheme } = useTheme()
14+
const searchParams = useSearchParams()
15+
const activeCategory = searchParams.get('category')
16+
return (
17+
<div className="flex flex-col items-center">
18+
<Image
19+
src={resolvedTheme === 'dark' ? '/img/logo-white.png' : '/img/logo-black.png'}
20+
width={90}
21+
height={90}
22+
alt="logo"
23+
/>
24+
<p className="text-center text-sm text-neutral-600 dark:text-neutral-500">
25+
Welcome to my blog! Your Source for Expert Tips and Insights!
26+
</p>
27+
28+
<nav className="my-6 flex w-full justify-between gap-4 border-y border-neutral-200 p-3 dark:border-neutral-800 md:w-max md:justify-center md:gap-10">
29+
{BLOG_LINK.map((link, index) => (
30+
<Link
31+
key={index}
32+
href={link.href}
33+
className={cn(
34+
'text-sm ',
35+
activeCategory === link.value
36+
? 'text-emerald-500 dark:text-emerald-300'
37+
: 'text-neutral-500 dark:text-neutral-300'
38+
)}
39+
>
40+
{link.label}
41+
</Link>
42+
))}
43+
</nav>
44+
</div>
45+
)
46+
}

0 commit comments

Comments
 (0)