Merge branch 'main' of github.com:r-freeman/portfolio

This commit is contained in:
r-freeman 2023-04-13 17:37:29 +01:00
commit 2942dd2fad
12 changed files with 838 additions and 57 deletions

3
.gitignore vendored
View File

@ -1,9 +1,10 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
prisma/**/*
!prisma/schema.prisma
.env
supabase/
.idea/
public/rss

View File

@ -1,8 +1,8 @@
import {useSupabaseClient} from '@supabase/auth-helpers-react'
import {ElementType, useEffect} from 'react'
import useSWR, {useSWRConfig} from 'swr'
import fetcher from '@/lib/fetcher'
import {numberFormat} from '@/lib/numberFormat'
import {supabase} from '@/lib/supabase'
import useSWR, {useSWRConfig} from 'swr'
type ViewsProps = {
as?: ElementType
@ -12,16 +12,15 @@ type ViewsProps = {
shouldRender?: boolean
}
const isProd = process.env.NODE_ENV === 'production'
export function Views({as: Component = 'span', slug, className, shouldUpdateViews = true, shouldRender = true}: ViewsProps) {
const supabaseClient = useSupabaseClient()
const {data} = useSWR(`/api/views/${slug}`, fetcher) as { data: { views: number } }
const {mutate} = useSWRConfig()
useEffect(() => {
if (shouldUpdateViews) {
// subscribe to analytics table and react to updates at row level
const sub = supabase
const sub = supabaseClient
.channel('any')
.on('postgres_changes', {
event: 'UPDATE',
@ -40,7 +39,7 @@ export function Views({as: Component = 'span', slug, className, shouldUpdateView
}, [])
useEffect(() => {
if (shouldUpdateViews && isProd) {
if (shouldUpdateViews) {
const registerView = async () => {
await fetcher(`/api/views/${slug}`,
{

View File

@ -1,3 +1,5 @@
import {GetServerSidePropsContext} from 'next'
import {createServerSupabaseClient} from '@supabase/auth-helpers-nextjs'
import {
getTopRepo,
getTotalFollowers,
@ -10,7 +12,9 @@ import {getTopArtist, getTopGenre} from '@/lib/spotify'
import {getStats} from '@/lib/statsfm'
import {Metric} from '@/types'
export async function getDashboardData() {
export async function getDashboardData(context: GetServerSidePropsContext) {
const supabaseClient = createServerSupabaseClient(context)
const {data: views} = await supabaseClient.rpc('total_views')
const [totalRepos, totalFollowers] = await Promise.all([
getTotalRepos(),
getTotalFollowers()
@ -20,7 +24,6 @@ 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 topArtist = await getTopArtist()
const {genre} = await getTopGenre()
const {hoursListened, minutesListened, streams} = await getStats()
@ -92,12 +95,12 @@ export async function getDashboardData() {
group: "Blog",
href: "/writing"
},
// {
// title: "Total article views",
// value: +totalArticleViews,
// group: "Blog",
// href: "/writing"
// }
{
title: "Total article views",
value: +views,
group: "Blog",
href: "/"
}
]
// sort metrics into named groups

View File

@ -1,9 +0,0 @@
import {createClient} from '@supabase/supabase-js'
const NEXT_PUBLIC_SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ""
const NEXT_PUBLIC_SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ""
export const supabase = createClient(
NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY
)

490
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,10 +13,12 @@
"@headlessui/react": "^1.7.7",
"@mapbox/rehype-prism": "^0.7.0",
"@next/mdx": "^13.1.1",
"@supabase/supabase-js": "^2.15.0",
"@supabase/auth-helpers-nextjs": "^0.6.0",
"@supabase/auth-helpers-react": "^0.3.1",
"@tailwindcss/typography": "^0.5.8",
"@types/mdx": "^2.0.3",
"@types/node": "^18.11.18",
"@types/nprogress": "^0.2.0",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"autoprefixer": "^10.4.13",
@ -29,6 +31,7 @@
"motion": "^10.15.5",
"next": "^13.3.0",
"node-fetch": "^3.3.0",
"nprogress": "^0.2.0",
"postcss": "^8.4.21",
"postcss-focus-visible": "^7.1.0",
"react": "^18.2.0",
@ -40,5 +43,8 @@
"tailwindcss": "^3.3.0",
"ts-node": "^10.9.1",
"typescript": "4.9.4"
},
"devDependencies": {
"supabase": "^1.50.2"
}
}

View File

@ -1,13 +1,38 @@
import type {AppProps} from 'next/app'
import {useRouter} from 'next/router'
import NProgress from 'nprogress'
import {createBrowserSupabaseClient} from '@supabase/auth-helpers-nextjs'
import {SessionContextProvider, Session} from '@supabase/auth-helpers-react'
import {useEffect, useState} from 'react'
import {Header} from '@/components/Header'
import {Footer} from '@/components/Footer'
import '../styles/nprogress.css'
import '../styles/tailwind.css'
import 'focus-visible'
export default function App({Component, pageProps}: AppProps) {
export default function App({Component, pageProps}: AppProps<{ initialSession: Session }>) {
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
const router = useRouter()
NProgress.configure({showSpinner: false})
useEffect(() => {
const handleRouteStart = () => NProgress.start()
const handleRouteDone = () => NProgress.done()
router.events.on("routeChangeStart", handleRouteStart)
router.events.on("routeChangeComplete", handleRouteDone)
router.events.on("routeChangeError", handleRouteDone)
return () => {
router.events.off("routeChangeStart", handleRouteStart)
router.events.off("routeChangeComplete", handleRouteDone)
router.events.off("routeChangeError", handleRouteDone)
}
})
return (
<>
<SessionContextProvider supabaseClient={supabaseClient} initialSession={pageProps.initialSession}>
<div className="fixed inset-0 flex justify-center sm:px-8">
<div className="flex w-full max-w-7xl lg:px-8">
<div className="w-full bg-white dark:bg-black-950 dark:ring-zinc-300/20"/>
@ -20,6 +45,6 @@ export default function App({Component, pageProps}: AppProps) {
</main>
<Footer/>
</div>
</>
</SessionContextProvider>
)
}

View File

@ -1,11 +1,18 @@
import {createServerSupabaseClient} from '@supabase/auth-helpers-nextjs'
import {NextApiRequest, NextApiResponse} from 'next'
import {supabase} from '@/lib/supabase'
import {Database} from '@/types/database.types'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const supabaseServerClient = createServerSupabaseClient<Database>({
req,
res,
})
if (req.method === 'POST') {
if (req.query.slug !== undefined) {
const slug: string = req.query.slug.toString()
await supabase.rpc('increment_views', {page_slug: slug})
// @ts-ignore
await supabaseServerClient.rpc('increment_views', {page_slug: slug})
return res.status(200).json({})
}
@ -14,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} else if (req.method === 'GET') {
if (req.query.slug !== undefined) {
const slug: string = req.query.slug.toString()
const response = await supabase
const response = await supabaseServerClient
.from('analytics')
.select('views')
.eq('slug', slug)

View File

@ -1,6 +1,6 @@
import React from 'react'
import Head from 'next/head'
import {GetStaticProps} from 'next'
import {GetServerSideProps} from 'next'
import useSWR from 'swr'
import {SimpleLayout} from '@/components/layouts/SimpleLayout'
import {Card} from '@/components/ui/Card'
@ -27,7 +27,7 @@ export default function Dashboard({metrics}: { metrics: MetricGroup }) {
<title>Dashboard - Ryan Freeman</title>
<meta
name="description"
content="This is my digital life in numbers which is updated daily. I use this dashboard to keep track of various metrics across platforms like Spotify, GitHub, Twitter and for monitoring the performance of my Raspberry Pi using Grafana and Prometheus."
content="This is my digital life in numbers, I use this dashboard to keep track of various metrics across platforms like Spotify, GitHub, Twitter and for monitoring the performance of my Raspberry Pi using Grafana and Prometheus."
/>
<meta
property="og:title"
@ -35,12 +35,12 @@ export default function Dashboard({metrics}: { metrics: MetricGroup }) {
/>
<meta
property="og:description"
content="This is my digital life in numbers which is updated daily. I use this dashboard to keep track of various metrics across platforms like Spotify, GitHub, Twitter and for monitoring the performance of my Raspberry Pi using Grafana and Prometheus."
content="This is my digital life in numbers, I use this dashboard to keep track of various metrics across platforms like Spotify, GitHub, Twitter and for monitoring the performance of my Raspberry Pi using Grafana and Prometheus."
/>
</Head>
<SimpleLayout
title="Dashboard."
intro="This is my digital life in numbers which is updated daily. I use this dashboard to keep track of various metrics across platforms like Spotify, GitHub, Twitter and for monitoring the performance of my Raspberry Pi using Grafana and Prometheus."
intro="This is my digital life in numbers, I use this dashboard to keep track of various metrics across platforms like Spotify, GitHub, Twitter and for monitoring the performance of my Raspberry Pi using Grafana and Prometheus."
gradient="bg-gradient-to-r from-orange-300 to-rose-300"
>
{metrics.map(({groupName, groupItems}) => (
@ -104,10 +104,10 @@ export default function Dashboard({metrics}: { metrics: MetricGroup }) {
)
}
export const getStaticProps: GetStaticProps = async () => {
export const getServerSideProps: GetServerSideProps = async (context) => {
return {
props: {
metrics: await getDashboardData()
metrics: await getDashboardData(context)
}
}
}

View File

@ -138,7 +138,6 @@ export default function Home({articles}: { articles: Article[] }) {
<Resume/>
</div>
</div>
<Views slug='home' shouldRender={false}/>
</Container>
</>
)

83
styles/nprogress.css Normal file
View File

@ -0,0 +1,83 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #4338ca;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #4338ca, 0 0 5px #4338ca;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #4338ca;
border-left-color: #4338ca;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

215
types/database.types.ts Normal file
View File

@ -0,0 +1,215 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json }
| Json[]
export interface Database {
graphql_public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
operationName?: string
query?: string
variables?: Json
extensions?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null
avif_autodetection: boolean | null
created_at: string | null
file_size_limit: number | null
id: string
name: string
owner: string | null
public: boolean | null
updated_at: string | null
}
Insert: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id: string
name: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
Update: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id?: string
name?: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
}
migrations: {
Row: {
executed_at: string | null
hash: string
id: number
name: string
}
Insert: {
executed_at?: string | null
hash: string
id: number
name: string
}
Update: {
executed_at?: string | null
hash?: string
id?: number
name?: string
}
}
objects: {
Row: {
bucket_id: string | null
created_at: string | null
id: string
last_accessed_at: string | null
metadata: Json | null
name: string | null
owner: string | null
path_tokens: string[] | null
updated_at: string | null
version: string | null
}
Insert: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Update: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
}
}
Views: {
[_ in never]: never
}
Functions: {
can_insert_object: {
Args: {
bucketid: string
name: string
owner: string
metadata: Json
}
Returns: undefined
}
extension: {
Args: {
name: string
}
Returns: string
}
filename: {
Args: {
name: string
}
Returns: string
}
foldername: {
Args: {
name: string
}
Returns: string[]
}
get_size_by_bucket: {
Args: Record<PropertyKey, never>
Returns: {
size: number
bucket_id: string
}[]
}
search: {
Args: {
prefix: string
bucketname: string
limits?: number
levels?: number
offsets?: number
search?: string
sortcolumn?: string
sortorder?: string
}
Returns: {
name: string
id: string
updated_at: string
created_at: string
last_accessed_at: string
metadata: Json
}[]
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}