mirror of
https://github.com/r-freeman/portfolio.git
synced 2024-11-22 12:25:41 +00:00
Merge branch 'main' of github.com:r-freeman/portfolio
This commit is contained in:
commit
2942dd2fad
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||
|
@ -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}`,
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
490
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
83
styles/nprogress.css
Normal 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
215
types/database.types.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user