Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ 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)
# GitHub (for server-side API calls)
# ============================================================
NEXT_PUBLIC_GITHUB_TOKEN="" # Personal access token for GitHub API
GITHUB_TOKEN="" # Personal access token for GitHub API (server only)

# ============================================================
# 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"
SPOTIFY_REDIRECT_URI="http://127.0.0.1:3000/api/spotify/callback"

# ============================================================
# IP info service
Expand All @@ -59,4 +59,4 @@ IP_INFO_TOKEN="" # Token from https://ipinfo.io
# ============================================================
# Vercel Cron
# ============================================================
CRON_SECRET="" # Secret key for cron jobs
CRON_SECRET="" # Secret key for cron jobs
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ setup-posthog.md

.env.production
env.production
.lh-tmp/*

30,790 changes: 30,790 additions & 0 deletions .lighthouse-desktop.json

Large diffs are not rendered by default.

30,763 changes: 30,763 additions & 0 deletions .lighthouse-mobile.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions ADMIN_SECTION_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,9 @@ className="p-2.5 text-zinc-500 hover:text-zinc-300 disabled:opacity-30"
- `/src/components/projects/admin/project-editor.tsx` - Project editor form

**Auth Files:**
- `/src/actions/auth.ts` - Cookie-based auth check (redundant)
- `/src/lib/auth-guard.ts` - Alternative auth check (redundant)
- `/src/utils/is-admin.ts` - Server utility auth check (active?)
- `/src/actions/auth.ts` - Cookie-based auth check (active)
- `/src/lib/auth-guard.ts` - Alternative auth check (active)
- `/src/utils/is-admin.ts` - Server utility auth check (active)

---

Expand Down
77 changes: 68 additions & 9 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ const nextConfig = {
return [
{
source: '/categories',
destination: '/blog/categories',
destination: '/blog/topics',
permanent: true
},
{
source: '/categories/:slug*',
destination: '/blog/categories/:slug*',
destination: '/blog/topics/:slug*',
permanent: true
},
{
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"drizzle-orm": "^0.45.1",
"framer-motion": "^12.23.25",
"geist": "1.2.2",
"glob": "^13.0.3",
"lucide-react": "^0.562.0",
"next": "16",
"next-mdx-remote": "^6.0.0",
Expand Down
Binary file modified public/remco-stoeten-frontend-engineer-resume.pdf
Binary file not shown.
33 changes: 2 additions & 31 deletions src/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,7 @@
'use server'

import { auth } from '@/server/auth'
import { env } from '@/server/env'
import { cookies } from 'next/headers'

const ADMIN_EMAIL = env.ADMIN_EMAIL
import { isAdmin } from '@/utils/is-admin'

export async function checkAdminStatus(): Promise<boolean> {
try {
const cookieStore = await cookies()
const cookieHeader = cookieStore
.getAll()
.map(cookie => `${cookie.name}=${cookie.value}`)
.join('; ')

const session = await auth.api.getSession({
headers: {
cookie: cookieHeader
}
})

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
}
return isAdmin()
}
8 changes: 4 additions & 4 deletions src/app/(admin)/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,19 @@ export default async function AdminPage() {

<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-4 md:w-auto md:inline-flex">
<TabsTrigger value="overview" className="gap-2">
<TabsTrigger value="overview" className="gap-2" aria-label="Dashboard Overview">
<LayoutDashboard className="w-4 h-4 hidden md:block" />
Overview
</TabsTrigger>
<TabsTrigger value="blogs" className="gap-2">
<TabsTrigger value="blogs" className="gap-2" aria-label="Manage Blogs">
<FileText className="w-4 h-4 hidden md:block" />
Blogs
</TabsTrigger>
<TabsTrigger value="contact" className="gap-2">
<TabsTrigger value="contact" className="gap-2" aria-label="Contact Messages">
<Mail className="w-4 h-4 hidden md:block" />
Contact
</TabsTrigger>
<TabsTrigger value="analytics" className="gap-2">
<TabsTrigger value="analytics" className="gap-2" aria-label="Site Analytics">
<BarChart3 className="w-4 h-4 hidden md:block" />
Analytics
</TabsTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: 'STOP USING ARROW FUNCTIONS'
publishedAt: '11-02-2026'
updatedAt: '11-02-2026'
Comment on lines +3 to +4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find all blog post files and their date formats
rg -n "publishedAt:|updatedAt:" --type=md -A0 | head -40

Repository: remcostoeten/remcostoeten.nl

Length of output: 1624


🏁 Script executed:

# Also check if there are any published posts (not drafts) to see the format pattern
fd "\.md$" src/app/\(marketing\)/blog/posts --type f | grep -v drafts | xargs grep -H "publishedAt:" | head -20

Repository: remcostoeten/remcostoeten.nl

Length of output: 824


🏁 Script executed:

# Search for date parsing or formatting logic in the codebase
rg "publishedAt|updatedAt|date.*format" --type=ts --type=tsx --type=js --type=jsx -i | grep -i "parse\|format\|date" | head -20

Repository: remcostoeten/remcostoeten.nl

Length of output: 99


🏁 Script executed:

# Search for date parsing/formatting in TypeScript/JavaScript files
rg "publishedAt|parseDate|formatDate|Date\(" --type=ts --type=js -B2 -A2 | grep -v "node_modules" | head -50

Repository: remcostoeten/remcostoeten.nl

Length of output: 2411


🏁 Script executed:

# Check if there's any date handling in the blog component or metadata processing
fd -e ts -e js src/app/\(marketing\)/blog | xargs grep -l "Date\|parse\|format" 2>/dev/null | head -10

Repository: remcostoeten/remcostoeten.nl

Length of output: 473


🏁 Script executed:

# Read the formatDate function in full
sed -n '1,300p' src/utils/utils.ts | grep -A 30 "export function formatDate"

Repository: remcostoeten/remcostoeten.nl

Length of output: 864


🏁 Script executed:

# Check if there are any tests or examples showing how dates should be formatted
fd -e test -e spec src | head -5
rg "formatDate\|publishedAt.*=" --type=ts --type=js -B1 -A1 | head -30

Repository: remcostoeten/remcostoeten.nl

Length of output: 54


Change date format to YYYY-MM-DD to match other blog posts and enable proper parsing.

The date '11-02-2026' doesn't follow the ISO 8601 format (YYYY-MM-DD) used consistently throughout other blog posts (e.g., '2025-12-31', '2024-12-30'). The formatDate() function in src/utils/utils.ts relies on JavaScript's Date constructor, which expects ISO 8601 format and will misparsé or fail on '11-02-2026T00:00:00'. Change to '2026-02-11' (or '2026-11-02' if intended for November).

🤖 Prompt for AI Agents
In
`@src/app/`(marketing)/blog/posts/drafts/stop-using-arrow-fnc-and-HUGE-REVEAL.md
around lines 3 - 4, The publishedAt and updatedAt date strings in the draft use
'11-02-2026' which is not ISO (YYYY-MM-DD); update both publishedAt and
updatedAt to ISO 8601 format (e.g., '2026-02-11' or '2026-11-02' depending on
intended month/day) so the formatDate() logic and Date constructor parse them
correctly; locate the keys publishedAt and updatedAt in the post frontmatter and
replace the values with the correct YYYY-MM-DD strings.

summary: 'Arrow functions are convenient. That does not make them good architecture.'
tags: ['JavaScript', 'Engineering', 'Yappin']
author: 'Remco Stoeten'
canonicalUrl: 'https://remcostoeten.nl/blog/stop-using-arrow-functions'
slug: 'stop-using-arrow-functions'
draft: true
---

Arrow functions are convenient.

That’s it. That’s the whole pitch.

They aren’t hoisted.
They don’t have stable names.
They can’t be overloaded.
They don’t participate in prototypes.
They replace immutable symbols with mutable variables.

Yet somehow they became the default.

And then I see this:

```js
export const Foo = () => {
return <Bar />
}
```

Why.

Compare:

```js
function Foo() {
return <Bar />
}

const Foo = () => {
return <Bar />
}
```

Same JSX.

Different semantics.

The first is a hoisted, named function declaration.

The second is a variable holding an anonymous closure.

“But arrows bind `this` automatically.”

Exactly. Hidden behavior.

Great for callbacks.

Bad for architecture.

Arrow functions optimize for keystrokes.

Function declarations optimize for systems.

If your codebase is bigger than a toy project, stop pretending they’re interchangeable.
15 changes: 15 additions & 0 deletions src/app/(marketing)/dev/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ReactNode } from 'react'
import { notFound } from 'next/navigation'
import { canAccessDevTools } from '@/lib/dev-access'

export const dynamic = 'force-dynamic'

export default async function DevLayout({ children }: { children: ReactNode }) {
const canAccess = await canAccessDevTools()

if (!canAccess) {
notFound()
}

return children
}
56 changes: 46 additions & 10 deletions src/app/(marketing)/dev/spotify/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,56 @@ function SpotifyDevContent() {
const [copied, setCopied] = useState<string | null>(null)

useEffect(() => {
const refreshToken = searchParams.get('refresh_token')
const accessToken = searchParams.get('access_token')
const urlError = searchParams.get('error')
const details = searchParams.get('details')
const authSuccess = searchParams.get('success') === 'true'

if (urlError) {
setError(`${urlError}${details ? `: ${details}` : ''}`)
}

if (refreshToken || accessToken) {
setTokens({
refresh_token: refreshToken,
access_token: accessToken
})
if (!authSuccess) {
return
}

let isActive = true

const loadTokens = async () => {
try {
const response = await fetch('/api/spotify/dev-token', {
cache: 'no-store'
})

if (!response.ok) {
throw new Error('Failed to read OAuth tokens')
}

const data = await response.json()
if (!isActive) return

if (!data.refresh_token && !data.access_token) {
setError(
'No token found. Please retry the authorization flow.'
)
return
}

setTokens({
refresh_token: data.refresh_token ?? null,
access_token: data.access_token ?? null
})

window.history.replaceState({}, '', '/dev/spotify')
} catch {
if (!isActive) return
setError('Failed to load generated tokens')
}
}

loadTokens()

return () => {
isActive = false
}
}, [searchParams])

Expand All @@ -45,7 +81,7 @@ function SpotifyDevContent() {
}

setAuthUrl(data.authUrl)
} catch (err) {
} catch {
setError('Failed to get authorization URL')
} finally {
setLoading(false)
Expand Down Expand Up @@ -236,8 +272,8 @@ function SpotifyDevContent() {
- Should be{' '}
<code className="text-zinc-400">
{typeof window !== 'undefined'
? `${window.location.origin}/dev/spotify`
: 'http://localhost:3000/dev/spotify'}
? `${window.location.origin}/api/spotify/callback`
: 'http://localhost:3000/api/spotify/callback'}
</code>
</li>
</ul>
Expand Down
35 changes: 18 additions & 17 deletions src/app/(marketing)/legal/legal-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,22 @@ export function LegalHeader({ language, onLanguageChange }: HeaderProps) {

return (
<div
className={`sticky top-0 z-30 border-b border-border/60 bg-background/90 backdrop-blur transition-transform duration-200 ${
isHidden
? '-translate-y-full md:translate-y-0'
: 'translate-y-0'
}`}
className={`sticky top-0 z-30 border-b border-border/60 bg-background/90 backdrop-blur transition-transform duration-200 ${isHidden
? '-translate-y-full md:translate-y-0'
: 'translate-y-0'
}`}
>
<div className="flex items-center justify-between gap-4 px-4 py-3 md:px-6">
<div className="flex items-center justify-between gap-4 px-4 py-3 md:px-5">
<div className="flex flex-wrap items-center gap-3">
{/* Navigation between legal pages */}


<Breadcrumbs />
</div>
<div className="flex items-center gap-2">
{/* Navigation between legal pages */}
{pathname === '/terms' && (
<Button asChild variant="ghost" size="sm">
<Button asChild variant="ghost" size="sm" className="h-7 text-xs">
<Link
href={`/privacy${searchParams.toString() ? `?${searchParams.toString()}` : ''}`}
>
Expand All @@ -65,18 +70,15 @@ export function LegalHeader({ language, onLanguageChange }: HeaderProps) {
</Button>
)}
{pathname === '/privacy' && (
<Button asChild variant="ghost" size="sm">
<Button asChild variant="ghost" size="sm" className="h-7 text-xs">
<Link
href={`/terms${searchParams.toString() ? `?${searchParams.toString()}` : ''}`}
>
Terms
</Link>
</Button>
)}

<Breadcrumbs />
</div>
<div className="flex items-center gap-2">
<div className="w-px h-4 bg-border/50 mx-1" />
<LanguageToggle
label="EN"
isActive={language === 'en'}
Expand Down Expand Up @@ -125,11 +127,10 @@ function LanguageToggle({ label, isActive, onClick }: ToggleProps) {
type="button"
onClick={onClick}
aria-pressed={isActive}
className={`rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-wide transition-colors ${
isActive
? 'border-primary/70 bg-primary text-primary-foreground shadow-sm'
: 'border-border/70 bg-background hover:border-primary/50 hover:text-foreground'
}`}
className={`rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-wide transition-colors ${isActive
? 'border-primary/70 bg-primary text-primary-foreground shadow-sm'
: 'border-border/70 bg-background hover:border-primary/50 hover:text-foreground'
}`}
>
{label}
</button>
Expand Down
6 changes: 1 addition & 5 deletions src/app/api/activity/combined/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ function getGitHubHeaders(): Record<string, string> {
Accept: 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
}
// Check both env vars to match the main GitHubService
const token =
process.env.GITHUB_TOKEN || process.env.NEXT_PUBLIC_GITHUB_TOKEN
const token = process.env.GITHUB_TOKEN
if (token) {
headers['Authorization'] = `token ${token.trim()}`
}
Expand Down Expand Up @@ -189,7 +187,6 @@ function parseGitHubEvent(event: any) {
}
}
case 'PullRequestEvent': {
const action = event.payload?.action
const prNumber = event.payload?.number
const prTitle = event.payload?.pull_request?.title?.trim() || ''
const prBody =
Expand Down Expand Up @@ -233,7 +230,6 @@ function parseGitHubEvent(event: any) {
}
}
case 'IssuesEvent': {
const action = event.payload?.action
const issueNumber = event.payload?.issue?.number
const issueTitle = event.payload?.issue?.title?.trim() || ''
const issueBody =
Expand Down
Loading