Skip to content

Commit

Permalink
Merge pull request #141 from jsjoeio/add-page-views
Browse files Browse the repository at this point in the history
feat: add page views
  • Loading branch information
jsjoeio authored Jun 1, 2020
2 parents a2734de + 83c0f3a commit 6535565
Show file tree
Hide file tree
Showing 18 changed files with 2,328 additions and 111 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,12 @@ public
.env
.env.development
.env.production

# firebase
service-account.json
serviceAccountKey.json

# netlify funcs
functions-build
# Local Netlify folder
.netlify
9 changes: 9 additions & 0 deletions config/webpack.functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// required to get firebase-sdk imported into netlify functions
// see: https://github.com/netlify/netlify-lambda/issues/112#issuecomment-488644361
const nodeExternals = require('webpack-node-externals')
const dotEnv = require('dotenv-webpack')

module.exports = {
externals: [nodeExternals()],
plugins: [new dotEnv()],
}
30 changes: 30 additions & 0 deletions functions/increment-views.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { db } from '../src/lib/db-admin'

exports.handler = async (event, context) => {
const { id } = event.queryStringParameters
if (!id) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Missing "id" query parameter',
}),
}
}

const ref = db.ref('joeprevite-dot-com/views').child(id)
const { snapshot } = await ref.transaction(currentViews => {
if (currentViews === null) {
return 1
}

return currentViews + 1
})

return {
statusCode: 200,
body: JSON.stringify({
pageId: id,
totalViews: snapshot.val(),
}),
}
}
39 changes: 39 additions & 0 deletions functions/page-views.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { db } from '../src/lib/db-admin'

exports.handler = async (event, context) => {
const { id } = event.queryStringParameters

if (!id) {
let totalViews
await db.ref('joeprevite-dot-com/views').once('value', snapshot => {
const views = snapshot.val()
// Grabs all the views for all posts
const allViews = Object.values(views).reduce(
(total, value) => total + value,
)
totalViews = allViews
})
return {
statusCode: 200,
body: JSON.stringify({
totalViews: totalViews || 0,
}),
}
}

const ref = db.ref('joeprevite-dot-com/views').child(id)

let totalViews
await ref.once('value', snapshot => {
const value = snapshot.val()
totalViews = value
})

return {
statusCode: 200,
body: JSON.stringify({
pageId: id,
totalViews: totalViews !== null ? totalViews : 0,
}),
}
}
12 changes: 12 additions & 0 deletions gatsby-config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
const config = require('./config/website')
const pathPrefix = config.pathPrefix === '/' ? '' : config.pathPrefix
const { createProxyMiddleware } = require('http-proxy-middleware')

require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
})

module.exports = {
developMiddleware: app => {
app.use(
'/.netlify/functions/',
createProxyMiddleware({
target: 'http://localhost:9000',
pathRewrite: {
'/.netlify/functions/': '',
},
}),
)
},
pathPrefix: config.pathPrefix,
siteMetadata: {
siteUrl: config.siteUrl + pathPrefix,
Expand Down
2 changes: 2 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[build]
command = "yarn build"
functions = "functions-build"
publish = "public"

[[plugins]]
Expand Down
16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
"@mdx-js/mdx": "^1.5.1",
"@mdx-js/react": "^1.5.5",
"@weknow/gatsby-remark-twitter": "^0.2.3",
"comma-number": "^2.0.1",
"core-js": "^3.3.3",
"dotenv": "^8.2.0",
"dotenv-parse-variables": "^0.2.3",
"dotenv-webpack": "^1.8.0",
"emotion": "^10.0.17",
"emotion-server": "^10.0.17",
"emotion-theming": "^10.0.19",
"firebase": "^7.14.6",
"firebase-admin": "^8.12.1",
"formik": "^2.0.8",
"framer-motion": "^1.10.3",
"gatsby": "^2.21.9",
Expand Down Expand Up @@ -60,14 +64,19 @@
"react-share": "^4.0.1",
"react-typography": "^0.16.19",
"serve": "^11.2.0",
"swr": "^0.2.2",
"typescript": "^3.8.3",
"typography": "^0.16.19",
"webpack-node-externals": "^1.7.2",
"yup": "^0.28.0"
},
"devDependencies": {
"babel-preset-gatsby": "^0.2.20",
"dayjs": "^1.8.18",
"http-proxy-middleware": "^1.0.4",
"hygen": "^5.0.3",
"netlify-lambda": "^1.6.3",
"npm-run-all": "^4.1.5",
"open": "^7.0.3",
"prettier": "1.19.1"
},
Expand All @@ -77,10 +86,13 @@
"license": "MIT",
"main": "n/a",
"scripts": {
"build": "GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build --log-pages",
"build": "GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build --log-pages && yarn build:lambda",
"build:lambda": "netlify-lambda build --config ./config/webpack.functions.js functions",
"build:local": "gatsby build",
"g:post": "hygen post new",
"dev": "gatsby develop",
"start": "serve public/"
"start": "run-p start:**",
"start:app": "npm run dev",
"start:lambda": "netlify-lambda serve --config ./config/webpack.functions.js functions"
}
}
3 changes: 3 additions & 0 deletions src/components/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ const getGlobalStyles = theme => {
summary {
display: list-item;
}
small {
color: ${lighten(0.35, theme.colors.black)};
}
`
}

Expand Down
109 changes: 109 additions & 0 deletions src/components/Post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react'
import useSWR from 'swr'
import format from 'comma-number'

import { css } from '@emotion/core'
import { bpMaxSM } from '../lib/breakpoints'
import Link from './Link'
import fetcher from '../lib/fetcher'

function Post({ post }) {
const { data } = useSWR(
`/.netlify/functions/page-views?id=${post.fields.slug}`,
fetcher,
)

const views = data?.totalViews

return (
<div
css={css`
:not(:first-of-type) {
margin-top: 10px;
}
:first-of-type {
margin-top: 10px;
${bpMaxSM} {
margin-top: 10px;
}
}
.gatsby-image-wrapper {
}
${bpMaxSM} {
padding: 20px;
}
display: flex;
flex-direction: column;
`}
>
{/* {post.frontmatter.banner && (
<div
css={css`
padding: 60px 60px 40px 60px;
${bpMaxSM} {
padding: 20px;
}
`}
>
<Link
aria-label={`View ${post.frontmatter.title} article`}
to={`/${post.fields.slug}`}
>
<Img sizes={post.frontmatter.banner.childImageSharp.fluid} />
</Link>
</div>
)} */}
<div
css={css`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
h2,
small {
margin-top: 30px;
margin-bottom: 10px;
}
small {
margin-left: 10px;
white-space: nowrap;
}
${bpMaxSM} {
flex-wrap: wrap;
small {
margin: 0;
}
}
`}
>
<h2>
<Link
aria-label={`View ${post.frontmatter.title} article`}
to={`/${post.fields.slug}`}
>
{post.frontmatter.title}
</Link>
</h2>
<small>{`${
views !== null && views !== undefined ? format(views) : '–––'
} views`}</small>
</div>
<p
css={css`
margin-top: 10px;
`}
>
{post.excerpt}
</p>{' '}
<Link
to={`/${post.fields.slug}`}
aria-label={`view "${post.frontmatter.title}" article`}
>
Read Article →
</Link>
</div>
)
}

export default Post
44 changes: 44 additions & 0 deletions src/components/ViewCounter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* credit to @leerob who wrote this
* @see https://github.com/leerob/leerob.io/blob/master/components/ViewCounter.js
*/
import React, { useState, useEffect } from 'react'
import format from 'comma-number'

import { loadDb } from '../lib/db'

const ViewCounter = ({ id }) => {
const [views, setViews] = useState('')

useEffect(() => {
const onViews = newViews => setViews(newViews.val())

let db

const fetchData = async () => {
db = await loadDb()
db.child(id).on('value', onViews, error => {
console.error('Error reading value', error)
})
}

fetchData()

return () => {
if (db) {
db.child(id).off('value', onViews)
}
}
}, [id])

useEffect(() => {
const registerView = () =>
fetch(`/.netlify/functions/increment-views?id=${id}`)

registerView()
}, [id])

return <p>{`${views ? format(views) : '–––'} views`}</p>
}

export default ViewCounter
27 changes: 27 additions & 0 deletions src/lib/db-admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* credit to @leerob who wrote this
* @see https://github.com/leerob/leerob.io/blob/master/lib/db-admin.js
*/
const admin = require('firebase-admin')

try {
admin.initializeApp({
credential: admin.credential.cert({
client_email: process.env.FIREBASE_CLIENT_EMAIL,
private_key: process.env.FIREBASE_PRIVATE_KEY,
project_id: 'website-pageviews-c8d4d',
}),
databaseURL: 'https://website-pageviews-c8d4d.firebaseio.com/',
})
} catch (error) {
/*
* We skip the "already exists" message which is
* not an actual error when we're hot-reloading.
*/
if (!/already exists/u.test(error.message)) {
// eslint-disable-next-line no-console
console.error('Firebase admin initialization error', error.stack)
}
}

export const db = admin.database()
26 changes: 26 additions & 0 deletions src/lib/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* credit to @leerob who wrote this
* @see https://github.com/leerob/leerob.io/blob/master/lib/db.js
*/
export async function loadDb() {
const firebase = await import('firebase/app')

await import('firebase/database')

try {
firebase.initializeApp({
databaseURL: 'https://website-pageviews-c8d4d.firebaseio.com/',
})
} catch (error) {
/*
* We skip the "already exists" message which is
* not an actual error when we're hot-reloading.
*/
if (!/already exists/u.test(error.message)) {
// eslint-disable-next-line no-console
console.error('Firebase initialization error', error.stack)
}
}

return firebase.database().ref('joeprevite-dot-com/views')
}
Loading

0 comments on commit 6535565

Please sign in to comment.