Skip to content

MichelKerkmeester/kerkmeester.com

Repository files navigation

Portfolio N°02

Initial set-up + tech stack for new version of my portfolio website built with Onlook, Vite, React, GSAP, and TinaCMS.

Table of Contents

Getting Started

Prerequisites

  • Node.js 18+ and npm
  • Git for version control
  • A code editor (VS Code or Cursor recommended)
  • Onlook Studio for visual editing

Installation

# Clone the repository
git clone [your-repo-url]
cd [project-name]

# Install dependencies
npm install

# Copy environment variables
cp .env.example .env.local

# Add your TinaCMS credentials to .env.local
# TINA_CLIENT_ID=your-client-id
# TINA_TOKEN=your-token

# Start development server
npm run dev

Development Commands

npm run dev        # Start dev server with TinaCMS
npm run build      # Production build
npm run preview    # Preview production build
npm run component  # Create new component
npm run type-check # Run TypeScript compiler
npm run lint       # Run ESLint
npm run format     # Format code with Prettier

Project Structure

src/
├── components/     # Reusable components
│   ├── ui/        # Basic UI components (buttons, inputs, etc.)
│   ├── layout/    # Layout components (header, footer, etc.)
│   └── sections/  # Page sections (hero, features, etc.)
├── pages/         # Route pages
├── styles/        # Global styles and design tokens
│   └── tokens/    # Design system tokens
├── hooks/         # Custom React hooks
├── lib/           # Utilities and configurations
├── scripts/       # Animation scripts
├── types/         # TypeScript type definitions
└── tina/          # CMS configuration

Working with Components

Creating Components with Cursor + Onlook

Method 1: Using the Component Generator (Recommended)

# Create a new UI component
npm run component Button ui

# Create a layout component  
npm run component Header layout

# Create a section component
npm run component HeroSection section

This automatically generates:

  • Component.tsx - Main component file with TypeScript
  • Component.module.css - Scoped CSS styles
  • Component.types.ts - TypeScript interfaces
  • index.ts - Barrel export

Method 2: Visual Creation with Onlook

  1. Design in Onlook:

    • Open Onlook and navigate to http://localhost:5173
    • Use the visual editor to create your component
    • Apply design tokens from the tokens panel
    • Test responsive breakpoints
  2. Export to Code:

    • Click "Export Component" in Onlook
    • Choose target directory (e.g., src/components/ui/)
    • Onlook generates the base React component
  3. Enhance in Cursor:

    • Open the exported component in Cursor
    • Add TypeScript types and interfaces
    • Implement GSAP animations
    • Add interactive behavior
    • Connect to TinaCMS if needed

Component Structure Example

// components/ui/Card/Card.tsx
import { forwardRef } from 'react'
import { clsx } from 'clsx'
import { useGSAP } from '@/hooks/useGSAP'
import { CardProps } from './Card.types'
import styles from './Card.module.css'

export const Card = forwardRef<HTMLDivElement, CardProps>(
  ({ className, children, animated = true, ...props }, ref) => {
    
    useGSAP((context) => {
      if (!animated) return
      
      context.add('.card', {
        hover: {
          scale: 1.02,
          boxShadow: 'var(--shadow-lg)',
          duration: 0.3
        }
      })
    }, [animated])
    
    return (
      <div
        ref={ref}
        className={clsx(styles.card, 'card', className)}
        {...props}
      >
        {children}
      </div>
    )
  }
)

Card.displayName = 'Card'
/* Card.module.css */
.card {
  background: var(--surface-raised);
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-base);
  transition: all var(--duration-base) var(--ease-out);
}

.card:hover {
  transform: translateY(-2px);
}

Using Components

import { Button, Card, Input } from '@/components/ui'

function MyPage() {
  return (
    <Card>
      <h2>Welcome</h2>
      <Input 
        label="Email" 
        type="email" 
        placeholder="Enter your email"
      />
      <Button 
        variant="primary" 
        size="lg"
        onClick={() => console.log('Clicked!')}
      >
        Get Started
      </Button>
    </Card>
  )
}

Design Tokens

Understanding the Token System

Design tokens are the visual design atoms of the design system. They're organized in categories:

styles/tokens/
├── colors.css      # All color values
├── typography.css  # Font families, sizes, weights
├── spacing.css     # Spacing scale (4px base)
├── effects.css     # Shadows, borders, radius
└── motion.css      # Animation durations, easings

Token Categories

Colors

/* Neutral Palette */
--neutral-50 through --neutral-950

/* Brand Colors */
--brand-50 through --brand-900
--brand-base (primary brand color)

/* Semantic Colors */
--color-success
--color-warning  
--color-error
--color-info

/* Content Colors */
--content-primary    /* Main text */
--content-secondary  /* Secondary text */
--content-tertiary   /* Disabled/hint text */
--content-inverse    /* Text on dark backgrounds */

/* Surface Colors */
--surface-base      /* Default background */
--surface-raised    /* Card backgrounds */
--surface-overlay   /* Modal overlays */

Typography

/* Font Families */
--font-sans      /* System fonts */
--font-serif     /* Serif fonts */
--font-mono      /* Code fonts */
--font-display   /* Headings */

/* Fluid Font Sizes */
--text-xs through --text-5xl

/* Font Weights */
--font-thin (100) through --font-black (900)

/* Line Heights */
--leading-none through --leading-loose

/* Letter Spacing */
--tracking-tighter through --tracking-widest

Spacing

/* 4px base unit scale */
--space-0 (0)
--space-px (1px)
--space-0_5 (2px)
--space-1 (4px)
--space-2 (8px)
--space-3 (12px)
--space-4 (16px)
--space-5 (20px)
--space-6 (24px)
--space-8 (32px)
--space-10 (40px)
--space-12 (48px)
--space-16 (64px)
--space-20 (80px)
--space-24 (96px)
--space-32 (128px)

/* Layout helpers */
--layout-gutter
--layout-max-width
--section-padding

Using Tokens

In CSS:

.component {
  /* Colors */
  color: var(--content-primary);
  background: var(--surface-raised);
  
  /* Spacing */
  padding: var(--space-4) var(--space-6);
  margin-bottom: var(--space-8);
  
  /* Typography */
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  font-weight: var(--font-medium);
  line-height: var(--leading-relaxed);
  
  /* Effects */
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-base);
  
  /* Animation */
  transition: all var(--duration-base) var(--ease-out);
}

In JavaScript/TypeScript:

// Using the tokens utility
import { tokens } from '@/lib/tokens'

const styles = {
  padding: tokens.space(4),
  color: tokens.color('brand-base'),
  fontSize: tokens.text('lg')
}

// Or use CSS variables directly
const cardStyle = {
  backgroundColor: 'var(--surface-raised)',
  borderRadius: 'var(--radius-lg)'
}

Dark Mode

Toggle dark mode by setting data-theme="dark" on the html element:

// components/ThemeToggle.tsx
function ThemeToggle() {
  const [theme, setTheme] = useState('light')
  
  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light'
    document.documentElement.setAttribute('data-theme', newTheme)
    localStorage.setItem('theme', newTheme)
    setTheme(newTheme)
  }
  
  return (
    <button onClick={toggleTheme}>
      {theme === 'light' ? '🌙' : '☀️'}
    </button>
  )
}

Fluid Responsive System

This project includes a global fluid responsive system that automatically scales the root font size across different viewport ranges. This creates a truly fluid responsive experience where all rem-based values scale proportionally.

How It Works

The system defines 5 viewport ranges with smooth transitions:

  • Mobile (1-991px): 14px → 16px
  • Tablet-Desktop (991-1440px): 16px → 18px
  • Desktop (1440-2560px): Fixed 18px
  • Wide Screen (2560-5120px): 18px → 20px
  • Ultra Wide (5120-7680px): 20px → 24px
  • Beyond 7680px: Fixed 24px

Benefits

  • 🎯 Single Source of Truth: Change root font-size, everything scales
  • 🌊 Smooth Transitions: No jumps between breakpoints
  • ♿ Accessibility: Respects user font preferences
  • ⚡ Performance: Pure CSS, no JavaScript needed
  • 🔧 Maintainable: Easy to adjust breakpoints and scaling

Usage

All design tokens use rem values that automatically scale:

.hero-title {
  font-size: var(--text-5xl); /* Scales from ~42px to ~72px */
  margin-bottom: var(--space-8); /* Scales proportionally */
}

.button {
  padding: var(--space-3) var(--space-6); /* All scale together */
  font-size: var(--text-base);
}

Page Routing

Creating a New Page

  1. Create the Page Component:
// pages/About/About.tsx
import { PageWrapper } from '@/components/layout'
import { usePageMeta } from '@/hooks/usePageMeta'
import { initAboutAnimations } from './About.scripts'
import styles from './About.module.css'

export default function About() {
  // Set page metadata
  usePageMeta({
    title: 'About Us - Your Company',
    description: 'Learn more about our mission and team'
  })
  
  return (
    <PageWrapper pageScripts={initAboutAnimations}>
      <section className={styles.hero}>
        <h1 className="animate-title">About Us</h1>
        <p className="animate-subtitle">Our story begins here...</p>
      </section>
    </PageWrapper>
  )
}
  1. Add Page-Specific Animations (optional):
// pages/About/About.scripts.ts
import { gsap } from '@/lib/gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

export function initAboutAnimations() {
  // Create animation context
  const ctx = gsap.context(() => {
    // Hero animations
    const tl = gsap.timeline()
    
    tl.from('.animate-title', {
      y: 100,
      opacity: 0,
      duration: 1,
      ease: 'power3.out'
    })
    .from('.animate-subtitle', {
      y: 50,
      opacity: 0,
      duration: 0.8
    }, '-=0.5')
    
    // Scroll animations
    gsap.from('.content-section', {
      scrollTrigger: {
        trigger: '.content-section',
        start: 'top 80%',
        toggleActions: 'play none none reverse'
      },
      y: 60,
      opacity: 0,
      stagger: 0.2
    })
  })
  
  // Return cleanup function
  return () => {
    ctx.revert()
  }
}
  1. Add Route to App:
// App.tsx
import { Routes, Route } from 'react-router-dom'
import { lazy, Suspense } from 'react'

// Lazy load pages for better performance
const Home = lazy(() => import('@/pages/Home'))
const About = lazy(() => import('@/pages/About'))
const Projects = lazy(() => import('@/pages/Projects'))
const Contact = lazy(() => import('@/pages/Contact'))

function App() {
  return (
    <Suspense fallback={<LoadingScreen />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/projects" element={<Projects />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </Suspense>
  )
}

Navigation

// components/layout/Navigation/Navigation.tsx
import { NavLink } from 'react-router-dom'
import styles from './Navigation.module.css'

const navItems = [
  { path: '/', label: 'Home' },
  { path: '/about', label: 'About' },
  { path: '/projects', label: 'Projects' },
  { path: '/contact', label: 'Contact' }
]

export function Navigation() {
  return (
    <nav className={styles.nav}>
      {navItems.map(({ path, label }) => (
        <NavLink
          key={path}
          to={path}
          className={({ isActive }) =>
            clsx(styles.link, isActive && styles.active)
          }
        >
          {label}
        </NavLink>
      ))}
    </nav>
  )
}

Content Management with TinaCMS

Initial Setup

  1. Configure TinaCMS (already in project):

    • Configuration is in tina/config.ts
    • Collections define content types
    • Media settings handle uploads
  2. Start CMS:

# Development mode (included in npm run dev)
npm run dev

# Or run CMS separately
npm run tina
  1. Access Admin Panel:
    • Navigate to http://localhost:3000/admin
    • Login with Tina Cloud credentials
    • Start editing content

Creating Content Types

Basic Page Collection:

// tina/collections/page.ts
export const pageCollection = {
  name: 'page',
  label: 'Pages',
  path: 'content/pages',
  format: 'mdx',
  fields: [
    {
      type: 'string',
      name: 'title',
      label: 'Page Title',
      isTitle: true,
      required: true
    },
    {
      type: 'string',
      name: 'description',
      label: 'SEO Description',
      ui: {
        component: 'textarea'
      }
    },
    {
      type: 'object',
      name: 'hero',
      label: 'Hero Section',
      fields: [
        {
          type: 'string',
          name: 'headline',
          label: 'Headline'
        },
        {
          type: 'string',
          name: 'subheadline',
          label: 'Subheadline'
        },
        {
          type: 'image',
          name: 'image',
          label: 'Hero Image'
        }
      ]
    },
    {
      type: 'object',
      list: true,
      name: 'blocks',
      label: 'Page Sections',
      templates: [
        // Define block templates here
      ]
    }
  ]
}

Making Pages Editable

// pages/Home/Home.tsx
import { useTina } from 'tinacms/dist/react'
import { client } from '@/lib/tina'

// Define the query
const QUERY = `
  query PageQuery($relativePath: String!) {
    page(relativePath: $relativePath) {
      title
      description
      hero {
        headline
        subheadline
        image
      }
      blocks {
        __typename
        ... on PageBlocksHero {
          headline
          subheadline
          ctaText
          ctaLink
        }
        ... on PageBlocksFeatures {
          title
          features {
            title
            description
            icon
          }
        }
      }
    }
  }
`

export default function HomePage({ data, query, variables }) {
  // Make content live-editable
  const { data: liveData } = useTina({
    query,
    variables,
    data
  })
  
  return (
    <PageWrapper>
      {/* Hero Section */}
      <section 
        className="hero"
        style={{ backgroundImage: `url(${liveData.page.hero.image})` }}
      >
        <h1>{liveData.page.hero.headline}</h1>
        <p>{liveData.page.hero.subheadline}</p>
      </section>
      
      {/* Dynamic Blocks */}
      <BlockRenderer blocks={liveData.page.blocks} />
    </PageWrapper>
  )
}

// Fetch data (for SSG/SSR)
export async function getStaticProps() {
  const { data } = await client.queries.page({
    relativePath: 'home.mdx'
  })
  
  return {
    props: {
      data,
      query: QUERY,
      variables: { relativePath: 'home.mdx' }
    }
  }
}

Creating Custom Blocks

// tina/blocks/hero.ts
export const heroBlock = {
  name: 'hero',
  label: 'Hero Section',
  ui: {
    defaultItem: {
      headline: 'Welcome to our site',
      subheadline: 'Start building something amazing'
    }
  },
  fields: [
    {
      type: 'string',
      name: 'headline',
      label: 'Headline',
      required: true
    },
    {
      type: 'string',
      name: 'subheadline',
      label: 'Subheadline'
    },
    {
      type: 'string',
      name: 'ctaText',
      label: 'Button Text'
    },
    {
      type: 'string',
      name: 'ctaLink',
      label: 'Button Link'
    },
    {
      type: 'string',
      name: 'variant',
      label: 'Style Variant',
      options: ['default', 'dark', 'gradient']
    }
  ]
}

Rich Text with Custom Components

// components/TinaMarkdown.tsx
import { TinaMarkdown as TinaMD } from 'tinacms/dist/rich-text'
import { Button } from '@/components/ui'

const components = {
  // Custom button in markdown
  Button: (props) => (
    <Button 
      variant={props.variant || 'primary'}
      href={props.url}
    >
      {props.text}
    </Button>
  ),
  
  // Custom callout box
  Callout: (props) => (
    <div className={`callout callout-${props.type}`}>
      {props.children}
    </div>
  ),
  
  // Code block with syntax highlighting
  code_block: (props) => (
    <pre className={`language-${props.lang}`}>
      <code>{props.value}</code>
    </pre>
  )
}

export function TinaMarkdown({ content }) {
  return <TinaMD content={content} components={components} />
}

Media Handling

// components/TinaImage.tsx
import { useState } from 'react'

export function TinaImage({ src, alt, className }) {
  const [isLoading, setIsLoading] = useState(true)
  
  return (
    <div className={clsx('image-wrapper', className)}>
      {isLoading && <div className="image-skeleton" />}
      <img
        src={src}
        alt={alt}
        onLoad={() => setIsLoading(false)}
        loading="lazy"
        style={{ display: isLoading ? 'none' : 'block' }}
      />
    </div>
  )
}

Development Workflow

Recommended Daily Workflow

  1. Morning Setup:

    git pull origin main
    npm install  # If package.json changed
    npm run dev
  2. Component Development Flow:

    • Design visually in Onlook
    • Export to code
    • Enhance with TypeScript in Cursor
    • Add animations with GSAP
    • Test responsiveness
    • Create stories/documentation
  3. Content Management Flow:

    • Define content schema in tina/
    • Run dev server to generate types
    • Create content in admin panel
    • Implement in components
    • Test edit experience

Best Practices

Component Development

  • ✅ Use semantic HTML elements
  • ✅ Apply design tokens consistently
  • ✅ Keep components under 150 lines
  • ✅ Separate logic into custom hooks
  • ✅ Use TypeScript for all components
  • ✅ Write CSS Modules for scoped styles
  • ✅ Add proper ARIA labels
  • ✅ Test keyboard navigation

Performance

  • ✅ Lazy load routes and heavy components
  • ✅ Optimize images (WebP with fallbacks)
  • ✅ Use CSS containment for better performance
  • ✅ Debounce scroll and resize handlers
  • ✅ Profile and optimize GSAP animations
  • ✅ Monitor bundle size with build analysis

Code Quality

  • ✅ Run type checking before commits
  • ✅ Use ESLint and Prettier
  • ✅ Write meaningful commit messages
  • ✅ Keep files focused on single responsibility
  • ✅ Document complex logic with comments
  • ✅ Use conventional naming patterns

Git Workflow

# Feature development
git checkout -b feature/component-name
# Make changes
git add .
git commit -m "feat: add Card component with hover animations"
git push origin feature/component-name
# Create pull request

# Conventional commits
feat: new feature
fix: bug fix
docs: documentation
style: formatting
refactor: code restructuring
test: adding tests
chore: maintenance

Deployment

Build for Production

# Run production build
npm run build

# Test production build locally
npm run preview

Environment Variables

# .env.production
TINA_CLIENT_ID=your-production-client-id
TINA_TOKEN=your-production-token
VITE_API_URL=https://api.yourdomain.com
VITE_SITE_URL=https://yourdomain.com

Deployment Options

Vercel (Recommended)

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel

# Set environment variables in Vercel dashboard

Netlify

# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "18"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

GitHub Pages

# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      
      - run: npm ci
      - run: npm run build
      
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

Performance Monitoring

  1. Lighthouse CI:

    npm install -D @lhci/cli
    
    # Add to package.json
    "scripts": {
      "lighthouse": "lhci autorun"
    }
  2. Bundle Analysis:

    npm run build -- --analyze
  3. Runtime Performance:

    • Monitor Core Web Vitals
    • Track GSAP animation performance
    • Check memory usage

Troubleshooting

Common Issues & Solutions

GSAP Animations Not Working

  • ✅ Ensure GSAP plugins are registered in lib/gsap.ts
  • ✅ Check element selectors exist when animation runs
  • ✅ Verify cleanup functions in useEffect/useGSAP
  • ✅ Use gsap.context() for proper cleanup
  • ✅ Check for CSS conflicts (transforms, transitions)

CSS Modules Not Applying

  • ✅ File must end with .module.css
  • ✅ Import as styles object
  • ✅ Use styles.className not string literals
  • ✅ Check for typos in class names
  • ✅ Clear build cache and restart dev server

TinaCMS Issues

  • ✅ Ensure environment variables are set
  • ✅ Check Git branch matches Tina config
  • ✅ Verify content file paths are correct
  • ✅ Run npm run tina to regenerate types
  • ✅ Check for schema validation errors

TypeScript Errors

  • ✅ Run npm run type-check to see all errors
  • ✅ Ensure all imports have proper types
  • ✅ Check tsconfig.json path aliases
  • ✅ Install missing @types/ packages
  • ✅ Use any sparingly, prefer unknown

Build Failures

  • ✅ Check for unused imports
  • ✅ Verify all environment variables
  • ✅ Ensure no syntax errors in JSON files
  • ✅ Check for circular dependencies
  • ✅ Clear node_modules and reinstall

Debug Mode

Add these to your .env.local for debugging:

# Enable GSAP dev tools
VITE_GSAP_DEV=true

# Enable React dev tools profiler
VITE_REACT_PROFILER=true

# Verbose logging
VITE_LOG_LEVEL=debug

Getting Help

  1. Check the project documentation
  2. Search existing GitHub issues
  3. Ask in the project Discord/Slack
  4. Review component examples in Storybook

Additional Resources

Contributing

  1. Fork the repository
  2. Create feature branch (git checkout -b feature/amazing-feature)
  3. Follow code style guide
  4. Write/update tests
  5. Update documentation
  6. Submit pull request

Code Style Guide

  • Use functional components with hooks
  • Prefer named exports for components
  • Use design tokens for all styling
  • Keep components pure when possible
  • Write descriptive variable names
  • Add JSDoc comments for complex functions

License

[Your License Type] - see LICENSE file for details

About

Portfolio N°02

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published