mirror of
https://github.com/r-freeman/portfolio.git
synced 2024-11-11 18:45:41 +00:00
Added real-time page views
This commit is contained in:
parent
1fabd19a53
commit
51737338b8
@ -18,8 +18,6 @@ type ArticleLayout = {
|
||||
slug: string
|
||||
}
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
export function ArticleLayout({
|
||||
children,
|
||||
isRssFeed = false,
|
||||
@ -109,7 +107,7 @@ export function ArticleLayout({
|
||||
<time dateTime={date}>
|
||||
<span>{formatDate(date)}</span>
|
||||
</time>
|
||||
<Views slug={slug} shouldUpdateViews={isProd}/>
|
||||
<Views slug={slug} shouldUpdateViews={true}/>
|
||||
</p>
|
||||
</header>
|
||||
<Prose className="mt-8">{children}</Prose>
|
||||
|
@ -1,11 +1,7 @@
|
||||
import useSWR from 'swr'
|
||||
import {ElementType, useEffect} from 'react'
|
||||
import {ElementType, useEffect, useState} from 'react'
|
||||
import fetcher from '@/lib/fetcher'
|
||||
import {numberFormat} from '@/lib/numberFormat'
|
||||
|
||||
type ViewsResponse = {
|
||||
views: string
|
||||
}
|
||||
import {supabase} from '@/lib/supabase'
|
||||
|
||||
type ViewsProps = {
|
||||
as?: ElementType
|
||||
@ -14,20 +10,52 @@ type ViewsProps = {
|
||||
shouldUpdateViews?: boolean
|
||||
}
|
||||
|
||||
const updateViews = (slug: string) => fetcher(`/api/views/${slug}`, {method: 'POST'})
|
||||
const NEXT_PUBLIC_SITE_URL = process.env.NEXT_PUBLIC_SITE_URL
|
||||
|
||||
export function Views({as: Component = 'span', slug, className, shouldUpdateViews = true}: ViewsProps) {
|
||||
const {data} = useSWR<ViewsResponse>(`/api/views/${slug}`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnMount: true
|
||||
export function Views({as: Component = 'span', slug, className, shouldUpdateViews = false}: ViewsProps) {
|
||||
const [views, setViews] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = supabase
|
||||
.channel('any')
|
||||
.on('postgres_changes', {
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'analytics',
|
||||
filter: `slug=eq.${slug}`
|
||||
}, payload => {
|
||||
setViews(payload.new.views)
|
||||
})
|
||||
const views = Number(data?.views)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const getViews = async () => {
|
||||
const {views} = await fetcher(`${NEXT_PUBLIC_SITE_URL}/api/views/${slug}`)
|
||||
|
||||
setViews(views)
|
||||
};
|
||||
|
||||
getViews()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldUpdateViews) {
|
||||
updateViews(slug).then(r => r);
|
||||
const registerView = async () => {
|
||||
const {views} = await fetcher(`${NEXT_PUBLIC_SITE_URL}/api/views/${slug}`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
}, [slug, shouldUpdateViews])
|
||||
)
|
||||
};
|
||||
|
||||
registerView()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Component className={className}>
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@/lib/github'
|
||||
import {getAllArticles} from '@/lib/getAllArticles'
|
||||
import {getTopArtist, getTopGenre} from '@/lib/spotify'
|
||||
import {getViews} from '@/lib/views'
|
||||
import {getStats} from '@/lib/statsfm'
|
||||
import {Metric} from '@/types'
|
||||
|
||||
@ -21,7 +20,7 @@ export async function getDashboardData() {
|
||||
const totalStars = await getTotalStars(totalRepos)
|
||||
const totalForks = await getTotalForks(totalRepos)
|
||||
const totalArticles = (await getAllArticles()).length
|
||||
const totalArticleViews = (await getViews()).views
|
||||
// const totalArticleViews = (await getViews()).views
|
||||
const topArtist = await getTopArtist()
|
||||
const {genre} = await getTopGenre()
|
||||
const {hoursListened, minutesListened, streams} = await getStats()
|
||||
@ -93,12 +92,12 @@ export async function getDashboardData() {
|
||||
group: "Blog",
|
||||
href: "/writing"
|
||||
},
|
||||
{
|
||||
title: "Total article views",
|
||||
value: +totalArticleViews,
|
||||
group: "Blog",
|
||||
href: "/writing"
|
||||
}
|
||||
// {
|
||||
// title: "Total article views",
|
||||
// value: +totalArticleViews,
|
||||
// group: "Blog",
|
||||
// href: "/writing"
|
||||
// }
|
||||
]
|
||||
|
||||
// sort metrics into named groups
|
||||
|
@ -1,11 +0,0 @@
|
||||
import {PrismaClient} from '@prisma/client'
|
||||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient }
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ||
|
||||
new PrismaClient({
|
||||
log: ['query']
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
6
lib/supabase.ts
Normal file
6
lib/supabase.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {createClient} from '@supabase/supabase-js'
|
||||
|
||||
export const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ""
|
||||
)
|
13
lib/views.ts
13
lib/views.ts
@ -1,13 +0,0 @@
|
||||
import {prisma} from '@/lib/prisma'
|
||||
|
||||
export async function getViews() {
|
||||
const totalViews: { _sum: { count: any } } = await prisma.views.aggregate({
|
||||
_sum: {
|
||||
count: true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
views: totalViews._sum.count.toString()
|
||||
}
|
||||
}
|
1137
package-lock.json
generated
1137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -12,9 +12,8 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@mapbox/rehype-prism": "^0.7.0",
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@next/mdx": "^13.1.1",
|
||||
"@prisma/client": "^4.12.0",
|
||||
"@supabase/supabase-js": "^2.15.0",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/mdx": "^2.0.3",
|
||||
"@types/node": "^18.11.18",
|
||||
@ -23,22 +22,20 @@
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-config-next": "^13.2.0",
|
||||
"eslint-config-next": "^13.3.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"feed": "^4.2.2",
|
||||
"focus-visible": "^5.2.0",
|
||||
"motion": "^10.15.5",
|
||||
"next": "^13.2.0",
|
||||
"next-auth": "^4.21.1",
|
||||
"next": "^13.3.0",
|
||||
"node-fetch": "^3.3.0",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-focus-visible": "^7.1.0",
|
||||
"prisma": "^4.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.3",
|
||||
"swr": "^2.0.0",
|
||||
"swr": "^2.1.2",
|
||||
"tailwind-merge": "^1.9.0",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"ts-node": "^10.9.1",
|
||||
|
@ -1,18 +0,0 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import GithubProvider from 'next-auth/providers/github'
|
||||
import {PrismaAdapter} from '@next-auth/prisma-adapter'
|
||||
import {PrismaClient} from '@prisma/client'
|
||||
|
||||
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID ?? ""
|
||||
const GITHUB_SECRET = process.env.GITHUB_SECRET ?? ""
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
adapter: PrismaAdapter(prisma),
|
||||
providers: [
|
||||
GithubProvider({
|
||||
clientId: GITHUB_CLIENT_ID,
|
||||
clientSecret: GITHUB_SECRET
|
||||
})
|
||||
],
|
||||
})
|
@ -1,47 +1,32 @@
|
||||
import type {NextApiRequest, NextApiResponse} from 'next'
|
||||
import {prisma} from '@/lib/prisma'
|
||||
import {NextApiRequest, NextApiResponse} from 'next'
|
||||
import {supabase} from '@/lib/supabase'
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (req.method === 'POST') {
|
||||
if (req.query.slug !== undefined) {
|
||||
const slug: string = req.query.slug.toString()
|
||||
await supabase.rpc('increment_views', {page_slug: slug})
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const newOrUpdatedViews = await prisma.views.upsert({
|
||||
where: {slug},
|
||||
create: {
|
||||
slug
|
||||
},
|
||||
update: {
|
||||
count: {
|
||||
increment: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
views: newOrUpdatedViews.count.toString()
|
||||
})
|
||||
return res.status(200).json({})
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const views = await prisma.views.findUnique({
|
||||
where: {
|
||||
slug
|
||||
}
|
||||
})
|
||||
return res.status(400).json({})
|
||||
} else if (req.method === 'GET') {
|
||||
if (req.query.slug !== undefined) {
|
||||
const slug: string = req.query.slug.toString()
|
||||
const response = await supabase
|
||||
.from('analytics')
|
||||
.select('views')
|
||||
.eq('slug', slug)
|
||||
.returns<any>()
|
||||
|
||||
let count = null
|
||||
if (views !== null) {
|
||||
count = views.count.toString()
|
||||
const {views} = response.data[0]
|
||||
|
||||
return res.status(200).json({views})
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
views: count
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
return res.status(500).json({message: e.message})
|
||||
return res.status(400).json({})
|
||||
}
|
||||
|
||||
return res.status(405).json({})
|
||||
}
|
@ -28,7 +28,7 @@ function Article(article: Article) {
|
||||
<Card.Eyebrow as="time" dateTime={article.date} decorate={false}>
|
||||
{formatDate(article.date)}
|
||||
</Card.Eyebrow>
|
||||
<Views slug={article.slug} shouldUpdateViews={false} className="text-sm text-zinc-500 dark:text-zinc-400"/>
|
||||
<Views slug={article.slug} className="text-sm text-zinc-500 dark:text-zinc-400"/>
|
||||
</p>
|
||||
</Card>
|
||||
</article>
|
||||
|
@ -19,7 +19,7 @@ function Article({article}: { article: Article }) {
|
||||
<Card.Eyebrow as="time" dateTime={article.date} decorate={false}>
|
||||
{formatDate(article.date)}
|
||||
</Card.Eyebrow>
|
||||
<Views slug={article.slug} shouldUpdateViews={false} className="text-sm text-zinc-500 dark:text-zinc-400"/>
|
||||
<Views slug={article.slug} className="text-sm text-zinc-500 dark:text-zinc-400"/>
|
||||
</p>
|
||||
</Card>
|
||||
</article>
|
||||
|
@ -1,69 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["multiSchema"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
schemas = ["public", "auth"]
|
||||
}
|
||||
|
||||
model Views {
|
||||
slug String @id
|
||||
count BigInt @default(1)
|
||||
@@schema("public")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@schema("auth")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
@@schema("auth")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
@@schema("auth")
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@schema("auth")
|
||||
}
|
Loading…
Reference in New Issue
Block a user