Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ src/
- Node.js 22+
- npm
- Optional: GitHub personal access token for Projects section
- Modern browser with `IntersectionObserver` support (Chrome 51+, Firefox 55+, Safari 12.1+, Edge 15+, and modern mobile browsers like iOS Safari 12.2+, Chrome for Android 51+) for animations

### 1. Clone

Expand Down
20 changes: 20 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,27 @@ import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ['@mui/material', '@mui/icons-material'],
cssChunking: 'strict',
},
headers: async () => [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
],
},
{
source: '/_next/static/(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
],
}

export default nextConfig
4 changes: 2 additions & 2 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ body {
animation: header-fade-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.08s both;
}

/* Section entrance – replaces framer-motion FadeIn */
/* Section entrance */
@keyframes fade-in-up {
from {
opacity: 0;
Expand All @@ -116,7 +116,7 @@ body {
var(--fade-delay, 0s) both;
}

/* Stagger children – replaces framer-motion StaggerWrapper/StaggerItem */
/* Stagger children */
.stagger-item {
opacity: 0;
}
Expand Down
12 changes: 12 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<head>
<link rel="dns-prefetch" href="https://api.github.com" />
<link
rel="preconnect"
href="https://api.github.com"
crossOrigin="anonymous"
/>
<link rel="dns-prefetch" href="https://medium.com" />
<link
rel="preconnect"
href="https://medium.com"
crossOrigin="anonymous"
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON_LD }}
Expand Down
13 changes: 11 additions & 2 deletions src/components/layout/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
'use client'
import { lazy, Suspense } from 'react'
import Box from '@mui/material/Box'
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import Link from '@mui/material/Link'
import { Privacy, usePrivacyModal } from '@/components/sections/privacy'
import { usePrivacyModal } from '@/components/sections/use-privacy-modal'

const Privacy = lazy(() =>
import('@/components/sections/privacy').then((m) => ({ default: m.Privacy })),
)

export function Footer() {
const year = new Date().getFullYear()
Expand Down Expand Up @@ -103,7 +108,11 @@ export function Footer() {
</Stack>
</Box>

<Privacy open={open} onClose={closeModal} />
{open && (
<Suspense fallback={null}>
Comment thread
algotyrnt marked this conversation as resolved.
<Privacy open={open} onClose={closeModal} />
</Suspense>
)}
</>
)
}
13 changes: 2 additions & 11 deletions src/components/sections/privacy.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client'
import { useState } from 'react'
import Dialog from '@mui/material/Dialog'
import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle'
Expand Down Expand Up @@ -56,6 +54,8 @@ const dialogContentBoxSx = {
'& ul': {
pl: { xs: 2.5, sm: 3 },
'& li': {
fontFamily:
'var(--font-inter), "Inter", "Helvetica", "Arial", sans-serif',
fontSize: { xs: '0.875rem', sm: '0.9rem' },
lineHeight: { xs: 1.5, sm: 1.6 },
letterSpacing: '0.005em',
Expand Down Expand Up @@ -286,12 +286,3 @@ export function Privacy({ open, onClose }: PrivacyProps) {
</Dialog>
)
}

export function usePrivacyModal() {
const [open, setOpen] = useState(false)

const openModal = () => setOpen(true)
const closeModal = () => setOpen(false)

return { open, openModal, closeModal }
}
11 changes: 11 additions & 0 deletions src/components/sections/use-privacy-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'
import { useState, useCallback } from 'react'

export function usePrivacyModal() {
const [open, setOpen] = useState(false)

const openModal = useCallback(() => setOpen(true), [])
const closeModal = useCallback(() => setOpen(false), [])

return { open, openModal, closeModal }
}
19 changes: 3 additions & 16 deletions src/components/theme/registry.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
'use client'
import * as React from 'react'
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'
import NextAppDirEmotionCacheProvider from './emotion-cache'
import { getTheme } from './theme'
import { lightTheme, darkTheme } from './theme'

function MuiThemeWrapper({ children }: { children: React.ReactNode }) {
const { resolvedTheme } = useTheme()
Expand All @@ -14,21 +13,9 @@ function MuiThemeWrapper({ children }: { children: React.ReactNode }) {
setMounted(true)
}, [])

const currentTheme = React.useMemo(() => {
const mode = (resolvedTheme ?? 'light') as 'light' | 'dark'
return getTheme(mode)
}, [resolvedTheme])
const theme = mounted && resolvedTheme === 'dark' ? darkTheme : lightTheme

Comment thread
algotyrnt marked this conversation as resolved.
if (!mounted) {
return null
}

return (
<MuiThemeProvider theme={currentTheme}>
<CssBaseline />
{children}
</MuiThemeProvider>
)
return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
}

export default function ThemeRegistry({
Expand Down
5 changes: 4 additions & 1 deletion src/components/theme/theme.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createTheme, PaletteMode } from '@mui/material/styles'
import { tokens } from './tokens'

export const getTheme = (mode: PaletteMode) => {
const buildTheme = (mode: PaletteMode) => {
const isLight = mode === 'light'

return createTheme({
Expand Down Expand Up @@ -62,3 +62,6 @@ export const getTheme = (mode: PaletteMode) => {
},
})
}

export const lightTheme = buildTheme('light')
export const darkTheme = buildTheme('dark')
7 changes: 1 addition & 6 deletions src/components/ui/use-in-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'

/**
* Lightweight IntersectionObserver hook that fires once when the element
* enters the viewport. Replaces framer-motion's `whileInView`.
* enters the viewport.
*/
export function useInView(margin = '-40px') {
const ref = useRef<HTMLDivElement>(null)
Expand All @@ -13,11 +13,6 @@ export function useInView(margin = '-40px') {
const el = ref.current
if (!el) return

if (!('IntersectionObserver' in window)) {
queueMicrotask(() => setVisible(true))
return
}

const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
Expand Down
Loading