backend integration, readme, contact form

This commit is contained in:
Sascha 2025-08-28 16:17:55 +02:00
parent 905517a690
commit 146a9494f5
22 changed files with 1074 additions and 428 deletions

135
README.md
View File

@ -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

782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

67
src/app/AppRouter.tsx Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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() {
@ -12,30 +13,35 @@ export default function Footer() {
<footer className="footer">
<div className="footer__container">
<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">
<button
<button
className="footer__social-button"
onClick={() => window.open(personalConfig.social.github.url, '_blank')}
aria-label="GitHub Profile"
>
<Github className="footer__social-icon" />
</button>
<button
<button
className="footer__social-button"
onClick={() => window.open(personalConfig.social.linkedin.url, '_blank')}
aria-label="LinkedIn Profile"
>
<Linkedin className="footer__social-icon" />
</button>
<button
<button
className="footer__social-button"
onClick={handleEmailClick}
aria-label="Send Email"

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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" },

View File

@ -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>
@ -44,7 +63,7 @@ export default function HeroSection({
<span className="hero-section__stat-label">{item.label}</span>
</div>
))}
</div>
</div>
</div>
</section>
);

View File

@ -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) {

View File

@ -1,4 +1,3 @@
import React from 'react';
import type { Skill } from '../../data/Skill';
interface SkillCategory {
@ -96,7 +95,7 @@ export default function SkillsSection({
{subtitle}
</p>
</div>
<div className="skills-section__grid">
{skillCategories.map((category, categoryIndex) => (
<div key={categoryIndex} className="skills-section__category">
@ -109,7 +108,7 @@ export default function SkillsSection({
const displayLevel = skillItem.level || calculateSkillLevel(skillItem.value);
// Use provided color or get color based on position in array
const colorClass = skillItem.color || getColorByPosition(skillIndex);
return (
<div key={skillIndex} className="skills-section__skill">
<div className="skills-section__skill-header">
@ -126,7 +125,7 @@ export default function SkillsSection({
</div>
</div>
<div className="skills-section__progress">
<div
<div
className={`skills-section__progress-bar skills-section__progress-bar--${colorClass}`}
style={{ width: `${skillItem.value}%` }}
></div>

21
src/pages/HomePage.tsx Normal file
View File

@ -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 />
</>
);
}

113
src/pages/ImprintPage.tsx Normal file
View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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 {
@ -68,4 +101,4 @@
width: 1rem;
height: 1rem;
}
}
}

View File

@ -11,8 +11,10 @@
&__name {
font-weight: bold;
font-size: 1.25rem;
color: var(--color-text);
text-decoration: none;
}
&__container {
&__button {
@extend %button-reset;
@ -27,7 +29,7 @@
}
}
}
&__burger-button {
@include globals.flex-center();
justify-content: space-between;

View File

@ -5,4 +5,5 @@
@forward 'skills-section';
@forward 'certifications-section';
@forward 'projects-section';
@forward 'contact-section';
@forward 'contact-section';
@forward 'imprint-page';

View File

@ -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;
}
}
}

28
src/utils/scrollUtils.ts Normal file
View File

@ -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);
}

2
src/vite-env.d.ts vendored
View File

@ -1,2 +1,4 @@
/// <reference types="vite/client" />
declare module '*.PNG';
declare module '*.png';
declare module '*.scss';

View File

@ -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: ['..'],
},
},
});