Initial set-up + tech stack for new version of my portfolio website built with Onlook, Vite, React, GSAP, and TinaCMS.
- Getting Started
- Project Structure
- Working with Components
- Design Tokens
- Fluid Responsive System
- Page Routing
- Content Management with TinaCMS
- Development Workflow
- Deployment
- Troubleshooting
- Node.js 18+ and npm
- Git for version control
- A code editor (VS Code or Cursor recommended)
- Onlook Studio for visual editing
# 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
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
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
# 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 TypeScriptComponent.module.css
- Scoped CSS stylesComponent.types.ts
- TypeScript interfacesindex.ts
- Barrel export
-
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
- Open Onlook and navigate to
-
Export to Code:
- Click "Export Component" in Onlook
- Choose target directory (e.g.,
src/components/ui/
) - Onlook generates the base React component
-
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
// 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);
}
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 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
/* 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 */
/* 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
/* 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
.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);
}
// 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)'
}
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>
)
}
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.
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
- 🎯 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
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);
}
- 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>
)
}
- 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()
}
}
- 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>
)
}
// 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>
)
}
-
Configure TinaCMS (already in project):
- Configuration is in
tina/config.ts
- Collections define content types
- Media settings handle uploads
- Configuration is in
-
Start CMS:
# Development mode (included in npm run dev)
npm run dev
# Or run CMS separately
npm run tina
- Access Admin Panel:
- Navigate to
http://localhost:3000/admin
- Login with Tina Cloud credentials
- Start editing content
- Navigate to
// 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
]
}
]
}
// 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' }
}
}
}
// 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']
}
]
}
// 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} />
}
// 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>
)
}
-
Morning Setup:
git pull origin main npm install # If package.json changed npm run dev
-
Component Development Flow:
- Design visually in Onlook
- Export to code
- Enhance with TypeScript in Cursor
- Add animations with GSAP
- Test responsiveness
- Create stories/documentation
-
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
- Define content schema in
- ✅ 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
- ✅ 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
- ✅ 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
# 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
# Run production build
npm run build
# Test production build locally
npm run preview
# .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
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Set environment variables in Vercel dashboard
# 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/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
-
Lighthouse CI:
npm install -D @lhci/cli # Add to package.json "scripts": { "lighthouse": "lhci autorun" }
-
Bundle Analysis:
npm run build -- --analyze
-
Runtime Performance:
- Monitor Core Web Vitals
- Track GSAP animation performance
- Check memory usage
- ✅ 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)
- ✅ 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
- ✅ 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
- ✅ 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, preferunknown
- ✅ Check for unused imports
- ✅ Verify all environment variables
- ✅ Ensure no syntax errors in JSON files
- ✅ Check for circular dependencies
- ✅ Clear node_modules and reinstall
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
- Check the project documentation
- Search existing GitHub issues
- Ask in the project Discord/Slack
- Review component examples in Storybook
- Component Library - Live component showcase
- Design System Docs - Token usage guide
- GSAP Documentation - Animation reference
- TinaCMS Docs - CMS guides
- React Router Docs - Routing reference
- Fork the repository
- Create feature branch (
git checkout -b feature/amazing-feature
) - Follow code style guide
- Write/update tests
- Update documentation
- Submit pull request
- 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
[Your License Type] - see LICENSE file for details