backend integration, readme, contact form
This commit is contained in:
parent
905517a690
commit
146a9494f5
135
README.md
135
README.md
|
|
@ -14,22 +14,43 @@ This portfolio website serves as a comprehensive showcase of professional experi
|
|||
|
||||
## Features
|
||||
|
||||
- **Hero Section**: Eye-catching introduction with personal branding
|
||||
- **Hero Section**: Eye-catching introduction with personal branding and smooth scroll to projects
|
||||
- **About Section**: Professional background and personal story
|
||||
- **Skills Section**: Technical expertise with visual skill indicators
|
||||
- **Projects Section**: Portfolio of completed projects with descriptions and links
|
||||
- **Certifications Section**: Professional certifications and achievements
|
||||
- **Services Section**: Available professional services
|
||||
- **Contact Section**: Easy ways to get in touch
|
||||
- **Contact Section**: Functional contact form with backend email integration
|
||||
- **Multi-page Routing**: React Router with smooth hash navigation between sections
|
||||
- **Legal Compliance**: German Impressum page for legal requirements
|
||||
- **Theme Support**: Built-in light/dark theme toggle with persistence
|
||||
- **Responsive Navigation**: Mobile-friendly navigation with hamburger menu
|
||||
- **Backend Integration**: Node.js/Express server with Gmail SMTP for contact form
|
||||
- **Enhanced UX**: Detailed error handling, loading states, and user feedback
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**: React 18 with TypeScript
|
||||
- **Build Tool**: Vite for fast development and building
|
||||
- **Styling**: SCSS with modular architecture
|
||||
- **Code Quality**: ESLint for consistent code standards
|
||||
- **AI Assistant**: Built with the assistance of GitHub Copilot for enhanced development productivity
|
||||
### Frontend
|
||||
|
||||
- **React 18** with TypeScript for type-safe component development
|
||||
- **Vite** for fast development and optimized production builds
|
||||
- **React Router Dom** for multi-page navigation and hash-based scrolling
|
||||
- **SCSS** with modular architecture and theme system
|
||||
- **Lucide React** for consistent iconography
|
||||
|
||||
### Backend
|
||||
|
||||
- **Node.js** with Express.js for RESTful API
|
||||
- **Nodemailer** with Gmail SMTP integration for contact form
|
||||
- **Joi** for request validation and data sanitization
|
||||
- **Express Rate Limit** for API protection and spam prevention
|
||||
- **CORS** for secure cross-origin requests
|
||||
|
||||
### Development & Quality
|
||||
|
||||
- **TypeScript** for enhanced developer experience and code reliability
|
||||
- **ESLint** for consistent code standards and best practices
|
||||
- **Vite HMR** for instant development feedback
|
||||
|
||||
## Planned Additions
|
||||
|
||||
|
|
@ -46,6 +67,27 @@ A secure, password-protected administrative interface is planned for future rele
|
|||
|
||||
This feature will enable dynamic content updates without requiring code changes, making the portfolio easily maintainable and always up-to-date.
|
||||
|
||||
### Language Localization (i18n)
|
||||
|
||||
Multi-language support to make the portfolio accessible to an international audience:
|
||||
|
||||
- **Primary Languages**: English and German localization
|
||||
- **Extensible Framework**: Architecture designed to support additional languages easily
|
||||
- **Dynamic Language Switching**: Real-time language toggle in the navigation
|
||||
- **Localized Content**: All text content, including:
|
||||
- Navigation labels and menu items
|
||||
- Section titles and descriptions
|
||||
- Button text and form labels
|
||||
- Error messages and status notifications
|
||||
- Project descriptions and technical details
|
||||
- Skills and certifications descriptions
|
||||
- **Browser Language Detection**: Automatic language selection based on user's browser settings
|
||||
- **Persistent Language Preference**: User's language choice saved and remembered across sessions
|
||||
- **SEO Optimization**: Proper language meta tags and hreflang attributes for better search engine visibility
|
||||
- **Professional Translation**: Native-quality translations for both English and German content
|
||||
|
||||
The localization system will be implemented using React Context and TypeScript for type-safe translations, ensuring consistent and maintainable multi-language support.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
|
@ -74,7 +116,38 @@ npm install
|
|||
npm run dev
|
||||
```
|
||||
|
||||
4. Open your browser and navigate to `http://localhost:5173`
|
||||
4. Set up the backend (for contact form functionality):
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
5. Create a `.env` file in the backend directory with your Gmail credentials:
|
||||
|
||||
```env
|
||||
EMAIL_SERVICE=gmail
|
||||
EMAIL_USER=your-email@gmail.com
|
||||
EMAIL_PASS=your-app-password
|
||||
RECIPIENT_EMAIL=your-email@gmail.com
|
||||
RECIPIENT_NAME=Your Name
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
6. Start the backend server:
|
||||
|
||||
```bash
|
||||
node server.js
|
||||
```
|
||||
|
||||
7. Open your browser and navigate to `http://localhost:5173`
|
||||
|
||||
### Backend Setup Notes
|
||||
|
||||
- The contact form requires a Gmail account with an app-specific password
|
||||
- Generate an app password in your Google Account settings (2-factor authentication required)
|
||||
- The backend server runs on port 3002 by default
|
||||
- CORS is configured to allow requests from the frontend development server
|
||||
|
||||
### Building for Production
|
||||
|
||||
|
|
@ -87,12 +160,46 @@ The built files will be available in the `dist` directory.
|
|||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Main application component
|
||||
├── components/ # Reusable UI components
|
||||
├── config/ # Configuration files
|
||||
├── data/ # Data type definitions
|
||||
└── scss/ # Styling with SCSS modules
|
||||
portfolio-page/
|
||||
├── backend/ # Node.js Express backend for contact form
|
||||
│ ├── middleware/ # Validation and security middleware
|
||||
│ ├── routes/ # API route handlers (contact form)
|
||||
│ ├── services/ # Email service with Gmail integration
|
||||
│ ├── server.js # Express server configuration
|
||||
│ └── .env # Environment variables (Gmail credentials)
|
||||
├── src/
|
||||
│ ├── app/ # Main application components
|
||||
│ │ ├── page.tsx # HomePage component with all sections
|
||||
│ │ └── AppRouter.tsx # React Router setup with hash navigation
|
||||
│ ├── assets/ # Static assets (images, icons)
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ │ ├── layout/ # Navigation, Footer, MobileMenu
|
||||
│ │ ├── sections/ # Hero, About, Skills, Projects, etc.
|
||||
│ │ └── ThemeToggle.tsx # Dark/light theme switcher
|
||||
│ ├── config/ # Configuration files
|
||||
│ │ ├── api.ts # API endpoints configuration
|
||||
│ │ └── personal.ts # Personal information and social links
|
||||
│ ├── data/ # TypeScript type definitions
|
||||
│ │ ├── Project.ts # Project data structure
|
||||
│ │ ├── Skill.ts # Skills and proficiency levels
|
||||
│ │ ├── Service.ts # Professional services offered
|
||||
│ │ └── Certification.ts # Professional certifications
|
||||
│ ├── pages/ # Page components for routing
|
||||
│ │ └── ImprintPage.tsx # German legal compliance page
|
||||
│ ├── scss/ # Modular SCSS styling
|
||||
│ │ ├── themes/ # Light/dark theme variables
|
||||
│ │ ├── layout/ # Navigation, footer styling
|
||||
│ │ ├── sections/ # Individual section styles
|
||||
│ │ ├── globals.scss # Global styles and resets
|
||||
│ │ ├── variables.scss # SCSS variables and breakpoints
|
||||
│ │ └── mixins.scss # Reusable SCSS mixins
|
||||
│ ├── utils/ # Utility functions
|
||||
│ │ └── scrollUtils.ts # Smooth scrolling navigation helpers
|
||||
│ └── main.tsx # Application entry point
|
||||
├── dist/ # Production build output
|
||||
├── vite.config.ts # Vite configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── package.json # Dependencies and scripts
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -10,10 +10,12 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.539.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.32.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import '../scss/App.scss';
|
||||
import Navbar from '../components/layout/Navigation';
|
||||
import Footer from '../components/layout/Footer';
|
||||
import HomePage from '../pages/HomePage';
|
||||
import ImprintPage from '../pages/ImprintPage';
|
||||
|
||||
function AppRouter() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
// Check for saved theme preference or default to light mode
|
||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Apply theme to document and save preference
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
// Handle hash-based navigation for sections
|
||||
const handleHashChange = () => {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(hash);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// Check hash on initial load
|
||||
handleHashChange();
|
||||
|
||||
// Listen for hash changes
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="app">
|
||||
<Navbar theme={theme} setTheme={setTheme} />
|
||||
<main className="app__main">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/imprint" element={<ImprintPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppRouter;
|
||||
|
|
@ -1,35 +1,7 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import '../scss/App.scss';
|
||||
import Navbar from '../components/layout/Navigation';
|
||||
import HeroSection from '../components/sections/HeroSection';
|
||||
import AboutSection from '../components/sections/AboutSection';
|
||||
import ServicesSection from '../components/sections/ServicesSection';
|
||||
import SkillsSection from '../components/sections/SkillsSection';
|
||||
import CertificationsSection from '../components/sections/CertificationsSection';
|
||||
import ProjectsSection from '../components/sections/ProjectsSection';
|
||||
import ContactSection from '../components/sections/ContactSection';
|
||||
import Footer from '../components/layout/Footer';
|
||||
import AppRouter from './AppRouter';
|
||||
|
||||
function PortfolioApp() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar theme={theme} setTheme={setTheme} />
|
||||
<HeroSection />
|
||||
<AboutSection />
|
||||
<ServicesSection />
|
||||
<SkillsSection />
|
||||
<CertificationsSection />
|
||||
<ProjectsSection />
|
||||
<ContactSection />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
return <AppRouter />;
|
||||
}
|
||||
|
||||
export default PortfolioApp;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Github, Linkedin, Mail } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { personalConfig, createEmailLink } from '../../config/personal';
|
||||
|
||||
export default function Footer() {
|
||||
|
|
@ -14,8 +15,13 @@ export default function Footer() {
|
|||
<div className="footer__separator"></div>
|
||||
|
||||
<div className="footer__content">
|
||||
<div className="footer__copyright">
|
||||
© {new Date().getFullYear()} {personalConfig.name}. All rights reserved.
|
||||
<div className="footer__bottom-row">
|
||||
<Link to="/imprint" className="footer__link">
|
||||
Impressum
|
||||
</Link>
|
||||
<div className="footer__copyright">
|
||||
© {new Date().getFullYear()} {personalConfig.name}. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer__social">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
type Props = {
|
||||
menuItems: string[];
|
||||
isOpen: boolean;
|
||||
onItemClick: (section: string) => void;
|
||||
};
|
||||
|
||||
export default function MobileMenu({ menuItems, isOpen }: Props) {
|
||||
export default function MobileMenu({ menuItems, isOpen, onItemClick }: Props) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="navbar__mobile-menu">
|
||||
{menuItems.map((item) => (
|
||||
<button key={item}>{item}</button>
|
||||
<button
|
||||
key={item}
|
||||
onClick={() => onItemClick(item)}
|
||||
className="navbar__mobile-menu__item"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import ThemeToggle from '../ThemeToggle';
|
||||
import MobileMenu from './MobileMenu';
|
||||
import { personalConfig } from '../../config/personal';
|
||||
import { scrollToSection } from '../../utils/scrollUtils';
|
||||
|
||||
type Props = {
|
||||
theme: 'light' | 'dark';
|
||||
|
|
@ -10,6 +12,9 @@ type Props = {
|
|||
|
||||
export default function Navbar({ theme, setTheme }: Props) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const menuItems = [
|
||||
'About',
|
||||
'Services',
|
||||
|
|
@ -19,21 +24,22 @@ export default function Navbar({ theme, setTheme }: Props) {
|
|||
'Contact',
|
||||
];
|
||||
|
||||
function scrollToSection(section: string): void {
|
||||
const element = document.getElementById(section);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
function handleNavigation(section: string): void {
|
||||
const sectionId = section.toLowerCase();
|
||||
scrollToSection(sectionId, location.pathname, navigate);
|
||||
|
||||
// Close mobile menu if open
|
||||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="navbar">
|
||||
<div className="navbar__name">{personalConfig.name}</div>
|
||||
<Link to="/" className="navbar__name">{personalConfig.name}</Link>
|
||||
<div className="navbar__container">
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
onClick={() => scrollToSection(item.toLowerCase())}
|
||||
onClick={() => handleNavigation(item)}
|
||||
className="navbar__container__button"
|
||||
>
|
||||
{item}
|
||||
|
|
@ -49,7 +55,11 @@ export default function Navbar({ theme, setTheme }: Props) {
|
|||
{[0, 1, 2].map(i => <span key={i} className="navbar__burger-button__item"></span>)}
|
||||
</div>
|
||||
|
||||
<MobileMenu menuItems={menuItems} isOpen={menuOpen} />
|
||||
<MobileMenu
|
||||
menuItems={menuItems}
|
||||
isOpen={menuOpen}
|
||||
onItemClick={handleNavigation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export default function AboutSection({
|
|||
name = "Sascha Bach",
|
||||
title = "About Me",
|
||||
subtitle = "Web developer specializing in modern web technologies. Since 2020, I've been building efficient, scalable solutions – from complex Angular applications to creative React projects. My focus is on clean, maintainable code that solves real-world problems.",
|
||||
bio = "As a dedicated web developer, I specialize in creating modern, responsive web applications using both Angular and React frameworks with TypeScript. I'm passionate about writing clean, maintainable code and ensuring exceptional user experiences across all devices and browsers. I have extensive experience with Angular and am actively developing my React expertise, while maintaining proficiency in SCSS, testing with Cypress, and working with XSLT templates.",
|
||||
bio = "As a dedicated web developer, I specialize in creating modern, responsive web applications using both Angular and React frameworks with TypeScript. I'm passionate about writing clean, maintainable code and ensuring exceptional user experiences across all devices and browsers. I have extensive experience with Angular and am actively developing my React expertise. Also, I have a history in working with Unity3D.",
|
||||
profileImage = saschaImage,
|
||||
skillBadges = [
|
||||
{ text: "Angular & React Developer", colorClass: "primary" },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { Download } from 'lucide-react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { StatItem } from '../../data/StatItem';
|
||||
import { scrollToProjects } from '../../utils/scrollUtils';
|
||||
|
||||
interface HeroSectionProps {
|
||||
title?: string;
|
||||
|
|
@ -21,6 +23,17 @@ export default function HeroSection({
|
|||
{ label: "Frameworks", value: "3+" },
|
||||
]
|
||||
}: HeroSectionProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleViewWork = () => {
|
||||
scrollToProjects(location.pathname, navigate);
|
||||
};
|
||||
|
||||
const handleDownloadResume = () => {
|
||||
// Add your resume download logic here
|
||||
console.log('Download resume clicked');
|
||||
};
|
||||
return (
|
||||
<section className="hero-section">
|
||||
<div className="hero-section__container">
|
||||
|
|
@ -29,10 +42,16 @@ export default function HeroSection({
|
|||
{description}
|
||||
</p>
|
||||
<div className="hero-section__buttons">
|
||||
<button className="hero-section__button hero-section__button--primary">
|
||||
<button
|
||||
className="hero-section__button hero-section__button--primary"
|
||||
onClick={handleViewWork}
|
||||
>
|
||||
{primaryButtonText}
|
||||
</button>
|
||||
<button className="hero-section__button hero-section__button--secondary">
|
||||
<button
|
||||
className="hero-section__button hero-section__button--secondary"
|
||||
onClick={handleDownloadResume}
|
||||
>
|
||||
<Download className="hero-section__button-icon" />
|
||||
{secondaryButtonText}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,30 @@ export default function ProjectsSection({
|
|||
image: ergoVRImage,
|
||||
technologies: ["Unity3D", "C#", "Oculus SDK"],
|
||||
github: personalConfig.projects.ergoVR
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Icarace",
|
||||
description: "Participated in the development of a web-platform for a fitness racing game for Icaros GmbH until 2018.",
|
||||
image: ergoVRImage,
|
||||
technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
|
||||
github: personalConfig.projects.ergoVR
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "ErgoVR",
|
||||
description: "Gravity is a fitness game for the Icaros Device developed by Icaros GmbH. I participated in developing behavior scripts in C#.",
|
||||
image: ergoVRImage,
|
||||
technologies: ["Unity3D", "C#", "Oculus SDK"],
|
||||
github: personalConfig.projects.ergoVR
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "ErgoVR",
|
||||
description: "A virtual reality application for analysis of motion sickness in VR environments.",
|
||||
image: ergoVRImage,
|
||||
technologies: ["Unity3D", "C#", "Oculus SDK"],
|
||||
github: personalConfig.projects.ergoVR
|
||||
}
|
||||
]
|
||||
}: ProjectsSectionProps) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import type { Skill } from '../../data/Skill';
|
||||
|
||||
interface SkillCategory {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import HeroSection from '../components/sections/HeroSection';
|
||||
import AboutSection from '../components/sections/AboutSection';
|
||||
import ServicesSection from '../components/sections/ServicesSection';
|
||||
import SkillsSection from '../components/sections/SkillsSection';
|
||||
import CertificationsSection from '../components/sections/CertificationsSection';
|
||||
import ProjectsSection from '../components/sections/ProjectsSection';
|
||||
import ContactSection from '../components/sections/ContactSection';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<HeroSection />
|
||||
<AboutSection />
|
||||
<ServicesSection />
|
||||
<SkillsSection />
|
||||
<CertificationsSection />
|
||||
<ProjectsSection />
|
||||
<ContactSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { personalConfig } from '../config/personal';
|
||||
|
||||
export default function ImprintPage() {
|
||||
return (
|
||||
<section className="imprint-page">
|
||||
<div className="imprint-page__container">
|
||||
<div className="imprint-page__header">
|
||||
<h1 className="imprint-page__title">Impressum</h1>
|
||||
<p className="imprint-page__subtitle">Legal Information</p>
|
||||
</div>
|
||||
|
||||
<div className="imprint-page__content">
|
||||
<div className="imprint-page__section">
|
||||
<h2 className="imprint-page__section-title">Angaben gemäß § 5 TMG</h2>
|
||||
<div className="imprint-page__info">
|
||||
<p><strong>Name:</strong> {personalConfig.name}</p>
|
||||
<p><strong>Adresse:</strong></p>
|
||||
<p>Musterstraße 123</p>
|
||||
<p>12345 Musterstadt</p>
|
||||
<p>Deutschland</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="imprint-page__section">
|
||||
<h2 className="imprint-page__section-title">Kontakt</h2>
|
||||
<div className="imprint-page__info">
|
||||
<p><strong>E-Mail:</strong> {personalConfig.email.full}</p>
|
||||
<p><strong>Telefon:</strong> +49 (0) 123 456789</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="imprint-page__section">
|
||||
<h2 className="imprint-page__section-title">Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
|
||||
<div className="imprint-page__info">
|
||||
<p>{personalConfig.name}</p>
|
||||
<p>Musterstraße 123</p>
|
||||
<p>12345 Musterstadt</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="imprint-page__section">
|
||||
<h2 className="imprint-page__section-title">Haftungsausschluss</h2>
|
||||
|
||||
<div className="imprint-page__subsection">
|
||||
<h3 className="imprint-page__subsection-title">Haftung für Inhalte</h3>
|
||||
<p className="imprint-page__text">
|
||||
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den
|
||||
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht
|
||||
unter der Verpflichtung, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach
|
||||
Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
|
||||
</p>
|
||||
<p className="imprint-page__text">
|
||||
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen
|
||||
Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt
|
||||
der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden
|
||||
Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="imprint-page__subsection">
|
||||
<h3 className="imprint-page__subsection-title">Haftung für Links</h3>
|
||||
<p className="imprint-page__text">
|
||||
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben.
|
||||
Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der
|
||||
verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
|
||||
</p>
|
||||
<p className="imprint-page__text">
|
||||
Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft.
|
||||
Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche
|
||||
Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht
|
||||
zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="imprint-page__subsection">
|
||||
<h3 className="imprint-page__subsection-title">Urheberrecht</h3>
|
||||
<p className="imprint-page__text">
|
||||
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem
|
||||
deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung
|
||||
außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors
|
||||
bzw. Erstellers.
|
||||
</p>
|
||||
<p className="imprint-page__text">
|
||||
Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.
|
||||
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte
|
||||
Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem
|
||||
auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis.
|
||||
Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="imprint-page__section">
|
||||
<h2 className="imprint-page__section-title">Datenschutz</h2>
|
||||
<div className="imprint-page__info">
|
||||
<p className="imprint-page__text">
|
||||
Die Nutzung unserer Webseite ist in der Regel ohne Angabe personenbezogener Daten möglich.
|
||||
Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder
|
||||
E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis.
|
||||
Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben.
|
||||
</p>
|
||||
<p className="imprint-page__text">
|
||||
Wir weisen darauf hin, dass die Datenübertragung im Internet (z.B. bei der Kommunikation per
|
||||
E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch
|
||||
Dritte ist nicht möglich.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -43,3 +43,14 @@ section {
|
|||
padding: 1rem 1rem;
|
||||
margin: 0; // Remove any section margins
|
||||
}
|
||||
|
||||
// App-level styles for routing
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,46 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&__bottom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__copyright {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__social {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
&__name {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__container {
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@
|
|||
@forward 'certifications-section';
|
||||
@forward 'projects-section';
|
||||
@forward 'contact-section';
|
||||
@forward 'imprint-page';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
@use '../globals';
|
||||
|
||||
.imprint-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
padding: 6rem 0 4rem; // Top padding to account for fixed navigation
|
||||
|
||||
&__container {
|
||||
max-width: 50rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
|
||||
@include globals.desktop-only {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include globals.desktop-only {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
&__section {
|
||||
background: var(--bg-secondary);
|
||||
padding: 2rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__subsection {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__subsection-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__info {
|
||||
line-height: 1.6;
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 1rem;
|
||||
text-align: justify;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@include globals.mobile-only {
|
||||
padding: 5rem 0 3rem;
|
||||
|
||||
&__title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
&__section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
&__subsection-title {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
export function scrollToSection(
|
||||
sectionId: string,
|
||||
currentPath: string,
|
||||
navigate?: NavigateFunction
|
||||
): void {
|
||||
// If we're on the homepage, scroll directly to the section
|
||||
if (currentPath === '/') {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
} else if (navigate) {
|
||||
// If we're on another page, navigate to homepage with hash
|
||||
navigate(`/#${sectionId}`);
|
||||
} else {
|
||||
// Fallback: direct navigation without router
|
||||
window.location.href = `/#${sectionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToProjects(
|
||||
currentPath: string,
|
||||
navigate?: NavigateFunction
|
||||
): void {
|
||||
scrollToSection('projects', currentPath, navigate);
|
||||
}
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
/// <reference types="vite/client" />
|
||||
declare module '*.PNG';
|
||||
declare module '*.png';
|
||||
declare module '*.scss';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
base: '/',
|
||||
optimizeDeps: {
|
||||
include: ['lucide-react'],
|
||||
entries: ['src/**/*.tsx', 'src/**/*.ts'],
|
||||
force: false,
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue