Skip to content

Commit

Permalink
feat: add view counter
Browse files Browse the repository at this point in the history
  • Loading branch information
jktrn committed Nov 4, 2023
1 parent 4db8731 commit 21cc1aa
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 6 deletions.
38 changes: 38 additions & 0 deletions app/api/pageviews/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Redis } from '@upstash/redis'
import { NextRequest, NextResponse } from 'next/server'

const redis = Redis.fromEnv()

export async function GET(request: NextRequest) {
const url = request.nextUrl
const slug = url.searchParams.get('slug')

if (!slug) {
return new NextResponse(JSON.stringify({ error: 'Slug is required' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
},
})
}

try {
const key = ['pageviews', 'projects', slug].join(':')
const pageViewCount = (await redis.get(key)) || 0

return new NextResponse(JSON.stringify({ slug, pageViewCount }), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
})
} catch (error) {
console.error('Redis error:', error)
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
})
}
}
1 change: 1 addition & 0 deletions app/blog/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'katex/dist/katex.css'
import { Metadata } from 'next'
import { MDXLayoutRenderer } from 'pliny/mdx-components'
import { allCoreContent, coreContent, sortPosts } from 'pliny/utils/contentlayer'

import { ReportView } from './view'

const defaultLayout = 'PostLayout'
Expand Down
2 changes: 1 addition & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ article[style*='--react-activity-calendar-level-0:#ebebeb'] {
@apply hidden;
}

svg[width="1372"] {
svg[width='1372'] {
@apply hidden;
}

Expand Down
46 changes: 43 additions & 3 deletions layouts/ListLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import Image from '@/components/Image'
import Link from '@/components/Link'
import Tag from '@/components/Tag'
import { Skeleton } from '@/components/shadcn/skeleton'
import siteMetadata from '@/data/siteMetadata'
import type { Blog } from 'contentlayer/generated'
import { Search } from 'lucide-react'
import { usePathname } from 'next/navigation'
import { CoreContent } from 'pliny/utils/contentlayer'
import { formatDate } from 'pliny/utils/formatDate'
import { useState } from 'react'
import { useEffect, useState } from 'react'

interface PaginationProps {
totalPages: number
Expand Down Expand Up @@ -73,11 +74,36 @@ export default function ListLayout({
pagination,
}: ListLayoutProps) {
const [searchValue, setSearchValue] = useState('')
const [pageViews, setPageViews] = useState<Record<string, number | undefined>>({})

const filteredBlogPosts = posts.filter((post) => {
const searchContent = post.title + post.summary + post.tags?.join(' ')
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
})

useEffect(() => {
posts.forEach((post) => {
const slug = post.slug
if (slug && !(slug in pageViews)) {
// Assume undefined means loading
setPageViews((prevPageViews) => ({
...prevPageViews,
[slug]: undefined,
}))

fetch(`/api/pageviews?slug=${encodeURIComponent(slug)}`)
.then((response) => response.json())
.then((data) => {
setPageViews((prevPageViews) => ({
...prevPageViews,
[slug]: data.pageViewCount,
}))
})
.catch((error) => console.error('Error fetching page views:', error))
}
})
}, [posts])

// If initialDisplayPosts exist, display it if no searchValue is specified
const displayPosts =
initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
Expand Down Expand Up @@ -106,7 +132,8 @@ export default function ListLayout({
<ul>
{!filteredBlogPosts.length && 'No posts found.'}
{displayPosts.map((post) => {
const { path, date, title, summary, tags, thumbnail } = post
const { slug, path, date, title, summary, tags, thumbnail } = post
const isLoadingViewCount = pageViews[slug] === undefined
return (
<li key={path} className="py-4">
<article className="space-y-2 xl:grid xl:grid-cols-5 xl:gap-4 xl:items-start xl:space-y-0">
Expand Down Expand Up @@ -137,7 +164,20 @@ export default function ListLayout({
<div>
<dl>
<dt className="sr-only">Published on</dt>
<dd className="text-base font-medium leading-6 text-muted-foreground">
<dd className="flex gap-1 text-base font-medium leading-6 text-muted-foreground">
{isLoadingViewCount ? (
<span className="flex items-center justify-center gap-2">
<Skeleton className="w-12 h-6" />
<span> views</span>
</span>
) : (
<span>
{pageViews[slug]?.toLocaleString() ||
'...'}{' '}
views
</span>
)}
<span></span>
<time dateTime={date}>
{formatDate(date, siteMetadata.locale)}
</time>
Expand Down
42 changes: 40 additions & 2 deletions layouts/PostSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { Authors, Blog } from 'contentlayer/generated'
import NextImage from 'next/image'
import { CoreContent } from 'pliny/utils/contentlayer'
import { formatDate } from 'pliny/utils/formatDate'
import { ReactNode, useState } from 'react'
import { ReactNode, useEffect, useState } from 'react'

interface LayoutProps {
content: CoreContent<Blog>
Expand All @@ -25,6 +25,31 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
const { path, slug, tags, date, title, thumbnail } = content
const displayThumbnail = thumbnail ? thumbnail : '/static/images/twitter-card.png'
const [isLoading, setIsLoading] = useState(true)
const [pageViews, setPageViews] = useState({
isLoading: true,
count: null,
})

useEffect(() => {
setPageViews((prev) => ({ ...prev, isLoading: true }))
if (slug) {
fetch(`/api/pageviews?slug=${encodeURIComponent(slug)}`)
.then((response) => response.json())
.then((data) => {
setPageViews({
isLoading: false,
count: data.pageViewCount.toLocaleString(),
})
})
.catch((error) => {
console.error('Error fetching page views:', error)
setPageViews({
isLoading: false,
count: null,
})
})
}
}, [slug])

return (
<>
Expand Down Expand Up @@ -56,7 +81,20 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
<dl className="relative pt-10">
<div>
<dt className="sr-only">Published on</dt>
<dd className="text-base font-medium leading-6 text-muted-foreground">
<dd className="flex items-center justify-center text-base font-medium leading-6 text-muted-foreground">
{pageViews !== null && (
<span className="text-muted-foreground">
{pageViews.isLoading ? (
<span className="flex items-center justify-center gap-2">
<Skeleton className="w-12 h-6" />
<span> views</span>
</span>
) : (
`${pageViews.count} views`
)}
</span>
)}
<span className="mx-2"></span>
<time dateTime={date}>
{formatDate(date, siteMetadata.locale)}
</time>
Expand Down

0 comments on commit 21cc1aa

Please sign in to comment.