diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..df4366fc --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +# ============================================================ +# Database +# ============================================================ +DATABASE_URL="" # Postgres Neon connection string + +# ============================================================ +# Better Auth +# ============================================================ +BETTER_AUTH_URL="" # Your app's base URL +BETTER_AUTH_SECRET="" # Secret key for auth (min 32 chars recommended) + +# ============================================================ +# GitHub OAuth (optional) +# ============================================================ +GITHUB_CLIENT_ID="" # Create OAuth app at https://github.com/settings/developers +GITHUB_CLIENT_SECRET="" # Retrieved from GitHub OAuth app settings + +# ============================================================ +# Google OAuth (optional) +# ============================================================ +GOOGLE_CLIENT_ID="" # Create OAuth app at https://console.cloud.google.com/apis/credentials +GOOGLE_CLIENT_SECRET="" # Retrieved from Google Cloud Console + +# ============================================================ +# Admin +# ============================================================ +ADMIN_EMAIL="" # Admin email for notifications + +# ============================================================ +# Resend (Email, optional) +# ============================================================ +RESEND_API_KEY="" # API key from https://resend.com +RESEND_EMAIL_FROM="" # Verified sender email + +# ============================================================ +# PostHog Analytics (optional) +# ============================================================ +NEXT_PUBLIC_POSTHOG_KEY="" # Project API key from PostHog +NEXT_PUBLIC_POSTHOG_HOST="" # PostHog host URL (e.g., https://app.posthog.com) + +# ============================================================ +# GitHub (for public profile data) +# ============================================================ +NEXT_PUBLIC_GITHUB_TOKEN="" # Personal access token for GitHub API + +# ============================================================ +# Spotify OAuth (optional) +# ============================================================ +SPOTIFY_CLIENT_ID="" # Create OAuth app at https://developer.spotify.com/dashboard +SPOTIFY_CLIENT_SECRET="" # Retrieved from Spotify app settings +SPOTIFY_REFRESH_TOKEN="" # Needed for server-side syncing without user login +SPOTIFY_REDIRECT_URI="http://127.0.0.1:3000/api/auth/spotify/callback" + +# ============================================================ +# IP info service +# ============================================================ +IP_INFO_TOKEN="" # Token from https://ipinfo.io + +# ============================================================ +# Vercel Cron +# ============================================================ +CRON_SECRET="" # Secret key for cron jobs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5cb102b9..0eef188c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.env*.local +setup-posthog.md +src/app/blog/posts/productivity/mac-to-linux.mdx +src/app/blog/posts/productivity/mac-to-linux.mdx +.env.production diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.posthog-events.json b/.posthog-events.json new file mode 100644 index 00000000..31a898b5 --- /dev/null +++ b/.posthog-events.json @@ -0,0 +1,67 @@ +[ + { + "eventName": "sign_in_initiated", + "eventDescription": "User initiated sign in with OAuth provider (GitHub or Google)", + "filePath": "src/components/auth/oauth-modal.tsx" + }, + { + "eventName": "sign_in_completed", + "eventDescription": "User successfully completed sign in via OAuth", + "filePath": "src/components/blog/sign-in-button.tsx" + }, + { + "eventName": "blog_post_reaction", + "eventDescription": "User reacted to a blog post with an emoji (fire, heart, clap, thinking, rocket)", + "filePath": "src/components/blog/reaction-bar.tsx" + }, + { + "eventName": "blog_comment_submitted", + "eventDescription": "User submitted a comment on a blog post", + "filePath": "src/components/blog/comment-section.tsx" + }, + { + "eventName": "blog_comment_deleted", + "eventDescription": "User deleted their own comment", + "filePath": "src/components/blog/comment-section.tsx" + }, + { + "eventName": "blog_search_performed", + "eventDescription": "User performed a search query in the blog search bar", + "filePath": "src/components/search/search-bar.tsx" + }, + { + "eventName": "blog_search_result_clicked", + "eventDescription": "User clicked on a search result to navigate to a blog post", + "filePath": "src/components/search/search-bar.tsx" + }, + { + "eventName": "contact_form_opened", + "eventDescription": "User opened the contact form popover", + "filePath": "src/components/contact/contact-popover.tsx" + }, + { + "eventName": "contact_form_submitted", + "eventDescription": "User submitted the contact form (conversion event)", + "filePath": "src/components/contact/contact-popover.tsx" + }, + { + "eventName": "email_copied", + "eventDescription": "User copied the email address from the footer", + "filePath": "src/components/layout/footer.tsx" + }, + { + "eventName": "social_link_clicked", + "eventDescription": "User clicked a social media link (GitHub, LinkedIn, X)", + "filePath": "src/components/layout/footer.tsx" + }, + { + "eventName": "project_link_clicked", + "eventDescription": "User clicked on a project link (GitHub or live site)", + "filePath": "src/components/home/projects.tsx" + }, + { + "eventName": "theme_toggled", + "eventDescription": "User toggled between light and dark theme", + "filePath": "src/components/ui/theme-switch.tsx" + } +] diff --git a/bun.lock b/bun.lock index 40aff037..683a5a77 100644 --- a/bun.lock +++ b/bun.lock @@ -3,49 +3,130 @@ "workspaces": { "": { "dependencies": { + "@neondatabase/serverless": "^1.0.2", "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", - "@tailwindcss/postcss": "4.0.0-alpha.13", + "@radix-ui/react-slot": "^1.2.4", + "@t3-oss/env-nextjs": "^0.13.10", + "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.12", "@types/node": "20.11.17", "@vercel/analytics": "^1.1.3", "@vercel/speed-insights": "^1.0.9", + "better-auth": "^1.4.7", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.1", "framer-motion": "^12.23.25", "geist": "1.2.2", - "lucide-react": "^0.555.0", + "lucide-react": "^0.562.0", "next": "16", "next-mdx-remote": "^5.0.0", "postcss": "^8.4.35", + "posthog-js": "^1.309.1", + "posthog-node": "^5.17.4", "react": "19", "react-dom": "19", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", - "shiki": "^3.19.0", - "sugar-high": "^0.6.0", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", - "tailwindcss": "4.0.0-alpha.13", + "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", + "uuid": "^13.0.0", + "zod": "^4.2.1", }, "devDependencies": { - "@tanstack/react-query-devtools": "^5.90.12", "@types/react": "19", "@types/react-dom": "19", + "@types/react-syntax-highlighter": "^15.5.13", + "drizzle-kit": "^0.31.8", }, }, }, "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@better-auth/core": ["@better-auth/core@1.4.7", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-rNfj8aNFwPwAMYo+ahoWDsqKrV7svD3jhHSC6+A77xxKodbgV0UgH+RO21GMaZ0PPAibEl851nw5e3bsNslW/w=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.7", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.7" } }, "sha512-k07C/FWnX6m+IxLruNkCweIxuaIwVTB2X40EqwamRVhYNBAhOYZFGLHH+PtQyM+Yf1Z4+8H6MugLOXSreXNAjQ=="], + + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -96,27 +177,47 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - "@next/env": ["@next/env@16.0.8", "", {}, "sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ=="], + "@neondatabase/serverless": ["@neondatabase/serverless@1.0.2", "", { "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" } }, "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw=="], + + "@next/env": ["@next/env@16.1.0", "", {}, "sha512-Dd23XQeFHmhf3KBW76leYVkejHlCdB7erakC2At2apL1N08Bm+dLYNP+nNHh0tzUXfPQcNcXiQyacw0PG4Fcpw=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-onHq8dl8KjDb8taANQdzs3XmIqQWV3fYdslkGENuvVInFQzZnuBYYOG2HGHqqtvgmEU7xWzhgndXXxnhk4Z3fQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Am6VJTp8KhLuAH13tPrAoVIXzuComlZlMwGr++o2KDjWiKPe3VwpxYhgV6I4gKls2EnsIMggL4y7GdXyDdJcFA=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fVicfaJT6QfghNyg8JErZ+EMNQ812IS0lmKfbmC01LF1nFBcKfcs4Q75Yy8IqnsCqH/hZwGhqzj3IGVfWV6vpA=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TojQnDRoX7wJWXEEwdfuJtakMDW64Q7NrxQPviUnfYJvAx5/5wcGE+1vZzQ9F17m+SdpFeeXuOr6v3jbyusYMQ=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-quhNFVySW4QwXiZkZ34SbfzNBm27vLrxZ2HwTfFFO1BBP0OY1+pI0nbyewKeq1FriqU+LZrob/cm26lwsiAi8Q=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-6JW0z2FZUK5iOVhUIWqE4RblAhUj1EwhZ/MwteGb//SpFTOHydnhbp3868gxalwea+mbOLWO6xgxj9wA9wNvNw=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+DK/akkAvvXn5RdYN84IOmLkSy87SCmpofJPdB8vbLmf01BzntPBSYXnMvnEEv/Vcf3HYJwt24QZ/s6sWAwOMQ=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Tr0j94MphimCCks+1rtYPzQFK+faJuhHWCegU9S9gDlgyOk8Y3kPmO64UcjyzZAlligeBtYZ/2bEyrKq0d2wqQ=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.8", "", { "os": "win32", "cpu": "x64" }, "sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA=="], + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@posthog/core": ["@posthog/core@1.8.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-jfzBtQIk9auRi/biO+G/gumK5KxqsD5wOr7XpYMROE/I3pazjP4zIziinp21iQuIQJMXrDvwt9Af3njgOGwtew=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -126,15 +227,21 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], @@ -142,56 +249,50 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ=="], - - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="], + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], - "@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="], + "@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.10", "", { "dependencies": { "@t3-oss/env-core": "0.13.10" }, "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ=="], - "@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.0-alpha.13", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.0-alpha.13", "@tailwindcss/oxide-darwin-arm64": "4.0.0-alpha.13", "@tailwindcss/oxide-darwin-x64": "4.0.0-alpha.13", "@tailwindcss/oxide-freebsd-x64": "4.0.0-alpha.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.0-alpha.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.0-alpha.13", "@tailwindcss/oxide-linux-arm64-musl": "4.0.0-alpha.13", "@tailwindcss/oxide-linux-x64-gnu": "4.0.0-alpha.13", "@tailwindcss/oxide-linux-x64-musl": "4.0.0-alpha.13", "@tailwindcss/oxide-win32-x64-msvc": "4.0.0-alpha.13" } }, "sha512-ocRljLw7s5nPHZ22pYfnw0xoNo3Dk8SlAA4Z2k5e4pBubGS92CtzgkSw8U4DhsGKty6IvmjbXKyMOGC/BB52Ug=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.0-alpha.13", "", { "os": "android", "cpu": "arm64" }, "sha512-LzsWIHyexltM5prLn/7B8jH2y3EU1j/GZ1JcFdw2ZqBWRmco8jvnQnjH/oUwZrP3aJbGfAL3aRISeu/rcWwUSA=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.0-alpha.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N/J/Yr9Fgjee7Ep7iO9oui6RtCDUndbXauXgHxWUZ4NXrFrIMKN+LdUIJm4B5vG0UZKgmglyjtQpR8D7DkaXRw=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.0-alpha.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-/DmZbESO1Q3fVfi3cx3aYyt0TyvFQb1wTU+lG08BOnuUvY8g5bHsCVsN9jxt1yqItVQ2PJIbOhZT+BrZ066iBg=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.0-alpha.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WF/Odsw7qo3CpPoffgzAa1CG2glfrWY0nzp1eayGxVDA5Z/EuEjIPFns1iXhOmFvaDkDZJcyoDdL+zpQwZkNZQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.0-alpha.13", "", { "os": "linux", "cpu": "arm" }, "sha512-7va/gSuoQD4PJn0IDisPMVuYJ/zhZUvaPODVuolpk0EQaJqa31EaDFCLgTR1F1beCZXUlaZi5poMe9tAjwUkow=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.0-alpha.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-+crN+9ITYtxq4TVxM2FHwLxnOvngG1yfyLrFtlSNY4OEpvAZjSEaW7oZAh9nUCREQTli+/0bi/fhzohQ3CUpBw=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.0-alpha.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-m47iIQ629DUEwd5bWaglpfLljQPp9ewEB+pn7fMhPTKCo8KS2xMfO20SvcpE3Ad+8zNEjGFgEuG2c2mfquMv1g=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.0-alpha.13", "", { "os": "linux", "cpu": "x64" }, "sha512-dzgkcu2Cm/aNzdIQH5OFKDR5XtNe7REnEQOTrejWT+07oiprhzrRnJEqKN2XcUEUlFjX8L6rUH60EE4+2Ef/+w=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.0-alpha.13", "", { "os": "linux", "cpu": "x64" }, "sha512-iv2Gw/zWE6y6ejl695xbeuWG3wn3kJPDhvOlPwSPg+XVzev1WT9wE0cXDFmZl4r2kuDtg2VwEU8aVL9uyekWeA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.0-alpha.13", "", { "os": "win32", "cpu": "x64" }, "sha512-WNWVyueMTlDiMz33eiPeE2goD11dY6UyvAukt/GsQ9842WcvJ3Can0LAbtJBz+NV/HLaeFzd1ELJ4N9edgwQNw=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.0.0-alpha.13", "", { "dependencies": { "@tailwindcss/oxide": "4.0.0-alpha.13", "lightningcss": "^1.24.0", "postcss-import": "^16.0.0", "tailwindcss": "4.0.0-alpha.13" } }, "sha512-b2ip3zkCGM+TvoZxDciwTffmq6XDxKqg2+N6/s9pOKDLVKjfQUwHgMkuPwAz8etncrhTm/T045E4Wx7FvmiIdw=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="], "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], "@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.91.1", "", {}, "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="], - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.1", "", { "dependencies": { "@tanstack/query-devtools": "5.91.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.10", "react": "^18 || ^19" } }, "sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -208,12 +309,16 @@ "@types/node": ["@types/node@20.11.17", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw=="], + "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="], + "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -230,7 +335,15 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "caniuse-lite": ["caniuse-lite@1.0.30001759", "", {}, "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + + "better-auth": ["better-auth@1.4.7", "", { "dependencies": { "@better-auth/core": "1.4.7", "@better-auth/telemetry": "1.4.7", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.5", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.22.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.4.1", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.41.0", "mongodb": "^6.18.0", "mysql2": "^3.14.4", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.16.3", "prisma": "^5.22.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^4.0.15", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-kVmDQxzqGwP4FFMOYpS5I7oAaoFW3hwooUAAtcbb2DrOYv5EUvRUDJbTMaPoMTj7URjNDQ6vG9gcCS1Q+0aVBw=="], + + "better-call": ["better-call@1.1.5", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -242,6 +355,8 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -250,6 +365,10 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -258,16 +377,30 @@ "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], + + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], @@ -286,22 +419,22 @@ "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="], - "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], - "framer-motion": ["framer-motion@12.23.25", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "framer-motion": ["framer-motion@12.23.26", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA=="], "geist": ["geist@1.2.2", "", { "peerDependencies": { "next": ">=13.2.0 <15" } }, "sha512-uRDrxhvdnPwWJmh+K5+/5LXSKwvJzaYCl9tDXgiBi4hj7hB4K7+n/WLcvJMFs5btvyn0r9OSwCd1s6CmqAsxEw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -314,24 +447,28 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "kysely": ["kysely@0.28.9", "", {}, "sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], @@ -360,12 +497,30 @@ "lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="], - "lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="], + "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], @@ -386,6 +541,20 @@ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], @@ -446,37 +615,49 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "next": ["next@16.0.8", "", { "dependencies": { "@next/env": "16.0.8", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.8", "@next/swc-darwin-x64": "16.0.8", "@next/swc-linux-arm64-gnu": "16.0.8", "@next/swc-linux-arm64-musl": "16.0.8", "@next/swc-linux-x64-gnu": "16.0.8", "@next/swc-linux-x64-musl": "16.0.8", "@next/swc-win32-arm64-msvc": "16.0.8", "@next/swc-win32-x64-msvc": "16.0.8", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g=="], + "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], + + "next": ["next@16.1.0", "", { "dependencies": { "@next/env": "16.1.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.0", "@next/swc-darwin-x64": "16.1.0", "@next/swc-linux-arm64-gnu": "16.1.0", "@next/swc-linux-arm64-musl": "16.1.0", "@next/swc-linux-x64-gnu": "16.1.0", "@next/swc-linux-x64-musl": "16.1.0", "@next/swc-win32-arm64-msvc": "16.1.0", "@next/swc-win32-x64-msvc": "16.1.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Y+KbmDbefYtHDDQKLNrmzE/YYzG2msqo2VXhzh5yrJ54tx/6TmGdkR5+kP9ma7i7LwZpZMfoY3m/AoPPPKxtVw=="], "next-mdx-remote": ["next-mdx-remote@5.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^3.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-RNNbqRpK9/dcIFZs/esQhuLA8jANqlH694yqoDBK8hkVdJUndzzGmnPHa2nyi90N4Z9VmzuSWNRpr5ItT3M7xQ=="], - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "postcss-import": ["postcss-import@16.1.1", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ=="], - "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "posthog-js": ["posthog-js@1.309.1", "", { "dependencies": { "@posthog/core": "1.8.1", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" } }, "sha512-JUJcQhYzNNKO0cgnSbowCsVi2RTu75XGZ2EmnTQti4tMGRCTOv/HCnZasdFniBGZ0rLugQkaScYca/84Ta2u5Q=="], + + "posthog-node": ["posthog-node@5.17.4", "", { "dependencies": { "@posthog/core": "1.8.1" } }, "sha512-hrd+Do/DMt40By12ESIDUfD81V9OASjq9XHjycZrGiD8cX/ZwCIVSJLUb7nQmvSCWcKII+u+nnPVuc4LjTDl9g=="], + + "preact": ["preact@10.28.0", "", {}, "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA=="], "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], - "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], @@ -484,8 +665,6 @@ "react-syntax-highlighter": ["react-syntax-highlighter@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg=="], - "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], @@ -496,34 +675,42 @@ "refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="], - "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], - - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "shiki": ["shiki@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/engine-oniguruma": "3.19.0", "@shikijs/langs": "3.19.0", "@shikijs/themes": "3.19.0", "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -534,13 +721,13 @@ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - "sugar-high": ["sugar-high@0.6.1", "", {}, "sha512-kg1qMW7WwJcueXIlHkChL/p2EWY3gf8rQmP6n5nUq2TWVqatqDTMLvViS9WgAjgyTKH5/3/b8sRwWPOOAo1zMA=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], - "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], - "tailwindcss": ["tailwindcss@4.0.0-alpha.13", "", {}, "sha512-YbLm8I10/bsGr7zrUNxn4Soj9Lc71anTWu/ulgry64rbBU4m0vov539+caeifAiKhsef2NUCW1twaqH3NfJd2Q=="], + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -572,28 +759,60 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "@neondatabase/serverless/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/hast/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/mdast/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "estree-util-visit/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/pg/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], - "hast-util-to-html/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "estree-util-visit/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "hast-util-to-jsx-runtime/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "mdast-util-find-and-replace/unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "mdast-util-find-and-replace/unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "mdast-util-from-markdown/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "mdast-util-mdx-jsx/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -606,6 +825,8 @@ "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "unified/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "unist-util-position/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -624,7 +845,57 @@ "vfile-message/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@radix-ui/react-separator/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@neondatabase/serverless/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "@types/pg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "mdast-util-find-and-replace/unist-util-is/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "mdast-util-find-and-replace/unist-util-visit-parents/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "mdast-util-phrasing/unist-util-is/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], } diff --git a/components.json b/components.json new file mode 100644 index 00000000..f764110c --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..5998b1bc --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + schema: './src/server/db/schema.ts', + out: './src/server/db/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL! + } +}) diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 00000000..95d5c929 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,30 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + outputFileTracingRoot: process.cwd(), + async redirects() { + return [ + { + source: '/categories', + destination: '/blog/categories', + permanent: true, + }, + { + source: '/categories/:slug*', + destination: '/blog/categories/:slug*', + permanent: true, + }, + { + source: '/topics', + destination: '/blog/topics', + permanent: true, + }, + { + source: '/topics/:slug*', + destination: '/blog/topics/:slug*', + permanent: true, + }, + ] + }, +} + +export default nextConfig diff --git a/package.json b/package.json index 06a0f916..cec8aa93 100644 --- a/package.json +++ b/package.json @@ -2,40 +2,60 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", - "start": "next start" + "build": "next build --webpack", + "ogbuild": "next build", + "start": "next start", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" + }, + "engines": { + "node": "24.x" }, "dependencies": { + "@neondatabase/serverless": "^1.0.2", "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", - "@tailwindcss/postcss": "4.0.0-alpha.13", + "@radix-ui/react-slot": "^1.2.4", + "@t3-oss/env-nextjs": "^0.13.10", + "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.12", "@types/node": "20.11.17", "@vercel/analytics": "^1.1.3", "@vercel/speed-insights": "^1.0.9", + "better-auth": "^1.4.7", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.1", "framer-motion": "^12.23.25", "geist": "1.2.2", - "lucide-react": "^0.555.0", + "lucide-react": "^0.562.0", "next": "16", "next-mdx-remote": "^5.0.0", "postcss": "^8.4.35", + "posthog-js": "^1.309.1", + "posthog-node": "^5.17.4", "react": "19", "react-dom": "19", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", - "shiki": "^3.19.0", - "sugar-high": "^0.6.0", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", - "tailwindcss": "4.0.0-alpha.13", + "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "uuid": "^13.0.0", + "zod": "^4.2.1" }, "devDependencies": { - "@tanstack/react-query-devtools": "^5.90.12", "@types/react": "19", - "@types/react-dom": "19" + "@types/react-dom": "19", + "@types/react-syntax-highlighter": "^15.5.13", + "drizzle-kit": "^0.31.8" } -} +} \ No newline at end of file diff --git a/public/api/spotify.json b/public/api/mock-api.json similarity index 100% rename from public/api/spotify.json rename to public/api/mock-api.json diff --git a/public/images/remco-stoeten.png b/public/images/remco-stoeten.png new file mode 100644 index 00000000..211357ba Binary files /dev/null and b/public/images/remco-stoeten.png differ diff --git a/public/logos/docker.svg b/public/logos/docker.svg new file mode 100644 index 00000000..0021a8a7 --- /dev/null +++ b/public/logos/docker.svg @@ -0,0 +1 @@ +Docker \ No newline at end of file diff --git a/public/logos/gnubash.svg b/public/logos/gnubash.svg new file mode 100644 index 00000000..857dd04c --- /dev/null +++ b/public/logos/gnubash.svg @@ -0,0 +1 @@ +GNU Bash \ No newline at end of file diff --git a/public/logos/javascript.svg b/public/logos/javascript.svg new file mode 100644 index 00000000..91e51623 --- /dev/null +++ b/public/logos/javascript.svg @@ -0,0 +1 @@ +JavaScript \ No newline at end of file diff --git a/public/logos/nextdotjs.svg b/public/logos/nextdotjs.svg new file mode 100644 index 00000000..06a2bd6e --- /dev/null +++ b/public/logos/nextdotjs.svg @@ -0,0 +1 @@ +Next.js \ No newline at end of file diff --git a/public/logos/nodedotjs.svg b/public/logos/nodedotjs.svg new file mode 100644 index 00000000..281c8296 --- /dev/null +++ b/public/logos/nodedotjs.svg @@ -0,0 +1 @@ +Node.js \ No newline at end of file diff --git a/public/logos/postgresql.svg b/public/logos/postgresql.svg new file mode 100644 index 00000000..dcf75b72 --- /dev/null +++ b/public/logos/postgresql.svg @@ -0,0 +1 @@ +PostgreSQL \ No newline at end of file diff --git a/public/logos/react.svg b/public/logos/react.svg new file mode 100644 index 00000000..60069956 --- /dev/null +++ b/public/logos/react.svg @@ -0,0 +1 @@ +React \ No newline at end of file diff --git a/public/logos/typescript.svg b/public/logos/typescript.svg new file mode 100644 index 00000000..e8beb740 --- /dev/null +++ b/public/logos/typescript.svg @@ -0,0 +1 @@ +TypeScript \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..8d7efe7d --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,11 @@ +User-agent: * +Allow: / +Disallow: /admin/ +Disallow: /api/ +Disallow: /_next/ + +# Sitemaps +Sitemap: https://remcostoeten.nl/sitemap.xml +Sitemap: https://remcostoeten.nl/sitemap-pages.xml +Sitemap: https://remcostoeten.nl/sitemap-posts.xml +Sitemap: https://remcostoeten.nl/sitemap-categories.xml diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 00000000..bf37d03f --- /dev/null +++ b/public/sw.js @@ -0,0 +1,80 @@ +const CACHE_NAME = 'app-cache-v1'; +const SPOTIFY_CACHE = 'spotify-api-v1'; +const GITHUB_CACHE = 'github-api-v1'; + +// Cache strategy for API calls +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Cache Spotify API responses (stale-while-revalidate) + if (url.pathname.includes('/api/spotify/')) { + event.respondWith( + caches.open(SPOTIFY_CACHE).then((cache) => { + return cache.match(request).then((cachedResponse) => { + // Return cached response immediately if available + if (cachedResponse) { + // Fetch in background to update cache + fetch(request).then((freshResponse) => { + if (freshResponse.ok) { + cache.put(request, freshResponse.clone()); + } + }).catch(() => { + // Ignore fetch errors when updating cache + }); + return cachedResponse; + } + + // Fetch and cache if not in cache + return fetch(request).then((response) => { + if (response.ok) { + const responseClone = response.clone(); + cache.put(request, responseClone); + } + return response; + }); + }); + }) + ); + return; + } + + // Cache GitHub API responses (cache-first) + if (url.pathname.includes('/api/github/')) { + event.respondWith( + caches.open(GITHUB_CACHE).then((cache) => { + return cache.match(request).then((cachedResponse) => { + if (cachedResponse) { + return cachedResponse; + } + return fetch(request).then((response) => { + if (response.ok) { + const responseClone = response.clone(); + cache.put(request, responseClone); + } + return response; + }); + }); + }) + ); + return; + } + + // Let other requests pass through + event.respondWith(fetch(request)); +}); + +// Clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME && cacheName !== SPOTIFY_CACHE && cacheName !== GITHUB_CACHE) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); \ No newline at end of file diff --git a/scripts/api-test.mjs b/scripts/api-test.mjs new file mode 100644 index 00000000..d67e2098 --- /dev/null +++ b/scripts/api-test.mjs @@ -0,0 +1,159 @@ +#!/usr/bin/env node + +import readline from 'readline'; +import { exec } from 'child_process'; +import { readFile } from 'fs/promises'; +import { existsSync } from 'fs'; + +/** + * API Tester CLI + * + * A small utility to test the local API endpoints used in the activity feed. + * It fetches the data, renders the JSON, and copies the curl command to your clipboard. + */ + +let BASE_URL = 'http://localhost:3000'; +let endpoints = [ + { name: 'GitHub Activity', path: '/api/github/activity', method: 'GET' }, + { name: 'GitHub Contributions', path: '/api/github/contributions', method: 'GET' }, + { name: 'GitHub Commits', path: '/api/github/commits', method: 'GET' }, + { name: 'GitHub Repo (remcostoeten.nl)', path: '/api/github/repo?owner=remcostoeten&repo=remcostoeten.nl', method: 'GET' }, + { name: 'Spotify Now Playing', path: '/api/spotify/now-playing', method: 'GET' }, + { name: 'Spotify Recent', path: '/api/spotify/recent', method: 'GET' }, +]; + +async function loadConfig() { + const configPath = './api-test.config.json'; + if (existsSync(configPath)) { + try { + const config = JSON.parse(await readFile(configPath, 'utf8')); + if (config.baseUrl) BASE_URL = config.baseUrl; + if (config.endpoints) endpoints = config.endpoints; + } catch (err) { + console.log('\x1b[33mWarning: Could not load config file, using defaults\x1b[0m'); + } + } +} + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +function showMenu() { + console.clear(); + console.log('\x1b[36m%s\x1b[0m', '=== remcostoeten.nl API Tester ==='); + console.log('\x1b[90m%s\x1b[0m', 'Select an endpoint to fetch data and copy curl to clipboard\n'); + + endpoints.forEach((ep, i) => { + const method = ep.method || 'GET'; + console.log(`\x1b[33m${i + 1}\x1b[0m: ${ep.name} \x1b[90m${method} ${ep.path}\x1b[0m`); + }); + + console.log('\n\x1b[31mq\x1b[0m: Quit'); + rl.question('\n\x1b[1mSelection: \x1b[0m', handleInput); +} + +async function handleInput(input) { + if (input.toLowerCase() === 'q') { + console.log('Goodbye!'); + rl.close(); + process.exit(0); + } + + const index = parseInt(input) - 1; + if (isNaN(index) || index < 0 || index >= endpoints.length) { + console.log('\n\x1b[31mInvalid selection. Try again.\x1b[0m'); + setTimeout(showMenu, 800); + return; + } + + const endpoint = endpoints[index]; + const url = BASE_URL + endpoint.path; + const curlCmd = `curl -X GET "${url}" -H "Accept: application/json"`; + + function copyToClipboard(text) { + const platform = process.platform; + let command; + + if (platform === 'darwin') { + command = `echo '${text}' | pbcopy`; + } else if (platform === 'linux') { + // Try xclip first, then wl-copy for Wayland + command = `echo '${text}' | xclip -selection clipboard || echo '${text}' | wl-copy`; + } else if (platform === 'win32') { + command = `echo ${text} | clip`; + } + + if (command) { + exec(command, (err) => { + if (!err) { + console.log('\x1b[32m✓ Curl command copied to clipboard\x1b[0m'); + } + }); + } + } + + copyToClipboard(curlCmd); + console.log(`\x1b[33mFetching:\x1b[0m ${url}`); + + try { + const startTime = Date.now(); + const res = await fetch(url, { + headers: { + 'Accept': 'application/json', + 'User-Agent': 'api-test.mjs/1.0' + } + }); + const duration = Date.now() - startTime; + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`HTTP ${res.status}: ${res.statusText}\nResponse: ${errorText.substring(0, 200)}...`); + } + + const contentType = res.headers.get('content-type'); + let data; + + if (contentType?.includes('application/json')) { + data = await res.json(); + } else { + data = await res.text(); + } + + console.log(`\x1b[32mStatus:\x1b[0m ${res.status} OK \x1b[90m(${duration}ms)\x1b[0m`); + console.log(`\x1b[36mContent-Type:\x1b[0m ${contentType || 'unknown'}`); + console.log(`\x1b[33m\nResponse Body:\x1b[0m`); + + if (typeof data === 'object') { + console.log(JSON.stringify(data, null, 2)); + } else { + console.log(data); + } + } catch (err) { + console.log(`\x1b[31m\nError:\x1b[0m ${err.message}`); + if (err.code === 'ECONNREFUSED') { + console.log(`\x1b[90mConnection refused. Make sure your Next.js server is running at ${BASE_URL}\x1b[0m`); + } else { + console.log(`\x1b[90mCheck your network connection and server status\x1b[0m`); + } + } + + console.log(`\x1b[90m\n--------------------------------------------------\x1b[0m`); + rl.question('Press Enter to return to menu...', () => { + showMenu(); + }); +} + +// Handle Ctrl+C +rl.on('SIGINT', () => { + console.log('\nGoodbye!'); + process.exit(0); +}); + +async function start() { + await loadConfig(); + showMenu(); +} + +start(); diff --git a/scripts/api.ts b/scripts/api.ts new file mode 100755 index 00000000..31627127 --- /dev/null +++ b/scripts/api.ts @@ -0,0 +1,156 @@ +#!/usr/bin/env bun +import readline from 'readline'; +import { exec } from 'child_process'; +import { db } from '../src/server/db/connection'; +import * as authSchema from '../src/server/db/auth-schema'; +import { sql } from 'drizzle-orm'; + +/** + * remcostoeten.nl Unified API CLI + * + * Consolidates functionality for testing endpoints, + * checking database status, and debugging users. + */ + +const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + +const endpoints = [ + { name: 'GitHub Activity', path: '/api/github/activity', method: 'GET' }, + { name: 'GitHub Contributions', path: '/api/github/contributions', method: 'GET' }, + { name: 'GitHub Commits', path: '/api/github/commits', method: 'GET' }, + { name: 'GitHub Repo (remcostoeten.nl)', path: '/api/github/repo?owner=remcostoeten&repo=remcostoeten.nl', method: 'GET' }, + { name: 'Spotify Now Playing', path: '/api/spotify/now-playing', method: 'GET' }, + { name: 'Spotify Recent', path: '/api/spotify/recent', method: 'GET' }, +]; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +function showMenu() { + console.clear(); + console.log('\x1b[36m%s\x1b[0m', '=== remcostoeten.nl Unified CLI ==='); + console.log('\x1b[32m%s\x1b[0m', '--- Database Operations ---'); + console.log('\x1b[33m1\x1b[0m: List Users (Debug)'); + console.log('\x1b[33m2\x1b[0m: Check Sync Status'); + + console.log('\n\x1b[32m%s\x1b[0m', '--- API Endpoint Testing ---'); + endpoints.forEach((ep, i) => { + const index = i + 3; + console.log(`\x1b[33m${index}\x1b[0m: ${ep.name} \x1b[90m${ep.path}\x1b[0m`); + }); + + console.log('\n\x1b[31mq\x1b[0m: Quit'); + rl.question('\n\x1b[1mSelection: \x1b[0m', handleInput); +} + +async function handleInput(input: string) { + if (input.toLowerCase() === 'q') { + console.log('Goodbye!'); + rl.close(); + process.exit(0); + } + + const index = parseInt(input); + + if (index === 1) { + await listUsers(); + } else if (index === 2) { + await checkSyncStatus(); + } else if (index >= 3 && index < endpoints.length + 3) { + await testEndpoint(endpoints[index - 3]); + } else { + console.log('\n\x1b[31mInvalid selection. Try again.\x1b[0m'); + setTimeout(showMenu, 800); + return; + } + + console.log(`\x1b[90m\n--------------------------------------------------\x1b[0m`); + rl.question('Press Enter to return to menu...', () => { + showMenu(); + }); +} + +async function listUsers() { + console.log('\n\x1b[36mQuerying users...\x1b[0m'); + try { + const users = await db.select().from(authSchema.user); + console.log(`\x1b[32mUsers found:\x1b[0m ${users.length}\n`); + users.forEach(u => { + console.log(`- \x1b[1m${u.email}\x1b[0m (Name: ${u.name}, Role: ${u.role})`); + }); + } catch (e) { + console.error('\n\x1b[31mError querying users:\x1b[0m', e); + } +} + +async function checkSyncStatus() { + console.log('\n\x1b[36mChecking sync status...\x1b[0m'); + try { + const githubRes = await db.execute(sql`SELECT count(*) FROM github_activities`); + const spotifyRes = await db.execute(sql`SELECT count(*) FROM spotify_listens`); + const metaRes = await db.execute(sql`SELECT * FROM sync_metadata`); + + const githubCount = (githubRes.rows[0] as any)?.count || 0; + const spotifyCount = (spotifyRes.rows[0] as any)?.count || 0; + + console.log(`- GitHub Activities: \x1b[33m${githubCount}\x1b[0m`); + console.log(`- Spotify Listens: \x1b[33m${spotifyCount}\x1b[0m`); + + if (metaRes.rows.length > 0) { + console.log('\n📅 Last Sync Times:'); + console.table(metaRes.rows); + } else { + console.log('\n📅 No sync metadata found yet.'); + } + } catch (e) { + console.error('\n\x1b[31mError checking status:\x1b[0m', e); + } +} + +async function testEndpoint(endpoint: typeof endpoints[0]) { + const url = BASE_URL + endpoint.path; + console.log(`\n\x1b[33mTesting:\x1b[0m ${url}`); + + try { + const startTime = Date.now(); + const res = await fetch(url, { + headers: { 'Accept': 'application/json' } + }); + const duration = Date.now() - startTime; + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`HTTP ${res.status}: ${res.statusText}\n${errorText.substring(0, 100)}`); + } + + const data = await res.json(); + console.log(`\x1b[32mStatus:\x1b[0m ${res.status} OK \x1b[90m(${duration}ms)\x1b[0m`); + console.log(`\x1b[33mResponse snippet:\x1b[0m`); + console.log(JSON.stringify(data, null, 2).substring(0, 500) + '...'); + + // Copy curl to clipboard + const curlCmd = `curl -X GET "${url}" -H "Accept: application/json"`; + const platform = process.platform; + let command = ''; + if (platform === 'darwin') command = `echo '${curlCmd}' | pbcopy`; + else if (platform === 'linux') command = `echo '${curlCmd}' | xclip -selection clipboard || echo '${curlCmd}' | wl-copy`; + + if (command) { + exec(command, (err) => { + if (!err) console.log('\x1b[90m(Curl command copied to clipboard)\x1b[0m'); + }); + } + } catch (err: any) { + console.log(`\x1b[31m\nError:\x1b[0m ${err.message}`); + } +} + +// Handle Ctrl+C +rl.on('SIGINT', () => { + console.log('\nGoodbye!'); + process.exit(0); +}); + +showMenu(); diff --git a/scripts/check-db.mjs b/scripts/check-db.mjs new file mode 100644 index 00000000..cb467984 --- /dev/null +++ b/scripts/check-db.mjs @@ -0,0 +1,40 @@ +import { Pool } from '@neondatabase/serverless' +import { drizzle } from 'drizzle-orm/neon-serverless' +import { sql } from 'drizzle-orm' + + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }) +const db = drizzle(pool) + +async function checkDatabase() { + try { + console.log('📊 Activity Sync Status:'); + + const githubRes = await db.execute(sql`SELECT count(*) FROM github_activities`); + const spotifyRes = await db.execute(sql`SELECT count(*) FROM spotify_listens`); + const metaRes = await db.execute(sql`SELECT * FROM sync_metadata`); + + const githubCount = githubRes.rows[0]?.count || 0; + const spotifyCount = spotifyRes.rows[0]?.count || 0; + + console.log(`- GitHub Activities: ${githubCount}`); + console.log(`- Spotify Listens: ${spotifyCount}`); + + if (metaRes.rows.length > 0) { + console.log('\n📅 Last Sync Times:'); + console.table(metaRes.rows); + } else { + console.log('\n📅 No sync metadata found yet (run a sync first).'); + } + + process.exit(0); + } catch (error) { + console.error('❌ Error checking database:', error); + process.exit(1); + } +} + +checkDatabase().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/optimize-media.sh b/scripts/optimize-media.sh new file mode 100755 index 00000000..48a7234a --- /dev/null +++ b/scripts/optimize-media.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# optimize-media.sh - Media optimization script using ffmpeg +# Usage: ./optimize-media.sh [max_width] +# Example: ./optimize-media.sh ./public/images/demo.gif 1200 + +FILE_PATH="$1" +MAX_WIDTH="${2:-1200}" + +if [ -z "$FILE_PATH" ]; then + echo "Usage: $0 [max_width]" + exit 1 +fi + +if [ ! -f "$FILE_PATH" ]; then + echo "Error: File not found: $FILE_PATH" + exit 1 +fi + +FILENAME=$(basename "$FILE_PATH") +DIRNAME=$(dirname "$FILE_PATH") +EXTENSION="${FILENAME##*.}" +BASENAME="${FILENAME%.*}" +OUTPUT_PATH="$DIRNAME/$BASENAME.webp" + +# Check for ffmpeg +if ! command -v ffmpeg &> /dev/null; then + echo "Error: ffmpeg is not installed." + echo "Install it via 'sudo apt install ffmpeg' or 'brew install ffmpeg'" + exit 1 +fi + +echo "Optimizing $FILENAME..." +echo "Max width: $MAX_WIDTH px" + +# Determine file type and optimize +case "${EXTENSION,,}" in + gif) + # Convert GIF to animated WebP + # -loop 0: Loop forever + # -vf scale: Resize maintaining aspect ratio + # -q:v 80: Quality factor + ffmpeg -i "$FILE_PATH" \ + -vf "scale='min($MAX_WIDTH,iw)':-1:flags=lanczos" \ + -vcodec libwebp -lossless 0 -compression_level 6 \ + -q:v 75 -loop 0 -preset default -an -vsync 0 \ + -y "$OUTPUT_PATH" + ;; + png|jpg|jpeg|tiff|bmp) + # Convert Image to static WebP + ffmpeg -i "$FILE_PATH" \ + -vf "scale='min($MAX_WIDTH,iw)':-1:flags=lanczos" \ + -c:v libwebp -quality 80 \ + -y "$OUTPUT_PATH" + ;; + *) + echo "Error: Unsupported file format .$EXTENSION" + exit 1 + ;; +esac + +if [ $? -eq 0 ]; then + ORIGINAL_SIZE=$(du -h "$FILE_PATH" | cut -f1) + NEW_SIZE=$(du -h "$OUTPUT_PATH" | cut -f1) + echo "✅ Success!" + echo "Output: $OUTPUT_PATH" + echo "Size: $ORIGINAL_SIZE -> $NEW_SIZE" +else + echo "❌ Failed to convert $FILENAME" + exit 1 +fi diff --git a/scripts/sync-activity.sh b/scripts/sync-activity.sh new file mode 100644 index 00000000..d9c8ae38 --- /dev/null +++ b/scripts/sync-activity.sh @@ -0,0 +1,39 @@ +# Configuration (defaults to your site, can be overridden by env) +SITE_URL="${REMCO_SITE_URL:-https://remcostoeten.nl}" +# Reads secret from environment (set this in your .zshrc/.bashrc/etc) +API_SECRET="${CRON_SECRET}" + +if [ -z "$API_SECRET" ]; then + echo "⚠️ CRON_SECRET is not set." + read -p "❓ Would you like to set it now for this session? (y/n): " confirm + if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then + read -sp "Enter CRON_SECRET: " user_secret + echo + API_SECRET="$user_secret" + export CRON_SECRET="$user_secret" + else + echo "❌ Skipping sync." + exit 0 + fi +fi + + +echo "🕒 [$(date)] Starting Activity Sync for $SITE_URL..." + +# Trigger the sync via POST (added -L to follow redirects) +RESPONSE=$(curl -s -L -X POST "$SITE_URL/api/sync" \ + -H "Authorization: Bearer $API_SECRET" \ + -H "Content-Type: application/json") + + +# Check if successful +if [[ $RESPONSE == *"\"success\":true"* ]]; then + NEW_GH=$(echo $RESPONSE | grep -o '"newItems":[0-9]*' | head -1 | cut -d: -f2) + NEW_SPOT=$(echo $RESPONSE | grep -o '"newItems":[0-9]*' | tail -1 | cut -d: -f2) + echo "✅ Sync successful!" + echo "📊 New items: GitHub ($NEW_GH), Spotify ($NEW_SPOT)" +else + echo "❌ Sync failed!" + echo "🔗 Response: $RESPONSE" + exit 1 +fi diff --git a/server-prompt.md b/server-prompt.md new file mode 100644 index 00000000..6529b607 --- /dev/null +++ b/server-prompt.md @@ -0,0 +1,351 @@ +```md +# PART 1: ENV + SETUP + +Goal: env.ts, drizzle.config.ts, db connection, helpers, schema, test query from frontend. + +We use Postgres Neon. I will set DATABASE_URL in .env myself. + +## Create files + +touch drizzle.config.ts + +mkdir src/server/db +touch src/server/env.ts +touch src/server/db/connection.ts +touch src/server/db/schema.ts +touch src/server/db/helpers.ts + +## Install packages + +bun install @neondatabase/serverless +bun install drizzle-orm +bun install drizzle-kit -D +bun install @t3-oss/env-nextjs +bun add posthog-js + +## Create src/server/env.ts + +Variables required: + +DATABASE_URL + +BETTER_AUTH_URL +BETTER_AUTH_SECRET + +GITHUB_CLIENT_ID +GITHUB_CLIENT_SECRET + +ADMIN_EMAIL + +RESEND_API_KEY +RESEND_EMAIL_FROM + +NEXT_PUBLIC_POSTHOG_KEY +NEXT_PUBLIC_POSTHOG_HOST + +Expected env.ts: + +```ts +import { createEnv } from '@t3-oss/env-core' +import { z } from 'zod' + +export const env = createEnv({ + clientPrefix: 'NEXT_PUBLIC_', + server: { + DATABASE_URL: z.string().url(), + BETTER_AUTH_URL: z.string().url(), + BETTER_AUTH_SECRET: z.string().min(1), + + GITHUB_CLIENT_ID: z.string().optional(), + GITHUB_CLIENT_SECRET: z.string().optional(), + + ADMIN_EMAIL: z.string().email().optional(), + + RESEND_API_KEY: z.string().optional(), + RESEND_EMAIL_FROM: z.string().email().optional(), + }, + client: { + NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().url().optional(), + }, + runtimeEnv: { + DATABASE_URL: process.env.DATABASE_URL, + BETTER_AUTH_URL: process.env.BETTER_AUTH_URL, + BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET, + + GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, + GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, + + ADMIN_EMAIL: process.env.ADMIN_EMAIL, + + RESEND_API_KEY: process.env.RESEND_API_KEY, + RESEND_EMAIL_FROM: process.env.RESEND_EMAIL_FROM, + + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, + } +}) +``` + +## Add tsconfig paths: + +```json +{ + "compilerOptions": { + "paths": { + "env": ["./src/server/env.ts"], + "db": ["./src/server/db/connection.ts"], + "schema": ["./src/server/db/schema.ts"] + } + } +} +``` + +## Update env.example with all required variables. +Do not commit .env. + +## Commit part 1 + +git checkout -b feature/server-env +git add . +git commit -m "env + paths + deps" +git push origin feature/server-env + + + +# PART 2: DRIZZLE CONFIG + +Create drizzle.config.ts containing: + +```ts +import { env } from 'env' + +export default { + schema: './src/server/db/schema.ts', + out: './src/server/db/migrations', + driver: 'pg', + dbCredentials: { + connectionString: env.DATABASE_URL + } +} +``` + +Add package scripts: + +```json +{ + "scripts": { + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" + } +} +``` + +Commit + +git add drizzle.config.ts package.json +git commit -m "add drizzle config + scripts" +git push + + + +# PART 3: DATABASE CONNECTION + +src/server/db/connection.ts: + +```ts +import { Pool } from '@neondatabase/serverless' +import { drizzle } from 'drizzle-orm/neon-serverless' +import { env } from 'env' +import * as schema from 'schema' + +const pool = new Pool({ connectionString: env.DATABASE_URL }) + +export const db = drizzle(pool, { schema }) + +export { schema } +``` + +Commit + +git add src/server/db/connection.ts +git commit -m "db neon drizzle connection" +git push + + + +# PART 4: HELPERS + +src/server/db/helpers.ts: + +```ts +import { text, timestamp, integer, boolean, real } from 'drizzle-orm/pg-core' + +export const timestamps = { + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +} + +export const createdTimestamp = { + createdAt: timestamp('created_at').defaultNow().notNull(), +} + +export const primaryId = { + id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), +} + +export const visitorId = { + visitorId: text('visitor_id').notNull(), +} + +export const optionalVisitorId = { + visitorId: text('visitor_id'), +} + +export const blogSlug = { + slug: text('slug').notNull(), +} + +export const viewCounters = { + uniqueViews: integer('unique_views').default(0).notNull(), + totalViews: integer('total_views').default(0).notNull(), +} + +export const draftStatus = { + isDraft: boolean('is_draft').default(false).notNull(), +} + +export const engagementMetrics = { + timeOnPage: integer('time_on_page'), + scrollDepth: real('scroll_depth'), + reachedEnd: boolean('reached_end').default(false).notNull(), +} + +export const deviceType = { + device: text('device').$type<'desktop' | 'mobile' | 'tablet'>(), +} + +export type DeviceType = 'desktop' | 'mobile' | 'tablet' +``` + +Commit + +git add src/server/db/helpers.ts +git commit -m "helpers" +git push + + + +# PART 5: SCHEMA + +src/server/db/schema.ts: + +```ts +import { pgTable, text, index, primaryKey, timestamp, boolean, integer } from 'drizzle-orm/pg-core' +import { + primaryId, + timestamps, + createdTimestamp, + visitorId, + blogSlug, + viewCounters, + draftStatus, + engagementMetrics, + deviceType, +} from './helpers' + +export const blogPosts = pgTable('blog_posts', { + slug: text('slug').primaryKey(), + ...viewCounters, + ...draftStatus, + ...timestamps, +}, (t) => [ + index('blog_posts_draft_idx').on(t.isDraft), +]) + +export const blogSessions = pgTable('blog_sessions', { + ...primaryId, + ...blogSlug, + ...visitorId, + ...engagementMetrics, + ...deviceType, + referrer: text('referrer'), + startedAt: timestamp('started_at').defaultNow().notNull(), + endedAt: timestamp('ended_at'), +}, (t) => [ + index('blog_sessions_slug_idx').on(t.slug), + index('blog_sessions_visitor_idx').on(t.visitorId), +]) + +export const blogLinkClicks = pgTable('blog_link_clicks', { + ...primaryId, + ...blogSlug, + ...visitorId, + sessionId: text('session_id').references(() => blogSessions.id, { onDelete: 'set null' }), + linkHref: text('link_href').notNull(), + linkText: text('link_text'), + isInternal: boolean('is_internal').default(true).notNull(), + clickedAt: timestamp('clicked_at').defaultNow().notNull(), +}, (t) => [ + index('blog_link_clicks_slug_idx').on(t.slug), +]) + +export const EMOJI_TYPES = ['fire','heart','clap','thinking','rocket'] as const +export type EmojiType = typeof EMOJI_TYPES[number] + +export const blogReactions = pgTable('blog_reactions', { + ...blogSlug, + emoji: text('emoji').$type().notNull(), + ...visitorId, + ...createdTimestamp, +}, (t) => [ + primaryKey({ columns: [t.slug, t.emoji, t.visitorId] }), + index('blog_reactions_slug_idx').on(t.slug), +]) + +export type BlogPost = typeof blogPosts.$inferSelect +export type BlogSession = typeof blogSessions.$inferSelect +export type BlogLinkClick = typeof blogLinkClicks.$inferSelect +export type BlogReaction = typeof blogReactions.$inferSelect +``` + +Commit + +git add src/server/db/schema.ts +git commit -m "schema" +git push + + + +# PART 6: TEST QUERY + +Create server action in app/page.tsx: + +```ts +'use server' +import { db } from 'db' +import { blogPosts } from 'schema' + +export async function countPosts() { + const result = await db.select().from(blogPosts) + return result.length +} +``` + +Then call from frontend: + +```ts +const count = await countPosts() + +return
{count}
+``` + +Test page loads without error and prints a number. + +Commit final part. + +git add . +git commit -m "test query working" +git push +``` diff --git a/src/actions/admin.ts b/src/actions/admin.ts new file mode 100644 index 00000000..bf2782d2 --- /dev/null +++ b/src/actions/admin.ts @@ -0,0 +1,67 @@ +'use server' + +import { db } from '@/server/db/connection' +import { blogPosts, blogViews, contactSubmissions, contactInteractions, contactAbandonments } from '@/server/db/schema' +import { desc, count, sql, eq } from 'drizzle-orm' + +export async function getAdminMetrics() { + const [ + totalViews, + uniqueVisitors, + viewsByCountry, + recentViews, + contactStats, + interactions, + abandonments, + postStats + ] = await Promise.all([ + // Total Views + db.select({ count: count() }).from(blogViews), + + // Unique Visitors (distinct fingerprint) + db.select({ count: count(blogViews.fingerprint) }).from(blogViews), + + // Views by Country + db.select({ + country: blogViews.geoCountry, + count: count() + }) + .from(blogViews) + .groupBy(blogViews.geoCountry) + .orderBy(desc(count())) + .limit(10), + + // Recent Views Log + db.select() + .from(blogViews) + .orderBy(desc(blogViews.viewedAt)) + .limit(50), + + // Contact Submissions + db.select().from(contactSubmissions).orderBy(desc(contactSubmissions.createdAt)), + + // Contact Interactions + db.select().from(contactInteractions).orderBy(desc(contactInteractions.createdAt)), + + // Contact Abandonments + db.select().from(contactAbandonments).orderBy(desc(contactAbandonments.createdAt)), + + // Per-post view stats + db.select({ + slug: blogPosts.slug, + totalViews: blogPosts.totalViews, + uniqueViews: blogPosts.uniqueViews + }).from(blogPosts) + ]) + + return { + totalViews: totalViews[0].count, + uniqueVisitors: uniqueVisitors[0].count, + viewsByCountry, + recentViews, + contactStats, + interactions, + abandonments, + postStats: postStats // Returns array of { slug, totalViews, uniqueViews } + } +} diff --git a/src/actions/analytics.ts b/src/actions/analytics.ts new file mode 100644 index 00000000..1b05f344 --- /dev/null +++ b/src/actions/analytics.ts @@ -0,0 +1,75 @@ +'use server' + +import { db } from '@/server/db/connection' +import { blogPosts, blogViews } from '@/server/db/schema' +import { eq, and, sql } from 'drizzle-orm' +import { headers } from 'next/headers' +import { getClientIp } from '@/lib/protection' +import { fetchGeoInfo } from '@/lib/geo' +import { createHash } from 'crypto' + +/** + * Tracks a blog post view with robust unique visitor detection. + * Uses a fingerprint of (IP + UserAgent + DailySalt) to detect uniqueness across Incognito. + */ +export async function trackBlogView(slug: string) { + try { + const ip = await getClientIp() + const headersList = await headers() + const userAgent = headersList.get('user-agent') || 'unknown' + + + const fingerprintInput = `${ip}|${userAgent}` + const fingerprint = createHash('sha256').update(fingerprintInput).digest('hex') + + // Ensure the blog post exists in the database + await db.insert(blogPosts) + .values({ slug }) + .onConflictDoNothing() + .execute() + + const existingView = await db.query.blogViews.findFirst({ + where: and( + eq(blogViews.slug, slug), + eq(blogViews.fingerprint, fingerprint) + ), + }) + + if (existingView) { + + await db.update(blogPosts) + .set({ totalViews: sql`${blogPosts.totalViews} + 1` }) + .where(eq(blogPosts.slug, slug)) + + return { unique: false } + } + + + const geoInfo = await fetchGeoInfo(ip) + + await db.insert(blogViews).values({ + slug, + fingerprint, + ipAddress: ip, + geoCountry: geoInfo?.country, + geoCity: geoInfo?.city, + geoRegion: geoInfo?.region, + geoLoc: geoInfo?.loc, + geoOrg: geoInfo?.org, + geoTimezone: geoInfo?.timezone, + }) + + await db.update(blogPosts) + .set({ + uniqueViews: sql`${blogPosts.uniqueViews} + 1`, + totalViews: sql`${blogPosts.totalViews} + 1` + }) + .where(eq(blogPosts.slug, slug)) + + return { unique: true } + + } catch (error) { + console.error('Failed to track blog view:', error) + return { success: false } + } +} diff --git a/src/actions/auth.ts b/src/actions/auth.ts new file mode 100644 index 00000000..16972855 --- /dev/null +++ b/src/actions/auth.ts @@ -0,0 +1,29 @@ +'use server' + +import { auth } from '@/server/auth' +import { env } from '@/server/env' +import { cookies } from 'next/headers' + +const ADMIN_EMAIL = env.ADMIN_EMAIL + +export async function checkAdminStatus(): Promise { + try { + const session = await auth.api.getSession({ + headers: { + cookie: cookies().toString() + } + }) + + if (!session?.user) { + return false + } + + const isEmailMatch = session.user.email?.toLowerCase() === ADMIN_EMAIL?.toLowerCase() + const isRoleAdmin = session.user.role === 'admin' + + return isRoleAdmin || isEmailMatch + } catch (error) { + console.error('[checkAdminStatus] Error:', error) + return false + } +} diff --git a/src/actions/contact.ts b/src/actions/contact.ts new file mode 100644 index 00000000..44f250a9 --- /dev/null +++ b/src/actions/contact.ts @@ -0,0 +1,281 @@ +'use server' + +import { db } from '@/server/db/connection' +import { contactSubmissions, contactInteractions, contactAbandonments } from '@/server/db/schema' +import { z } from 'zod' +import { headers } from 'next/headers' +import { gt, and, eq, count, desc } from 'drizzle-orm' +import { cookies } from 'next/headers' +import { v4 as uuidv4 } from 'uuid' +import { getClientIp, checkHoneypot, checkRateLimit } from '@/lib/protection' +import { fetchGeoInfo } from '@/lib/geo' + +const validation = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + subject: z.string().optional(), + message: z.string().min(10, 'Message must be at least 10 characters'), + _gotcha: z.string().optional(), // Honeypot field +}) + +export async function submitContactForm(formData: FormData) { + const rawData = { + name: formData.get('name'), + email: formData.get('email'), + subject: formData.get('subject'), + message: formData.get('message'), + _gotcha: formData.get('_gotcha'), + } as any + + // Spam check: Honeypot + if (checkHoneypot(rawData._gotcha)) { + // Silently fail (pretend success) to confuse bots + return { success: true, message: 'Message sent successfully!' } + } + + const result = validation.safeParse(rawData) + + if (!result.success) { + return { success: false, errors: result.error.flatten().fieldErrors } + } + + const ip = await getClientIp() + + // Rate limit check: 3 submissions per hour + // Passing the table and columns to our reusable service + const rateLimit = await checkRateLimit( + contactSubmissions, + contactSubmissions.ipAddress, + contactSubmissions.createdAt, + ip, + { limit: 3, windowMs: 60 * 60 * 1000 } + ) + + if (!rateLimit.allowed) { + return { success: false, message: rateLimit.message } + } + + try { + const geoInfo = await fetchGeoInfo(ip) + + await db.insert(contactSubmissions).values({ + name: result.data.name, + email: result.data.email, + subject: result.data.subject || null, + message: result.data.message, + ipAddress: ip, + geoCountry: geoInfo?.country, + geoCity: geoInfo?.city, + geoRegion: geoInfo?.region, + geoLoc: geoInfo?.loc, + geoOrg: geoInfo?.org, + geoTimezone: geoInfo?.timezone, + }) + + return { success: true, message: 'Message sent successfully!' } + } catch (error) { + console.error('Failed to submit contact form:', error) + return { success: false, message: 'Something went wrong. Please try again.' } + } +} + +// Helper function to get or generate a visitor ID +async function getVisitorId(): Promise { + const cookieStore = await cookies() + const existingId = cookieStore.get('visitor_id')?.value + + if (existingId) { + return existingId + } + + const newVisitorId = uuidv4() + cookieStore.set('visitor_id', newVisitorId, { + maxAge: 60 * 60 * 24 * 365, // 1 year + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax' + }) + + return newVisitorId +} + +async function getClientInfo() { + const headersList = await headers() + return { + ip: (headersList.get('x-forwarded-for') || headersList.get('x-real-ip') || 'unknown'), + userAgent: headersList.get('user-agent') || 'unknown', + referrer: headersList.get('referer') || null + } +} + +// Track contact button click +export async function trackContactButtonClick() { + try { + const visitorId = await getVisitorId() + const clientInfo = await getClientInfo() + + const geoInfo = await fetchGeoInfo(clientInfo.ip) + + await db.insert(contactInteractions).values({ + visitorId, + interactionType: 'button_click', + ipAddress: clientInfo.ip, + userAgent: clientInfo.userAgent, + referrer: clientInfo.referrer, + geoCountry: geoInfo?.country, + geoCity: geoInfo?.city, + geoRegion: geoInfo?.region, + geoLoc: geoInfo?.loc, + geoOrg: geoInfo?.org, + geoTimezone: geoInfo?.timezone, + }) + + return { success: true } + } catch (error) { + console.error('Failed to track contact button click:', error) + return { success: false } + } +} + +// Track form start (when user starts typing) +export async function trackFormStart(sessionId?: string) { + try { + const visitorId = await getVisitorId() + const clientInfo = await getClientInfo() + + const geoInfo = await fetchGeoInfo(clientInfo.ip) + + const interaction = await db.insert(contactInteractions).values({ + visitorId, + interactionType: 'form_start', + ipAddress: clientInfo.ip, + userAgent: clientInfo.userAgent, + referrer: clientInfo.referrer, + sessionId: sessionId || null, + geoCountry: geoInfo?.country, + geoCity: geoInfo?.city, + geoRegion: geoInfo?.region, + geoLoc: geoInfo?.loc, + geoOrg: geoInfo?.org, + geoTimezone: geoInfo?.timezone, + }).returning({ id: contactInteractions.id }) + + return { success: true, interactionId: interaction[0].id } + } catch (error) { + console.error('Failed to track form start:', error) + return { success: false } + } +} + +// Track form abandonment (when user leaves form without submitting) +export async function trackFormAbandonment( + interactionId: string, + timeToAbandon: number, + lastFieldTouched?: string, + formData?: Record +) { + try { + const visitorId = await getVisitorId() + const clientInfo = await getClientInfo() + + const geoInfo = await fetchGeoInfo(clientInfo.ip) + + // Record the abandonment details + await db.insert(contactAbandonments).values({ + visitorId, + interactionId, + timeToAbandon, + lastFieldTouched: lastFieldTouched || null, + formData: formData ? JSON.stringify(formData) : null, + ipAddress: clientInfo.ip, + geoCountry: geoInfo?.country, + geoCity: geoInfo?.city, + geoRegion: geoInfo?.region, + geoLoc: geoInfo?.loc, + geoOrg: geoInfo?.org, + geoTimezone: geoInfo?.timezone, + }) + + // Also record as a general interaction for analytics + await db.insert(contactInteractions).values({ + visitorId, + interactionType: 'form_abandon', + ipAddress: clientInfo.ip, + userAgent: clientInfo.userAgent, + referrer: clientInfo.referrer, + geoCountry: geoInfo?.country, + geoCity: geoInfo?.city, + geoRegion: geoInfo?.region, + geoLoc: geoInfo?.loc, + geoOrg: geoInfo?.org, + geoTimezone: geoInfo?.timezone, + }) + + return { success: true } + } catch (error) { + console.error('Failed to track form abandonment:', error) + return { success: false } + } +} + +// Get contact form analytics +export async function getContactAnalytics() { + try { + const totalClicks = await db + .select({ count: count() }) + .from(contactInteractions) + .where(eq(contactInteractions.interactionType, 'button_click')) + + const totalFormStarts = await db + .select({ count: count() }) + .from(contactInteractions) + .where(eq(contactInteractions.interactionType, 'form_start')) + + const totalSubmissions = await db + .select({ count: count() }) + .from(contactSubmissions) + + const totalAbandonments = await db + .select({ count: count() }) + .from(contactInteractions) + .where(eq(contactInteractions.interactionType, 'form_abandon')) + + // Calculate average time to abandon + const abandonmentTimes = await db + .select({ timeToAbandon: contactAbandonments.timeToAbandon }) + .from(contactAbandonments) + + const avgTimeToAbandon = abandonmentTimes.length > 0 + ? Math.round(abandonmentTimes.reduce((sum, record) => sum + (record.timeToAbandon || 0), 0) / abandonmentTimes.length) + : 0 + + // Get abandonment by time ranges + const abandonmentRanges = { + under5s: abandonmentTimes.filter(t => (t.timeToAbandon || 0) < 5000).length, + under15s: abandonmentTimes.filter(t => (t.timeToAbandon || 0) >= 5000 && (t.timeToAbandon || 0) < 15000).length, + under30s: abandonmentTimes.filter(t => (t.timeToAbandon || 0) >= 15000 && (t.timeToAbandon || 0) < 30000).length, + over30s: abandonmentTimes.filter(t => (t.timeToAbandon || 0) >= 30000).length, + } + + return { + success: true, + data: { + totalClicks: totalClicks[0].count, + totalFormStarts: totalFormStarts[0].count, + totalSubmissions: totalSubmissions[0].count, + totalAbandonments: totalAbandonments[0].count, + avgTimeToAbandon, + abandonmentRanges, + conversionRate: totalFormStarts[0].count > 0 + ? Math.round((totalSubmissions[0].count / totalFormStarts[0].count) * 100) + : 0, + clickToStartRate: totalClicks[0].count > 0 + ? Math.round((totalFormStarts[0].count / totalClicks[0].count) * 100) + : 0, + } + } + } catch (error) { + console.error('Failed to get contact analytics:', error) + return { success: false, message: 'Failed to retrieve analytics' } + } +} diff --git a/src/app/_dev/spotify/page.tsx b/src/app/_dev/spotify/page.tsx new file mode 100644 index 00000000..6d6a432b --- /dev/null +++ b/src/app/_dev/spotify/page.tsx @@ -0,0 +1,284 @@ +'use client' + +import { Suspense, useEffect, useState } from 'react' +import { useSearchParams } from 'next/navigation' + +function SpotifyDevContent() { + const searchParams = useSearchParams() + const [authUrl, setAuthUrl] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [tokens, setTokens] = useState<{ + refresh_token: string | null; + access_token: string | null; + } | null>(null); + const [copied, setCopied] = useState(null); + + useEffect(() => { + const refreshToken = searchParams.get('refresh_token'); + const accessToken = searchParams.get('access_token'); + const urlError = searchParams.get('error'); + const details = searchParams.get('details'); + + if (urlError) { + setError(`${urlError}${details ? `: ${details}` : ''}`); + } + + if (refreshToken || accessToken) { + setTokens({ + refresh_token: refreshToken, + access_token: accessToken, + }); + } + }, [searchParams]); + + const handleGetAuthUrl = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch('/api/spotify/auth-url'); + const data = await response.json(); + + if (data.error) { + setError(data.error); + return; + } + + setAuthUrl(data.authUrl); + } catch (err) { + setError('Failed to get authorization URL'); + } finally { + setLoading(false); + } + }; + + const handleCopy = async (text: string, type: string) => { + await navigator.clipboard.writeText(text); + setCopied(type); + setTimeout(() => setCopied(null), 2000); + }; + + function handleStartAuth() { + if (authUrl) { + window.location.href = authUrl; + } + }; + + return ( +
+
+
+

+ Spotify Token Generator +

+

+ Generate a refresh token for the Spotify API integration +

+
+ + {/* Error display */} + {error && ( +
+

{error}

+
+ )} + + {/* Success: Display tokens */} + {tokens?.refresh_token && ( +
+
+

+ ✓ Authorization successful! +

+

+ Copy the refresh token below and add it to your{' '} + + .env + {' '} + file. +

+
+ +
+ handleCopy(tokens.refresh_token!, 'refresh')} + /> + + {tokens.access_token && ( + handleCopy(tokens.access_token!, 'access')} + /> + )} +
+ +
+

+ Add this to your .env{' '} + file: +

+
+                                SPOTIFY_REFRESH_TOKEN={tokens.refresh_token}
+                            
+
+
+ )} + + {/* Step-by-step flow when no tokens yet */} + {!tokens?.refresh_token && ( +
+ {/* Step 1: Get auth URL */} +
+
+
+ 1 +
+

+ Generate Authorization URL +

+
+ +

+ First, generate the Spotify authorization URL. Make sure you + have{' '} + + SPOTIFY_CLIENT_ID + {' '} + and{' '} + + SPOTIFY_CLIENT_SECRET + {' '} + in your{' '} + + .env + {' '} + file. +

+ + +
+ + {/* Step 2: Authorize with Spotify */} + {authUrl && ( +
+
+
+ 2 +
+

+ Authorize with Spotify +

+
+ +

+ Click the button below to authorize your Spotify account. + You'll be redirected back here with your refresh token. +

+ + +
+ )} +
+ )} + + {/* Requirements info */} +
+

+ Required Environment Variables +

+
    +
  • + SPOTIFY_CLIENT_ID - From + Spotify Developer Dashboard +
  • +
  • + SPOTIFY_CLIENT_SECRET - + From Spotify Developer Dashboard +
  • +
  • + SPOTIFY_REDIRECT_URI - + Should be{' '} + + {typeof window !== 'undefined' + ? `${window.location.origin}/dev/spotify` + : 'http://localhost:3000/dev/spotify'} + +
  • +
+
+
+
+ ); +} + +function TokenField({ + label, + value, + envVar, + copied, + onCopy, +}: { + label: string; + value: string; + envVar: string; + copied: boolean; + onCopy: () => void; +}) { + return ( +
+
+ {label} + {envVar} +
+
+ + +
+
+ ); +} + +function SpotifyIcon() { + return ( + + + + ) +} + +export default function SpotifyDevPage() { + return ( + Loading...}> + + + ) +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 00000000..7f37557c --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,21 @@ +import { checkAdminStatus } from "@/actions/auth"; +import { redirect } from "next/navigation"; +import { ReactNode } from "react"; + +export default async function AdminLayout({ children }: { children: ReactNode }) { + const isAdmin = await checkAdminStatus(); + + if (!isAdmin) { + redirect("/"); + } + + return ( +
+
+

Admin Dashboard

+

Welcome back, Remco.

+
+ {children} +
+ ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 00000000..8528f111 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,54 @@ +import { getAdminMetrics } from '@/actions/admin' +import { getAllBlogPosts } from '@/utils/utils' +import { BlogList } from '@/components/admin/blogs/blog-list' +import { UserMetrics } from '@/components/admin/metrics/user-metrics' +import { ContactOverview } from '@/components/admin/contact/contact-overview' + +export const dynamic = 'force-dynamic' + +export default async function AdminPage() { + const metrics = await getAdminMetrics() + const allPosts = getAllBlogPosts() + + const statsMap = new Map(metrics.postStats.map(s => [s.slug, s])) + + const postsWithStats = allPosts.map(post => { + const stats = statsMap.get(post.slug) + return { + ...post, + totalViews: stats?.totalViews || 0, + uniqueViews: stats?.uniqueViews || 0 + } + }) + + // Better approach: Let's fetch the view counts for ALL posts in one go in getAdminMetrics + // But for now, let's render the structure first. + + return ( +
+
+ {/* Left Column: Blog Management (2/3 width) */} +
+ + +
+ + {/* More admin widgets can go here */} +
+
+ + {/* Right Column: Analytics & Metrics (1/3 width) */} +
+ +
+
+
+ ); +} + diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 00000000..20d1b0e1 --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/server/auth" +import { toNextJsHandler } from "better-auth/next-js" + +export const { GET, POST } = toNextJsHandler(auth) diff --git a/src/app/api/example/route.ts b/src/app/api/example/route.ts new file mode 100644 index 00000000..47504371 --- /dev/null +++ b/src/app/api/example/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from 'next/server' +import PostHogClient from '@/app/posthog' + +export async function POST(request: Request) { + const posthog = PostHogClient() + + try { + const data = await request.json() + + // Capture an event + posthog.capture({ + distinctId: 'user-id', // Replace with actual user ID from auth + event: 'api_endpoint_called', + properties: { + endpoint: '/api/example', + data: data, + }, + }) + + return NextResponse.json({ success: true, message: 'Event captured successfully' }) + } catch (error) { + return NextResponse.json({ error: 'Failed to capture event' }, { status: 500 }) + } finally { + // IMPORTANT: Always shutdown to flush events + await posthog.shutdown() + } +} \ No newline at end of file diff --git a/src/app/api/github/activity/route.ts b/src/app/api/github/activity/route.ts new file mode 100644 index 00000000..0e8b8895 --- /dev/null +++ b/src/app/api/github/activity/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { githubService } from '@/server/services/github'; + +export const revalidate = 60; // Cache for 1 minute + +export async function GET() { + try { + const activity = await githubService.getRecentActivity(10); + return NextResponse.json(activity); + } catch (error) { + console.error('Error in /api/github/activity:', error); + return NextResponse.json( + { error: 'Failed to fetch GitHub activity' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/github/commits/route.ts b/src/app/api/github/commits/route.ts index 05f30442..fe071b21 100644 --- a/src/app/api/github/commits/route.ts +++ b/src/app/api/github/commits/route.ts @@ -18,7 +18,6 @@ interface GitHubCommit { }; } -// Get latest commit for a repository export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); @@ -43,12 +42,10 @@ export async function GET(request: NextRequest) { headers['Authorization'] = `token ${token}`; } - // Fetch latest commit for the repository const response = await fetch( `${GITHUB_API_BASE}/repos/${owner}/${repo}/commits?per_page=1`, { headers, - // Cache for 5 minutes to avoid rate limiting next: { revalidate: 300 } } ); @@ -56,7 +53,6 @@ export async function GET(request: NextRequest) { if (!response.ok) { const rateLimitRemaining = response.headers.get('x-ratelimit-remaining'); - // For 404 (repository not found), return gracefully if (response.status === 404) { console.log(`Repository ${owner}/${repo} not found (404)`); return NextResponse.json({ diff --git a/src/app/api/github/contributions/route.ts b/src/app/api/github/contributions/route.ts new file mode 100644 index 00000000..a8de32fd --- /dev/null +++ b/src/app/api/github/contributions/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server' +import { githubService } from '@/server/services/github' + +export const dynamic = 'force-dynamic' + +async function fetchWithRetry(year: number, maxRetries = 3) { + let lastError: Error | null = null + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const contributions = await githubService.getDailyContributions(year) + return contributions + } catch (error) { + lastError = error as Error + console.warn(`GitHub API attempt ${attempt + 1} failed:`, error) + + if (attempt < maxRetries - 1) { + const delay = Math.min(1000 * Math.pow(2, attempt), 5000) + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + } + + throw lastError +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const yearParam = searchParams.get('year') + const year = yearParam ? parseInt(yearParam) : new Date().getFullYear() + + const contributions = await fetchWithRetry(year) + + const contributionsArray = Array.from(contributions.entries()).map(([date, data]) => ({ + date, + data + })) + + return NextResponse.json(contributionsArray) + } catch (error) { + console.error('Error in /api/github/contributions after retries:', error) + return NextResponse.json( + { error: 'Failed to fetch GitHub contributions' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/github/events/route.ts b/src/app/api/github/events/route.ts new file mode 100644 index 00000000..72b2553b --- /dev/null +++ b/src/app/api/github/events/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server' +import { githubService } from '@/server/services/github' + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const from = searchParams.get('from'); + const to = searchParams.get('to'); + + if (!from || !to) { + return NextResponse.json( + { error: 'Missing required parameters: from, to' }, + { status: 400 } + ); + } + + const events = await githubService.getDetailedEvents(from, to); + return NextResponse.json(events); + } catch (error) { + console.error('Error in /api/github/events:', error); + return NextResponse.json( + { error: 'Failed to fetch GitHub events' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/github/repo/route.ts b/src/app/api/github/repo/route.ts new file mode 100644 index 00000000..fdf4ddf2 --- /dev/null +++ b/src/app/api/github/repo/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const GITHUB_API_BASE = 'https://api.github.com'; + +// Language colors from GitHub +const LANGUAGE_COLORS: Record = { + TypeScript: '#3178c6', + JavaScript: '#f1e05a', + Python: '#3572A5', + Rust: '#dea584', + Go: '#00ADD8', + Java: '#b07219', + 'C++': '#f34b7d', + C: '#555555', + 'C#': '#178600', + Ruby: '#701516', + PHP: '#4F5D95', + Swift: '#F05138', + Kotlin: '#A97BFF', + Dart: '#00B4AB', + Shell: '#89e051', + HTML: '#e34c26', + CSS: '#563d7c', + SCSS: '#c6538c', + Vue: '#41b883', + Svelte: '#ff3e00', + Astro: '#ff5a03', + MDX: '#fcb32c', +}; + +function getHeaders(): Record { + const headers: Record = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'remcostoeten-portfolio-activity', + }; + + const token = process.env.GITHUB_TOKEN || process.env.NEXT_PUBLIC_GITHUB_TOKEN; + if (token) { + headers['Authorization'] = `token ${token}`; + } + + return headers; +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const owner = searchParams.get('owner'); + const repo = searchParams.get('repo'); + + if (!owner || !repo) { + return NextResponse.json({ error: 'Missing owner or repo parameter' }, { status: 400 }); + } + + try { + const [repoRes, langRes] = await Promise.all([ + fetch(`${GITHUB_API_BASE}/repos/${owner}/${repo}`, { + headers: getHeaders(), + next: { revalidate: 300 } + }), + fetch(`${GITHUB_API_BASE}/repos/${owner}/${repo}/languages`, { + headers: getHeaders(), + next: { revalidate: 300 }, + }), + ]); + + if (!repoRes.ok) { + return NextResponse.json( + { error: `GitHub API error: ${repoRes.status}` }, + { status: repoRes.status } + ); + } + + const repoData = await repoRes.json(); + const langData = langRes.ok ? await langRes.json() : {}; + + const totalBytes: number = Object.values(langData as Record).reduce((sum, val) => sum + val, 0); + const languages = Object.entries(langData as Record) + .map(([name, bytes]) => ({ + name, + color: LANGUAGE_COLORS[name] || '#6e7681', + percentage: totalBytes > 0 ? (bytes / totalBytes) * 100 : 0, + })) + .sort((a, b) => b.percentage - a.percentage) + .slice(0, 5); + + return NextResponse.json({ + name: repoData.name, + fullName: repoData.full_name, + description: repoData.description, + url: repoData.html_url, + topics: repoData.topics || [], + languages, + stars: repoData.stargazers_count, + forks: repoData.forks_count, + isPrivate: repoData.private, + owner: { + login: repoData.owner?.login, + avatarUrl: repoData.owner?.avatar_url, + }, + }); + } catch (error) { + console.error('Error fetching repo data:', error); + return NextResponse.json({ error: 'Failed to fetch repository data' }, { status: 500 }); + } +} diff --git a/src/app/api/spotify-recent/route.ts b/src/app/api/spotify-recent/route.ts deleted file mode 100644 index c5b8b149..00000000 --- a/src/app/api/spotify-recent/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const limit = searchParams.get('limit') || '10'; - - try { - // Get the access token from the Authorization header - const authHeader = request.headers.get('authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return NextResponse.json( - { error: 'Missing or invalid authorization header' }, - { status: 401 } - ); - } - - const accessToken = authHeader.substring(7); - - const response = await fetch( - `https://api.spotify.com/v1/me/player/recently-played?limit=${limit}`, - { - headers: { - 'Authorization': `Bearer ${accessToken}` - } - } - ); - - if (!response.ok) { - const errorData = await response.text(); - console.error('Spotify recent tracks error:', errorData); - return NextResponse.json( - { error: 'Failed to fetch recent tracks' }, - { status: response.status } - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (error) { - console.error('Spotify recent tracks error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} diff --git a/src/app/api/spotify-token/route.ts b/src/app/api/spotify-token/route.ts deleted file mode 100644 index 7f79133f..00000000 --- a/src/app/api/spotify-token/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - const clientId = process.env.SPOTIFY_CLIENT_ID; - const clientSecret = process.env.SPOTIFY_CLIENT_SECRET; - - if (!clientId || !clientSecret) { - return NextResponse.json( - { error: 'Spotify credentials not configured' }, - { status: 500 } - ); - } - - try { - const body = await request.text(); - const params = new URLSearchParams(body); - - const response = await fetch('https://accounts.spotify.com/api/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` - }, - body: params - }); - - if (!response.ok) { - const errorData = await response.text(); - console.error('Spotify token error:', errorData); - return NextResponse.json( - { error: 'Failed to get access token' }, - { status: response.status } - ); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (error) { - console.error('Spotify token error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} diff --git a/src/app/api/spotify/callback/route.ts b/src/app/api/spotify/callback/route.ts index e6714f0e..8f0272e7 100644 --- a/src/app/api/spotify/callback/route.ts +++ b/src/app/api/spotify/callback/route.ts @@ -12,11 +12,11 @@ export async function GET(request: NextRequest) { const error = searchParams.get('error'); if (error) { - return NextResponse.redirect(new URL('/spotify-setup?error=' + error, request.url)); + return NextResponse.redirect(new URL('/?error=' + error, request.url)); } if (!code) { - return NextResponse.redirect(new URL('/spotify-setup?error=no_code', request.url)); + return NextResponse.redirect(new URL('/?error=no_code', request.url)); } const clientId = process.env.SPOTIFY_CLIENT_ID; @@ -24,10 +24,9 @@ export async function GET(request: NextRequest) { const redirectUri = process.env.SPOTIFY_REDIRECT_URI; if (!clientId || !clientSecret) { - return NextResponse.redirect(new URL('/spotify-setup?error=missing_credentials', request.url)); + return NextResponse.redirect(new URL('/?error=missing_credentials', request.url)); } - // Exchange code for tokens const authString = `${clientId}:${clientSecret}`; const base64Auth = Buffer.from(authString).toString('base64'); @@ -46,13 +45,12 @@ export async function GET(request: NextRequest) { if (!tokenResponse.ok) { const errorData = await tokenResponse.json(); - return NextResponse.redirect(new URL(`/spotify-setup?error=token_exchange_failed&details=${errorData.error_description || errorData.error}`, request.url)); + return NextResponse.redirect(new URL(`/?error=token_exchange_failed&details=${errorData.error_description || errorData.error}`, request.url)); } const tokenData = await tokenResponse.json(); - // Redirect to setup page with tokens (only showing refresh token for security) - const redirectUrl = new URL('/spotify-setup', request.url); + const redirectUrl = new URL('/dev/spotify', request.url); redirectUrl.searchParams.set('success', 'true'); redirectUrl.searchParams.set('refresh_token', tokenData.refresh_token); redirectUrl.searchParams.set('access_token', tokenData.access_token); @@ -60,6 +58,6 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(redirectUrl); } catch (error) { console.error('Error in Spotify callback:', error); - return NextResponse.redirect(new URL('/spotify-setup?error=unknown_error', request.url)); + return NextResponse.redirect(new URL('/?error=unknown_error', request.url)); } } \ No newline at end of file diff --git a/src/app/api/spotify/now-playing/route.ts b/src/app/api/spotify/now-playing/route.ts new file mode 100644 index 00000000..ec06cfcc --- /dev/null +++ b/src/app/api/spotify/now-playing/route.ts @@ -0,0 +1,90 @@ +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +const SPOTIFY_API_BASE = 'https://api.spotify.com/v1'; +const SPOTIFY_ACCOUNTS_BASE = 'https://accounts.spotify.com'; + +export async function GET(request: NextRequest) { + try { + const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN; + + if (!refreshToken || refreshToken === 'your_refresh_token_here' || refreshToken.startsWith('#')) { + return NextResponse.json({ isPlaying: false, message: 'No refresh token configured' }); + } + + const clientId = process.env.SPOTIFY_CLIENT_ID; + const clientSecret = process.env.SPOTIFY_CLIENT_SECRET; + + if (!clientId || !clientSecret) { + return NextResponse.json({ error: 'Missing Spotify credentials' }, { status: 500 }); + } + + const authString = `${clientId}:${clientSecret}`; + const base64Auth = Buffer.from(authString).toString('base64'); + + const tokenResponse = await fetch(`${SPOTIFY_ACCOUNTS_BASE}/api/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${base64Auth}` + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken + }) + }); + + const tokenData = await tokenResponse.json(); + + if (tokenData.error) { + console.error('Error refreshing Spotify token for now playing:', tokenData.error); + return NextResponse.json({ error: tokenData.error }, { status: 400 }); + } + + const accessToken = tokenData.access_token; + + const nowPlayingResponse = await fetch(`${SPOTIFY_API_BASE}/me/player/currently-playing`, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + + if (nowPlayingResponse.status === 204 || nowPlayingResponse.status > 400) { + return NextResponse.json({ isPlaying: false }); + } + + const data = await nowPlayingResponse.json(); + + if (!data.item) { + return NextResponse.json({ isPlaying: false }); + } + + const isPlaying = data.is_playing; + const progressMs = data.progress_ms; + const item = data.item; + + const track = { + id: item.id, + name: item.name, + artist: item.artists.map((artist: any) => artist.name).join(', '), + album: item.album.name, + url: item.external_urls.spotify, + image: item.album.images[0]?.url || '', + played_at: new Date().toISOString(), + duration_ms: item.duration_ms + }; + + return NextResponse.json({ + isPlaying, + progress_ms: progressMs, + duration_ms: item.duration_ms, + track + }); + + } catch (error) { + console.error('Error in Spotify now playing API:', error); + return NextResponse.json({ error: 'Failed to fetch now playing' }, { status: 500 }); + } +} diff --git a/src/app/api/spotify/recent/route.ts b/src/app/api/spotify/recent/route.ts index 7d4c8274..a0896b00 100644 --- a/src/app/api/spotify/recent/route.ts +++ b/src/app/api/spotify/recent/route.ts @@ -6,7 +6,6 @@ export const dynamic = 'force-dynamic'; const SPOTIFY_API_BASE = 'https://api.spotify.com/v1'; const SPOTIFY_ACCOUNTS_BASE = 'https://accounts.spotify.com'; -// Get recently played tracks export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); @@ -19,7 +18,6 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'No refresh token configured' }, { status: 404 }); } - // Get fresh access token const clientId = process.env.SPOTIFY_CLIENT_ID; const clientSecret = process.env.SPOTIFY_CLIENT_SECRET; @@ -51,7 +49,6 @@ export async function GET(request: NextRequest) { const accessToken = tokenData.access_token; - // Get recent tracks const recentResponse = await fetch(`${SPOTIFY_API_BASE}/me/player/recently-played?limit=${limit}`, { headers: { 'Authorization': `Bearer ${accessToken}` diff --git a/src/app/api/spotify/token/route.ts b/src/app/api/spotify/token/route.ts index 1d8d3b02..2004ca96 100644 --- a/src/app/api/spotify/token/route.ts +++ b/src/app/api/spotify/token/route.ts @@ -4,7 +4,6 @@ export const dynamic = 'force-dynamic'; const SPOTIFY_ACCOUNTS_BASE = 'https://accounts.spotify.com'; -// Exchange authorization code for tokens export async function POST(request: Request) { try { const { code } = await request.json(); diff --git a/src/app/api/sync/route.ts b/src/app/api/sync/route.ts new file mode 100644 index 00000000..cd5cbbd2 --- /dev/null +++ b/src/app/api/sync/route.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from 'next/server' +import { syncAll, syncGitHubActivities, syncSpotifyListens, getSyncStatus } from '@/server/services/activity-sync' +import { isAdmin } from '@/utils/is-admin' + +export const dynamic = 'force-dynamic' +export const maxDuration = 60 + +/** + * GET /api/sync - Get sync status + * POST /api/sync - Trigger a sync (admin or cron only) + * POST /api/sync?service=github - Sync only GitHub + * POST /api/sync?service=spotify - Sync only Spotify + */ + +export async function GET() { + try { + const status = await getSyncStatus() + return NextResponse.json(status) + } catch (error) { + console.error('Error getting sync status:', error) + return NextResponse.json( + { error: 'Failed to get sync status' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const authHeader = request.headers.get('authorization') + const cronSecret = process.env.CRON_SECRET + const isCronRequest = cronSecret && authHeader === `Bearer ${cronSecret}` + + const isVercelCron = request.headers.get('x-vercel-cron') === '1' + + if (!isCronRequest && !isVercelCron) { + const userIsAdmin = await isAdmin() + if (!userIsAdmin) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + } + + const service = request.nextUrl.searchParams.get('service') + + console.log(`[Sync] Starting sync... (service: ${service || 'all'})`) + const startTime = Date.now() + + let result + if (service === 'github') { + result = { github: await syncGitHubActivities() } + } else if (service === 'spotify') { + result = { spotify: await syncSpotifyListens() } + } else { + result = await syncAll() + } + + const duration = Date.now() - startTime + console.log(`[Sync] Completed in ${duration}ms`, result) + + return NextResponse.json({ + success: true, + duration, + ...result, + }) + } catch (error) { + console.error('Sync error:', error) + return NextResponse.json( + { error: 'Sync failed', message: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/src/app/auth/callback/page.tsx b/src/app/auth/callback/page.tsx new file mode 100644 index 00000000..a802fdc9 --- /dev/null +++ b/src/app/auth/callback/page.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { useEffect, Suspense } from 'react'; +import { useSearchParams } from 'next/navigation'; + +function OAuthCallbackContent() { + const searchParams = useSearchParams(); + + useEffect(() => { + // Check if we have an error or success + const error = searchParams.get('error'); + const code = searchParams.get('code'); + + if (window.opener) { + if (error) { + // Send error message to parent window + window.opener.postMessage( + { + type: 'oauth-error', + error: error, + }, + window.location.origin + ); + } else if (code) { + // Send success message to parent window + window.opener.postMessage( + { + type: 'oauth-success', + }, + window.location.origin + ); + } + + // Close the popup after a short delay + setTimeout(() => { + window.close(); + }, 500); + } + }, [searchParams]); + + return ( +
+
+
+ + + + +
+

+ Completing authentication... +

+
+
+ ); +} + +export default function OAuthCallbackPage() { + return ( + +
+
+ + + + +
+

+ Loading... +

+
+ + }> + +
+ ); +} diff --git a/src/app/blog/[...slug]/page.tsx b/src/app/blog/[...slug]/page.tsx index 218d7f98..8aca723c 100644 --- a/src/app/blog/[...slug]/page.tsx +++ b/src/app/blog/[...slug]/page.tsx @@ -1,24 +1,42 @@ import { notFound } from 'next/navigation' -import { getBlogPosts, calculateReadTime } from '@/utils/utils' +import { getBlogPosts, getAllBlogPosts, calculateReadTime } from '@/utils/utils' import { baseUrl } from '@/app/sitemap' -import { CustomMDX } from '@/components/mdx' -import { BlogPostClient, PostNavigation } from '@/components/blog-post-client' +import { CustomMDX } from '@/components/blog/mdx' +import { BlogPostClient, PostNavigation } from '@/components/blog/post-view' +import { TableOfContents } from '@/components/blog/table-of-contents' +import { ReactionBar } from '@/components/blog/reaction-bar' +import { CommentSection } from '@/components/blog/comment-section' +import { checkAdminStatus } from '@/actions/auth' +import { PostAdminControls } from '@/components/blog/post-admin-controls' +import { BlogPostStructuredData } from '@/components/seo/structured-data' +import { db } from '@/server/db/connection' +import { blogPosts } from '@/server/db/schema' +import { eq } from 'drizzle-orm' + +// Force dynamic rendering due to auth requirements +export const dynamic = 'force-dynamic' export async function generateStaticParams() { let posts = getBlogPosts() - return posts.map((post) => ({ - slug: post.slug.split('/'), // Convert string slug to array for catch-all route - })) + return posts + .filter(post => post && post.slug) + .map((post) => ({ + slug: post.slug.split('/'), + })) } -export async function generateMetadata({ params }) { +export async function generateMetadata({ params }: { params: Promise<{ slug: string | string[] }> }) { const resolvedParams = await params - // Join the slug array to create the full slug path let slug = Array.isArray(resolvedParams.slug) ? resolvedParams.slug.join('/') : resolvedParams.slug + + if (!slug) { + return {} + } + let post = getBlogPosts().find((post) => post.slug === slug) if (!post) { return {} @@ -58,77 +76,100 @@ export async function generateMetadata({ params }) { } } -export default async function Blog({ params }) { +// No dynamic imports in this file - moved to client component + +export default async function Blog({ params }: { params: Promise<{ slug: string | string[] }> }) { const resolvedParams = await params - // Join the slug array to create the full slug path let slug = Array.isArray(resolvedParams.slug) ? resolvedParams.slug.join('/') : resolvedParams.slug - const allPosts = getBlogPosts() - const post = allPosts.find((post) => post.slug === slug) + if (!slug) { + notFound() + } + + // Get all posts including drafts for now + // We'll filter client-side based on admin status + const allPosts = getAllBlogPosts() + const post = allPosts.find((p) => p.slug === slug) if (!post) { notFound() } - // Find prev/next posts for navigation + // Check if user is admin on the server side for initial render + const isAdminUser = await checkAdminStatus() + + if (post.metadata.draft && !isAdminUser) { + notFound() + } + + // Fetch view counts + const viewData = await db.query.blogPosts.findFirst({ + where: eq(blogPosts.slug, slug), + columns: { + uniqueViews: true, + totalViews: true + } + }) + const currentIndex = allPosts.findIndex(p => p.slug === slug) const prevPost = currentIndex < allPosts.length - 1 ? allPosts[currentIndex + 1] : null const nextPost = currentIndex > 0 ? allPosts[currentIndex - 1] : null return ( -
-