diff --git a/personal-dashboard/package-lock.json b/personal-dashboard/package-lock.json index 478f730..d563f1d 100644 --- a/personal-dashboard/package-lock.json +++ b/personal-dashboard/package-lock.json @@ -8,6 +8,8 @@ "name": "personal-dashboard", "version": "0.1.0", "dependencies": { + "@upstash/redis": "^1.28.4", + "date-fns": "^3.4.0", "next": "14.1.3", "react": "^18", "react-dom": "^18" @@ -626,6 +628,14 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@upstash/redis": { + "version": "1.28.4", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.28.4.tgz", + "integrity": "sha512-UalkSAny/dz1m8giEhD3Y5ru1o+CPHI32wFyS3MyzDzj2TRvEN+lTw+mPwi20ojk0H2gs8TBW3qsrvwuLLy+pA==", + "dependencies": { + "crypto-js": "^4.2.0" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1236,6 +1246,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1260,6 +1275,15 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-fns": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.4.0.tgz", + "integrity": "sha512-Akz4R8J9MXBsOgF1QeWeCsbv6pntT5KCPjU0Q9prBxVmWJYPLhwAIsNg3b0QAdr0ttiozYLD3L/af7Ra0jqYXw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/personal-dashboard/package.json b/personal-dashboard/package.json index 3dea56e..ce6ce99 100644 --- a/personal-dashboard/package.json +++ b/personal-dashboard/package.json @@ -9,19 +9,21 @@ "lint": "next lint" }, "dependencies": { + "@upstash/redis": "^1.28.4", + "date-fns": "^3.4.0", + "next": "14.1.3", "react": "^18", - "react-dom": "^18", - "next": "14.1.3" + "react-dom": "^18" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.3", "postcss": "^8", "tailwindcss": "^3.3.0", - "eslint": "^8", - "eslint-config-next": "14.1.3" + "typescript": "^5" } } diff --git a/personal-dashboard/src/app/analytics/page.tsx b/personal-dashboard/src/app/analytics/page.tsx new file mode 100644 index 0000000..e06f3dc --- /dev/null +++ b/personal-dashboard/src/app/analytics/page.tsx @@ -0,0 +1,5 @@ +const Page = ()=>{ +return

Hello world

+} + +export default Page; \ No newline at end of file diff --git a/personal-dashboard/src/app/globals.css b/personal-dashboard/src/app/globals.css index 875c01e..b5c61c9 100644 --- a/personal-dashboard/src/app/globals.css +++ b/personal-dashboard/src/app/globals.css @@ -1,33 +1,3 @@ @tailwind base; @tailwind components; @tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} diff --git a/personal-dashboard/src/app/layout.tsx b/personal-dashboard/src/app/layout.tsx index 3314e47..6ef6e52 100644 --- a/personal-dashboard/src/app/layout.tsx +++ b/personal-dashboard/src/app/layout.tsx @@ -16,7 +16,7 @@ export default function RootLayout({ }>) { return ( - {children} + {children} ); } diff --git a/personal-dashboard/src/lib/redis.ts b/personal-dashboard/src/lib/redis.ts new file mode 100644 index 0000000..8666e60 --- /dev/null +++ b/personal-dashboard/src/lib/redis.ts @@ -0,0 +1,10 @@ +// NOTE: @about 17:00 - minutes + +import { Redis } from "@upstash/redis"; + +export const redis = new Redis({ + url: "https://us1-sharing-oryx-41733.upstash.io", + token: process.env.REDIS_KEY!, +}); + +// const data = await redis.set("foo", "bar"); diff --git a/personal-dashboard/src/middleware.ts b/personal-dashboard/src/middleware.ts new file mode 100644 index 0000000..2c4dec6 --- /dev/null +++ b/personal-dashboard/src/middleware.ts @@ -0,0 +1,28 @@ +// NOTE: @about 6:00 minutes +import { NextRequest, NextResponse } from "next/server"; +import { analytics } from "./utils/analytics"; + +export default async function middleware(req: NextRequest) { + // Anytime the url is `http://localhost:3000/`, `req.nextUrl.pathname === "/"` will evaluates to `true`, it is truthy. + if (req.nextUrl.pathname === "/") { + // track analytics event + // console.log("Tracking"); + // NOTE: @about ?:00 - minutes + try { + // what are we going to track? + // pageview is our namespace that we are going to track + // namespace fancy word for event name + analytics.track("pageview", { page: "/", country: req.geo?.country }); + } catch (error) { + // fail silently + console.log(error); + } + } + return NextResponse.next(); +} + +// See NextJS documentation for why this is needed. +// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher +export const matcher = { + matcher: ["/"], +}; diff --git a/personal-dashboard/src/utils/analytics.ts b/personal-dashboard/src/utils/analytics.ts new file mode 100644 index 0000000..b252998 --- /dev/null +++ b/personal-dashboard/src/utils/analytics.ts @@ -0,0 +1,35 @@ +// NOTE: @about 11:00 - minutes + +import { redis } from "@/lib/redis"; +import { getDate } from "@/utils/"; + +type AnalyticsArgs = { + retention?: number; +}; + +type TrackOptions = { persist?: boolean }; + +export class Analytics { + private retention: number = 60 * 60 * 24 * 7; + + constructor(opts?: AnalyticsArgs) { + if (opts?.retention) this.retention = opts.retention; + } + + async track(namespace: string, event: object = {}, opts?: TrackOptions) { + let key = `analytics::${namespace}`; + + if (!opts?.persist) { + key += `::${getDate()}`; + } + + // db call to persist this event + await redis.hincrby(key, JSON.stringify(event), 1); + + if (!opts?.persist) await redis.expire(key, this.retention); + } +} + +export const analytics = new Analytics(); + +// analytics.track() diff --git a/personal-dashboard/src/utils/index.ts b/personal-dashboard/src/utils/index.ts new file mode 100644 index 0000000..686c11f --- /dev/null +++ b/personal-dashboard/src/utils/index.ts @@ -0,0 +1,9 @@ +// NOTE: @about 25:00 - minutes + +import { format, subDays } from "date-fns"; + +export const getDate = (sub: number = 0) => { + const dateXDaysAgo = subDays(new Date(), sub); + + return format(dateXDaysAgo, "dd/MM/yyyy"); +};