Projekt: Portfolio-Seite
Baue eine professionelle Portfolio-Webseite mit React. Von der Planung über Komponenten bis zum fertigen Projekt.
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
- Fuege einen Dark-Mode Toggle mit Context und localStorage-Persistenz hinzu
- Erstelle eine Projekt-Detailseite mit React Router fuer jedes Projekt
- Fuege Animationen hinzu – z.B. Scroll-basierte Einblendungen
- 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!