Zum Inhalt springen
React Anfänger 60 min

Projekt: Portfolio-Seite

Baue eine professionelle Portfolio-Webseite mit React. Von der Planung über Komponenten bis zum fertigen Projekt.

Aktualisiert:

Projekt: Portfolio-Seite

Zeit, dein Wissen in die Praxis umzusetzen! In diesem Projekt baust du eine professionelle Portfolio-Webseite mit React – komplett mit Navigation, Projektgalerie, Kontaktformular und responsivem Design.

Was wir bauen

Unsere Portfolio-Seite hat folgende Features:

  • Hero-Bereich mit Name und Kurzvorstellung
  • Ueber-Mich-Sektion mit Skills
  • Projekt-Galerie mit Filter
  • Kontaktformular mit Validierung
  • Responsive Design fuer alle Geraete
  • Smooth Scrolling zwischen Sektionen

Projekt aufsetzen

npm create vite@latest portfolio -- --template react
cd portfolio
npm install react-router-dom
npm run dev

Projektstruktur

src/
├── components/
│   ├── Navbar.jsx
│   ├── Hero.jsx
│   ├── About.jsx
│   ├── ProjectCard.jsx
│   ├── Projects.jsx
│   ├── Contact.jsx
│   └── Footer.jsx
├── data/
│   └── projects.js
├── styles/
│   └── App.css
├── App.jsx
└── main.jsx

Schritt 1: Projektdaten definieren

// src/data/projects.js
export const projects = [
  {
    id: 1,
    title: 'Wetter-App',
    description: 'Eine Wetter-Anwendung mit OpenWeatherMap API. Zeigt aktuelle Wetterdaten und 5-Tage-Vorhersage.',
    tags: ['React', 'API', 'CSS'],
    image: 'https://picsum.photos/seed/weather/600/400',
    github: 'https://github.com/user/weather-app',
    live: 'https://weather-app.example.com',
    category: 'frontend'
  },
  {
    id: 2,
    title: 'Todo-App',
    description: 'Feature-reiche Todo-Anwendung mit LocalStorage-Persistenz, Kategorien und Dark Mode.',
    tags: ['React', 'Hooks', 'LocalStorage'],
    image: 'https://picsum.photos/seed/todo/600/400',
    github: 'https://github.com/user/todo-app',
    live: 'https://todo-app.example.com',
    category: 'frontend'
  },
  {
    id: 3,
    title: 'Blog-API',
    description: 'REST API fuer einen Blog mit Authentifizierung, CRUD-Operationen und Pagination.',
    tags: ['Node.js', 'Express', 'MongoDB'],
    image: 'https://picsum.photos/seed/blog/600/400',
    github: 'https://github.com/user/blog-api',
    category: 'backend'
  },
  {
    id: 4,
    title: 'E-Commerce Dashboard',
    description: 'Admin-Dashboard mit Statistiken, Bestellverwaltung und Echtzeit-Updates.',
    tags: ['React', 'Chart.js', 'Tailwind'],
    image: 'https://picsum.photos/seed/dashboard/600/400',
    github: 'https://github.com/user/dashboard',
    live: 'https://dashboard.example.com',
    category: 'fullstack'
  }
];

export const skills = [
  { name: 'JavaScript', level: 90 },
  { name: 'React', level: 85 },
  { name: 'CSS / Tailwind', level: 80 },
  { name: 'Node.js', level: 70 },
  { name: 'TypeScript', level: 65 },
  { name: 'Git', level: 85 }
];

Schritt 2: Globale Styles

/* src/styles/App.css */
:root {
  --color-primary: #2563eb;
  --color-primary-dark: #1d4ed8;
  --color-bg: #ffffff;
  --color-bg-secondary: #f8fafc;
  --color-text: #1e293b;
  --color-text-secondary: #64748b;
  --color-border: #e2e8f0;
  --max-width: 1100px;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  color: var(--color-text);
  line-height: 1.6;
}

html {
  scroll-behavior: smooth;
}

section {
  padding: 5rem 1.5rem;
}

.container {
  max-width: var(--max-width);
  margin: 0 auto;
}

.section-title {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

.section-subtitle {
  color: var(--color-text-secondary);
  margin-bottom: 2.5rem;
}

Schritt 3: Navbar-Komponente

// src/components/Navbar.jsx
import { useState, useEffect } from 'react';

function Navbar() {
  const [scrolled, setScrolled] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);

  useEffect(() => {
    function handleScroll() {
      setScrolled(window.scrollY > 50);
    }
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  const links = [
    { href: '#about', label: 'Ueber mich' },
    { href: '#projects', label: 'Projekte' },
    { href: '#contact', label: 'Kontakt' }
  ];

  return (
    <nav style={{
      position: 'fixed',
      top: 0,
      left: 0,
      right: 0,
      zIndex: 1000,
      padding: scrolled ? '0.75rem 2rem' : '1.25rem 2rem',
      backgroundColor: scrolled ? 'rgba(255,255,255,0.95)' : 'transparent',
      backdropFilter: scrolled ? 'blur(10px)' : 'none',
      boxShadow: scrolled ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
      transition: 'all 0.3s ease',
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'center'
    }}>
      <a href="#" style={{
        fontSize: '1.25rem',
        fontWeight: 700,
        textDecoration: 'none',
        color: 'var(--color-primary)'
      }}>
        {'<Max />'}
      </a>

      <div style={{
        display: 'flex',
        gap: '2rem',
        alignItems: 'center'
      }}>
        {links.map(link => (
          <a
            key={link.href}
            href={link.href}
            style={{
              textDecoration: 'none',
              color: 'var(--color-text)',
              fontWeight: 500,
              transition: 'color 0.2s'
            }}
          >
            {link.label}
          </a>
        ))}
      </div>
    </nav>
  );
}

export default Navbar;

Schritt 4: Hero-Bereich

// src/components/Hero.jsx
function Hero() {
  return (
    <section style={{
      minHeight: '100vh',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      textAlign: 'center',
      background: 'linear-gradient(135deg, #f0f4ff 0%, #e0e7ff 100%)',
      padding: '2rem'
    }}>
      <div>
        <p style={{
          color: 'var(--color-primary)',
          fontWeight: 600,
          marginBottom: '0.5rem',
          fontSize: '1.1rem'
        }}>
          Hallo, ich bin
        </p>
        <h1 style={{
          fontSize: 'clamp(2.5rem, 6vw, 4rem)',
          fontWeight: 800,
          marginBottom: '1rem',
          lineHeight: 1.1
        }}>
          Max Mustermann
        </h1>
        <p style={{
          fontSize: '1.25rem',
          color: 'var(--color-text-secondary)',
          maxWidth: '600px',
          margin: '0 auto 2rem'
        }}>
          Frontend-Entwickler mit Leidenschaft fuer React, moderne Webtechnologien und benutzerfreundliche Interfaces.
        </p>
        <div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
          <a href="#projects" style={{
            padding: '12px 28px',
            backgroundColor: 'var(--color-primary)',
            color: 'white',
            textDecoration: 'none',
            borderRadius: '8px',
            fontWeight: 600,
            transition: 'background-color 0.2s'
          }}>
            Meine Projekte
          </a>
          <a href="#contact" style={{
            padding: '12px 28px',
            border: '2px solid var(--color-primary)',
            color: 'var(--color-primary)',
            textDecoration: 'none',
            borderRadius: '8px',
            fontWeight: 600
          }}>
            Kontakt
          </a>
        </div>
      </div>
    </section>
  );
}

export default Hero;

Schritt 5: About-Sektion mit Skills

// src/components/About.jsx
import { skills } from '../data/projects';

function SkillBar({ name, level }) {
  return (
    <div style={{ marginBottom: '1rem' }}>
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        marginBottom: '0.25rem'
      }}>
        <span style={{ fontWeight: 500 }}>{name}</span>
        <span style={{ color: 'var(--color-text-secondary)' }}>{level}%</span>
      </div>
      <div style={{
        height: '8px',
        backgroundColor: 'var(--color-border)',
        borderRadius: '4px',
        overflow: 'hidden'
      }}>
        <div style={{
          width: `${level}%`,
          height: '100%',
          backgroundColor: 'var(--color-primary)',
          borderRadius: '4px',
          transition: 'width 1s ease'
        }} />
      </div>
    </div>
  );
}

function About() {
  return (
    <section id="about" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
      <div className="container">
        <h2 className="section-title">Ueber mich</h2>
        <p className="section-subtitle">Das bringe ich mit</p>

        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
          gap: '3rem'
        }}>
          <div>
            <p style={{ lineHeight: 1.8, color: 'var(--color-text-secondary)' }}>
              Ich bin ein leidenschaftlicher Frontend-Entwickler mit Fokus auf React
              und moderne Webtechnologien. Nach meinem Informatik-Studium habe ich
              mehrere Jahre Erfahrung in der Webentwicklung gesammelt.
            </p>
            <p style={{ lineHeight: 1.8, color: 'var(--color-text-secondary)', marginTop: '1rem' }}>
              Ich liebe es, intuitive und performante Benutzeroberflaechen zu bauen.
              Sauberer Code und gute User Experience sind mir besonders wichtig.
            </p>
          </div>

          <div>
            <h3 style={{ marginBottom: '1.5rem' }}>Meine Skills</h3>
            {skills.map(skill => (
              <SkillBar key={skill.name} name={skill.name} level={skill.level} />
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

export default About;

Schritt 6: Projekt-Galerie mit Filter

// src/components/ProjectCard.jsx
function ProjectCard({ project }) {
  return (
    <div style={{
      borderRadius: '12px',
      overflow: 'hidden',
      border: '1px solid var(--color-border)',
      transition: 'transform 0.2s, box-shadow 0.2s',
      backgroundColor: 'white'
    }}
    onMouseEnter={(e) => {
      e.currentTarget.style.transform = 'translateY(-4px)';
      e.currentTarget.style.boxShadow = '0 10px 30px rgba(0,0,0,0.1)';
    }}
    onMouseLeave={(e) => {
      e.currentTarget.style.transform = 'translateY(0)';
      e.currentTarget.style.boxShadow = 'none';
    }}>
      <img
        src={project.image}
        alt={project.title}
        style={{ width: '100%', height: '200px', objectFit: 'cover' }}
      />
      <div style={{ padding: '1.25rem' }}>
        <h3 style={{ marginBottom: '0.5rem' }}>{project.title}</h3>
        <p style={{
          color: 'var(--color-text-secondary)',
          fontSize: '0.9rem',
          marginBottom: '1rem'
        }}>
          {project.description}
        </p>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginBottom: '1rem' }}>
          {project.tags.map(tag => (
            <span key={tag} style={{
              padding: '2px 10px',
              backgroundColor: '#eff6ff',
              color: 'var(--color-primary)',
              borderRadius: '12px',
              fontSize: '0.8rem',
              fontWeight: 500
            }}>
              {tag}
            </span>
          ))}
        </div>
        <div style={{ display: 'flex', gap: '0.75rem' }}>
          <a href={project.github} target="_blank" rel="noopener noreferrer" style={{
            padding: '6px 14px',
            border: '1px solid var(--color-border)',
            borderRadius: '6px',
            textDecoration: 'none',
            color: 'var(--color-text)',
            fontSize: '0.85rem'
          }}>
            GitHub
          </a>
          {project.live && (
            <a href={project.live} target="_blank" rel="noopener noreferrer" style={{
              padding: '6px 14px',
              backgroundColor: 'var(--color-primary)',
              color: 'white',
              borderRadius: '6px',
              textDecoration: 'none',
              fontSize: '0.85rem'
            }}>
              Live Demo
            </a>
          )}
        </div>
      </div>
    </div>
  );
}

export default ProjectCard;
// src/components/Projects.jsx
import { useState } from 'react';
import { projects } from '../data/projects';
import ProjectCard from './ProjectCard';

function Projects() {
  const [filter, setFilter] = useState('all');

  const categories = ['all', 'frontend', 'backend', 'fullstack'];
  const filteredProjects = filter === 'all'
    ? projects
    : projects.filter(p => p.category === filter);

  return (
    <section id="projects">
      <div className="container">
        <h2 className="section-title">Meine Projekte</h2>
        <p className="section-subtitle">Eine Auswahl meiner Arbeiten</p>

        <div style={{
          display: 'flex',
          gap: '0.75rem',
          marginBottom: '2rem',
          flexWrap: 'wrap'
        }}>
          {categories.map(cat => (
            <button
              key={cat}
              onClick={() => setFilter(cat)}
              style={{
                padding: '6px 16px',
                border: filter === cat ? 'none' : '1px solid var(--color-border)',
                borderRadius: '20px',
                backgroundColor: filter === cat ? 'var(--color-primary)' : 'transparent',
                color: filter === cat ? 'white' : 'var(--color-text)',
                cursor: 'pointer',
                fontWeight: 500,
                textTransform: 'capitalize'
              }}
            >
              {cat === 'all' ? 'Alle' : cat}
            </button>
          ))}
        </div>

        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
          gap: '1.5rem'
        }}>
          {filteredProjects.map(project => (
            <ProjectCard key={project.id} project={project} />
          ))}
        </div>
      </div>
    </section>
  );
}

export default Projects;

Schritt 7: Kontaktformular

// src/components/Contact.jsx
import { useState } from 'react';

function Contact() {
  const [formData, setFormData] = useState({
    name: '', email: '', message: ''
  });
  const [errors, setErrors] = useState({});
  const [submitted, setSubmitted] = useState(false);

  function handleChange(e) {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }));
  }

  function handleSubmit(e) {
    e.preventDefault();
    const newErrors = {};
    if (!formData.name.trim()) newErrors.name = 'Name ist erforderlich';
    if (!formData.email.includes('@')) newErrors.email = 'Gueltige E-Mail erforderlich';
    if (formData.message.length < 10) newErrors.message = 'Mindestens 10 Zeichen';

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    console.log('Nachricht gesendet:', formData);
    setSubmitted(true);
  }

  if (submitted) {
    return (
      <section id="contact" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
        <div className="container" style={{ textAlign: 'center' }}>
          <h2 style={{ color: '#16a34a' }}>Nachricht gesendet!</h2>
          <p>Vielen Dank, {formData.name}. Ich melde mich bald bei dir.</p>
        </div>
      </section>
    );
  }

  const inputStyle = {
    width: '100%', padding: '12px', borderRadius: '8px',
    border: '1px solid var(--color-border)', fontSize: '1rem'
  };

  return (
    <section id="contact" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
      <div className="container" style={{ maxWidth: '600px' }}>
        <h2 className="section-title">Kontakt</h2>
        <p className="section-subtitle">Schreib mir eine Nachricht</p>

        <form onSubmit={handleSubmit}>
          <div style={{ marginBottom: '1rem' }}>
            <input name="name" value={formData.name} onChange={handleChange}
              placeholder="Dein Name" style={{
                ...inputStyle,
                borderColor: errors.name ? '#ef4444' : 'var(--color-border)'
              }} />
            {errors.name && <small style={{ color: '#ef4444' }}>{errors.name}</small>}
          </div>
          <div style={{ marginBottom: '1rem' }}>
            <input name="email" type="email" value={formData.email} onChange={handleChange}
              placeholder="Deine E-Mail" style={{
                ...inputStyle,
                borderColor: errors.email ? '#ef4444' : 'var(--color-border)'
              }} />
            {errors.email && <small style={{ color: '#ef4444' }}>{errors.email}</small>}
          </div>
          <div style={{ marginBottom: '1rem' }}>
            <textarea name="message" value={formData.message} onChange={handleChange}
              placeholder="Deine Nachricht..." rows={5} style={{
                ...inputStyle,
                borderColor: errors.message ? '#ef4444' : 'var(--color-border)',
                resize: 'vertical'
              }} />
            {errors.message && <small style={{ color: '#ef4444' }}>{errors.message}</small>}
          </div>
          <button type="submit" style={{
            width: '100%', padding: '14px',
            backgroundColor: 'var(--color-primary)', color: 'white',
            border: 'none', borderRadius: '8px', fontSize: '1rem',
            fontWeight: 600, cursor: 'pointer'
          }}>
            Nachricht senden
          </button>
        </form>
      </div>
    </section>
  );
}

export default Contact;

Schritt 8: Alles zusammensetzen

// src/App.jsx
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import About from './components/About';
import Projects from './components/Projects';
import Contact from './components/Contact';
import './styles/App.css';

function App() {
  return (
    <div>
      <Navbar />
      <Hero />
      <About />
      <Projects />
      <Contact />
      <footer style={{
        padding: '2rem',
        textAlign: 'center',
        backgroundColor: '#1e293b',
        color: '#94a3b8'
      }}>
        <p>2026 Max Mustermann. Mit React gebaut.</p>
      </footer>
    </div>
  );
}

export default App;

Uebungen zur Erweiterung

  1. Fuege einen Dark-Mode Toggle mit Context und localStorage-Persistenz hinzu
  2. Erstelle eine Projekt-Detailseite mit React Router fuer jedes Projekt
  3. Fuege Animationen hinzu – z.B. Scroll-basierte Einblendungen
  4. Deploye die Seite auf Netlify oder Vercel

Was kommt als Naechstes?

Du hast dein erstes komplettes React-Projekt gebaut! Im naechsten Projekt erstellst du einen Online-Shop mit Warenkorb, Produktfilter und State Management.

Zusammenfassung

  • Eine Portfolio-Seite ist das perfekte Einstiegsprojekt fuer React
  • Komponenten aufteilen macht den Code uebersichtlich und wartbar
  • Daten auslagern (in data/-Dateien) trennt Inhalt von Darstellung
  • Formular-Validierung sorgt fuer eine gute User Experience
  • CSS Custom Properties erleichtern konsistentes Styling
  • Responsive Design ist Pflicht fuer moderne Webseiten

Pro-Tipp: Dein Portfolio ist deine Visitenkarte als Entwickler. Ersetze die Beispiel-Projekte durch deine eigenen Arbeiten, passe das Design an deinen Stil an und deploye es auf einer eigenen Domain. Recruiter schauen sich Portfolios an – mache es beeindruckend!

Zurück zum React Kurs