web accessiblity skill update

This commit is contained in:
Sascha 2025-09-25 15:55:04 +02:00
parent 9b985e07b6
commit ca488cfce9
13 changed files with 832 additions and 375 deletions

View File

@ -1,4 +1,5 @@
import { Code, Palette, ShieldCheck } from 'lucide-react';
import { Code, Palette, ShieldCheck, Eye } from 'lucide-react';
import { getTexts } from '../../config/texts';
import type { SkillBadge } from '../../data/SkillBadge';
import type { Service } from '../../data/Service';
import saschaImage from '../../assets/sascha.png';
@ -13,39 +14,49 @@ interface AboutSectionProps {
featureCards?: Service[];
}
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. Also, I have a history in working with Unity3D.",
profileImage = saschaImage,
skillBadges = [
{ text: "Angular & React Developer", colorClass: "primary" },
{ text: "Responsive Design Expert", colorClass: "secondary" },
{ text: "Cross-Browser Compatible", colorClass: "tertiary" },
{ text: "Continuous Learner", colorClass: "quaternary" }
],
featureCards = [
{
icon: Code,
title: "Web Development",
description: "Building modern web applications with Angular and React frameworks, leveraging TypeScript and JavaScript for robust, scalable solutions.",
colorClass: "primary"
},
{
icon: Palette,
title: "Responsive Design",
description: "Creating pixel-perfect, responsive interfaces that work seamlessly across all devices and screen sizes.",
colorClass: "secondary"
},
{
icon: ShieldCheck,
title: "Cross-Browser Compatibility",
description: "Ensuring consistent user experiences across all major browsers with thorough testing and optimization.",
colorClass: "tertiary"
}
]
}: AboutSectionProps) {
export default function AboutSection(props: AboutSectionProps = {}) {
const texts = getTexts().about;
// Use centralized texts as defaults, allow props to override
const {
name = texts.name,
title = texts.title,
subtitle = texts.subtitle,
bio = texts.bio,
profileImage = saschaImage,
skillBadges = [
{ text: "Angular & React Developer", colorClass: "primary" },
{ text: "Responsive Design Expert", colorClass: "secondary" },
{ text: "Cross-Browser Compatible", colorClass: "tertiary" },
{ text: "Continuous Learner", colorClass: "quaternary" }
],
featureCards = [
{
icon: Code,
title: "Web Development",
description: "Building modern web applications with Angular and React frameworks, leveraging TypeScript and JavaScript for robust, scalable solutions.",
colorClass: "primary"
},
{
icon: Palette,
title: "Responsive Design",
description: "Creating pixel-perfect, responsive interfaces that work seamlessly across all devices and screen sizes.",
colorClass: "secondary"
},
{
icon: ShieldCheck,
title: "Cross-Browser Compatibility",
description: "Ensuring consistent user experiences across all major browsers with thorough testing and optimization.",
colorClass: "tertiary"
},
{
icon: Eye,
title: "Web Accessibility",
description: "Implementing accessibility best practices to deliver websites that support screen readers, keyboard navigation, and assistive technologies.",
colorClass: "quaternary"
}
]
} = props;
return (
<section id="about" className="about-section">
<div className="about-section__container">

View File

@ -1,4 +1,5 @@
import { Award } from 'lucide-react';
import { getTexts } from '../../config/texts';
import type { Certification } from '../../data/Certification';
interface CertificationsSectionProps {
@ -7,42 +8,46 @@ interface CertificationsSectionProps {
certifications?: Certification[];
}
export default function CertificationsSection({
title = "Certifications & Achievements",
subtitle = "My professional qualifications and continuous learning",
certifications = [
{
name: "Practical Prompt Engineering Masterclass: Hands-On Learning",
issuer: "Udemy Lecture - Asif Farooqi, Abdullah Dar",
year: "2025",
icon: ""
},
{
name: "Understanding Typescript",
issuer: "Udemy Lecture - Maximilian Schwarzmüller",
year: "2025",
icon: ""
},
{
name: "Angular Step by Step",
issuer: "Udemy Lecture - Shivprasad Koirala",
year: "2020",
icon: ""
},
{
name: "Angular and Typescript",
issuer: "LinkedIn TestDome",
year: "2020",
icon: ""
},
{
name: "Bachelor of Science in Computer Science",
issuer: "TU Darmstadt",
year: "2015",
icon: ""
}
]
}: CertificationsSectionProps) {
export default function CertificationsSection(props: CertificationsSectionProps = {}) {
const texts = getTexts().certifications;
// Use centralized texts as defaults, allow props to override
const {
title = texts.title,
subtitle = texts.subtitle,
certifications = [
{
name: "Practical Prompt Engineering Masterclass: Hands-On Learning",
issuer: "Udemy Lecture - Asif Farooqi, Abdullah Dar",
year: "2025",
icon: ""
},
{
name: "Understanding Typescript",
issuer: "Udemy Lecture - Maximilian Schwarzmüller",
year: "2025",
icon: ""
},
{
name: "Angular Step by Step",
issuer: "Udemy Lecture - Shivprasad Koirala",
year: "2020",
icon: ""
},
{
name: "Angular and Typescript",
issuer: "LinkedIn TestDome",
year: "2020",
icon: ""
},
{
name: "Bachelor of Science in Computer Science",
issuer: "TU Darmstadt",
year: "2015",
icon: ""
}
]
} = props;
// Sort certifications by year in descending order (newest first)
const sortedCertifications = [...certifications].sort((a, b) => {
return parseInt(b.year) - parseInt(a.year);

View File

@ -46,6 +46,12 @@ export default function ContactSection(props: ContactSectionProps = {}) {
window.location.href = `mailto:info@sascha-bach.de?subject=${subject}&body=${body}`;
};
// Replace the hardcoded email with obfuscated construction
const getEmailAddress = () => {
const parts = ['freelancer', 'sascha-bach', 'de'];
return parts[0] + '@' + parts[1] + '.' + parts[2];
};
return (
<section id="contact" className="contact-section">
<div className="contact-section__container">
@ -75,9 +81,10 @@ export default function ContactSection(props: ContactSectionProps = {}) {
<button
onClick={() => {
const email = getEmailAddress();
const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss your project");
const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
window.location.href = `mailto:freelancer@sascha-bach.de?subject=${subject}&body=${body}`;
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
}}
className="contact-section__email-button"
type="button"

View File

@ -2,6 +2,7 @@ import { Download } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import type { StatItem } from '../../data/StatItem';
import { scrollToProjects } from '../../utils/scrollUtils';
import { getTexts } from '../../config/texts';
interface HeroSectionProps {
title?: string;
@ -11,18 +12,17 @@ interface HeroSectionProps {
statItems?: StatItem[];
}
export default function HeroSection({
title = "Web-Developer",
description = "Creating responsive, cross-browser compatible web applications with Angular, React, and modern web technologies",
primaryButtonText = "View my work",
secondaryButtonText = "Download Resume",
statItems = [
{ label: "Years of Experience", value: "10+" },
{ label: "Responsive Design", value: "100%" },
{ label: "Technologies Used", value: "10+" },
{ label: "Frameworks", value: "3+" },
]
}: HeroSectionProps) {
export default function HeroSection(props: HeroSectionProps = {}) {
const texts = getTexts().hero;
// Use centralized texts as defaults, allow props to override
const {
title = texts.title,
description = texts.description,
primaryButtonText = texts.primaryButtonText,
secondaryButtonText = texts.secondaryButtonText,
statItems = texts.statItems,
} = props;
const location = useLocation();
const navigate = useNavigate();

View File

@ -13,52 +13,51 @@ interface ProjectsSectionProps {
projects?: Project[];
}
export default function ProjectsSection({
title,
subtitle,
projects = [
{
id: 1,
title: "Portfolio Website",
description: "A personal portfolio website to showcase my skills, projects, and experience.",
image: portfolioImage,
technologies: ["React", "TypeScript", "SCSS", "Vercel", "Github Copilot"],
github: personalConfig.projects.portfolio,
live: ""
},
{
id: 2,
title: "ErgoVR",
description: "A virtual reality application for analysis of motion sickness in VR environments from 2015.",
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: icaraceImage,
technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
github: personalConfig.projects.ergoVR
}
]
}: ProjectsSectionProps) {
const texts = getTexts();
export default function ProjectsSection(props: ProjectsSectionProps = {}) {
const texts = getTexts().projects;
// Use texts from centralized config with fallbacks
const sectionTitle = title || texts.projects.title;
const sectionSubtitle = subtitle || texts.projects.subtitle;
// Use centralized texts as defaults, allow props to override
const {
title = texts.title,
subtitle = texts.subtitle,
projects = [
{
id: 1,
title: "Portfolio Website",
description: "A personal portfolio website to showcase my skills, projects, and experience.",
image: portfolioImage,
technologies: ["React", "TypeScript", "SCSS", "Vercel", "Github Copilot"],
github: personalConfig.projects.portfolio,
live: ""
},
{
id: 2,
title: "ErgoVR",
description: "A virtual reality application for analysis of motion sickness in VR environments from 2015.",
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: icaraceImage,
technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
github: personalConfig.projects.ergoVR
}
]
} = props;
return (
<section id="projects" className="projects-section">
<div className="projects-section__container">
{/* Section Header */}
<div className="projects-section__header">
<h2 className="projects-section__title">
{sectionTitle}
{title}
</h2>
<p className="projects-section__subtitle">
{sectionSubtitle}
{subtitle}
</p>
</div>
@ -82,7 +81,7 @@ export default function ProjectsSection({
className="projects-section__action-button projects-section__action-button--secondary"
>
<Github className="projects-section__action-icon" />
{texts.projects.codeButtonText}
{texts.codeButtonText}
</a>
)}
{project.live && (
@ -93,7 +92,7 @@ export default function ProjectsSection({
className="projects-section__action-button projects-section__action-button--primary"
>
<ExternalLink className="projects-section__action-icon" />
{texts.projects.liveButtonText}
{texts.liveButtonText}
</a>
)}
</div>

View File

@ -1,4 +1,5 @@
import { Code, Laptop, Lightbulb, Palette, Rocket, ShieldCheck } from "lucide-react";
import { Eye, Lightbulb, Palette, Laptop, Rocket, ShieldCheck } from 'lucide-react';
import { getTexts } from '../../config/texts';
import type { Service } from '../../data/Service';
interface ServicesSectionProps {
@ -7,48 +8,52 @@ interface ServicesSectionProps {
services?: Service[];
}
export default function ServicesSection({
title = "Services I Offer",
subtitle = "Solutions tailored to your needs",
services = [
{
icon: Code,
title: "Modern Framework Development",
description: "Building scalable, maintainable web applications using Angular and React frameworks with TypeScript and modern development practices.",
colorClass: "primary"
},
{
icon: Palette,
title: "Responsive Web Design",
description: "Creating mobile-first, responsive interfaces that provide optimal viewing experiences across all devices and screen sizes.",
colorClass: "secondary"
},
{
icon: ShieldCheck,
title: "Cross-Browser Testing",
description: "Ensuring consistent functionality and appearance across all major browsers including Chrome, Firefox, Safari, and Edge.",
colorClass: "tertiary"
},
{
icon: Laptop,
title: "Frontend Architecture",
description: "Designing and implementing scalable frontend architectures with component-based development and modern tooling.",
colorClass: "quaternary"
},
{
icon: Lightbulb,
title: "Performance Optimization",
description: "Optimizing web applications for speed and efficiency through code splitting, lazy loading, and performance best practices.",
colorClass: "quinary"
},
{
icon: Rocket,
title: "Testing & Quality Assurance",
description: "Implementing comprehensive testing strategies using Cypress for end-to-end testing and ensuring code quality.",
colorClass: "senary"
}
]
}: ServicesSectionProps) {
export default function ServicesSection(props: ServicesSectionProps = {}) {
const texts = getTexts().services;
// Use centralized texts as defaults, allow props to override
const {
title = texts.title,
subtitle = texts.subtitle,
services = [
{
icon: Eye,
title: "Web Accessiblity",
description: "Building inclusive and accessible web applications by implementing Web Content Accessibility Guidelines, semantic HTML, and support for assistive technologies.",
colorClass: "primary"
},
{
icon: Palette,
title: "Responsive Web Design",
description: "Creating mobile-first, responsive interfaces that provide optimal viewing experiences across all devices and screen sizes.",
colorClass: "secondary"
},
{
icon: ShieldCheck,
title: "Cross-Browser Testing",
description: "Ensuring consistent functionality and appearance across all major browsers including Chrome, Firefox, Safari, and Edge.",
colorClass: "tertiary"
},
{
icon: Laptop,
title: "Frontend Architecture",
description: "Designing and implementing scalable frontend architectures with component-based development and modern tooling.",
colorClass: "quaternary"
},
{
icon: Lightbulb,
title: "Performance Optimization",
description: "Optimizing web applications for speed and efficiency through code splitting, lazy loading, and performance best practices.",
colorClass: "quinary"
},
{
icon: Rocket,
title: "Testing & Quality Assurance",
description: "Implementing comprehensive testing strategies using Cypress for end-to-end testing and ensuring code quality.",
colorClass: "senary"
}
]
} = props;
return (
<section id="services" className="services-section">
<div className="services-section__container">

View File

@ -1,3 +1,4 @@
import { getTexts } from '../../config/texts';
import type { Skill } from '../../data/Skill';
interface SkillCategory {
@ -25,65 +26,69 @@ const getColorByPosition = (index: number): string => {
return colors[index % colors.length];
};
export default function SkillsSection({
title = "Skills & Technologies",
subtitle = "Technologies I work with to build amazing web experiences",
skillCategories = [
{
title: "Web Frameworks",
skills: [
{ skill: "Angular", value: 90 },
{ skill: "React", value: 70 },
{ skill: "Next.js", value: 70 },
{ skill: "TypeScript", value: 95 }
]
},
{
title: "Styling & Design",
skills: [
{ skill: "SCSS/Sass", value: 95 },
{ skill: "CSS3", value: 90 },
{ skill: "Accessibility (a11y) Standards", value: 45 },
{ skill: "Bootstrap", value: 65 }
]
},
{
title: "Backend Development",
skills: [
{ skill: "Node.js", value: 65 },
{ skill: "Express.js", value: 25 },
{ skill: "REST APIs", value: 70 },
{ skill: "MongoDB", value: 50 }
]
},
{
title: "Development Tools",
skills: [
{ skill: "Git & GitHub", value: 90 },
{ skill: "Visual Studio Code", value: 75 },
{ skill: "Vite", value: 80 },
{ skill: "NPM", value: 85 }
]
},
{
title: "Testing & Quality",
skills: [
{ skill: "Cypress", value: 85 },
{ skill: "Jasmine/Karma", value: 75 },
{ skill: "ESLint", value: 90 },
{ skill: "Prettier", value: 95 }
]
},
{
title: "AI-Tools",
skills: [
{ skill: "GitHub Copilot", value: 85 },
{ skill: "Prompt Engineering", value: 45 },
{ skill: "Prototyping / MVP-Building with AI", value: 45 }
]
}
]
}: SkillsSectionProps) {
export default function SkillsSection(props: SkillsSectionProps = {}) {
const texts = getTexts().skills;
// Use centralized texts as defaults, allow props to override
const {
title = texts.title,
subtitle = texts.subtitle,
skillCategories = [
{
title: "Web Frameworks",
skills: [
{ skill: "Angular", value: 90 },
{ skill: "React", value: 70 },
{ skill: "Next.js", value: 70 },
{ skill: "TypeScript", value: 95 }
]
},
{
title: "Styling & Design",
skills: [
{ skill: "SCSS/Sass", value: 95 },
{ skill: "CSS3", value: 90 },
{ skill: "Accessibility (a11y) Standards", value: 45 },
{ skill: "Bootstrap", value: 65 }
]
},
{
title: "Backend Development",
skills: [
{ skill: "Node.js", value: 65 },
{ skill: "Express.js", value: 25 },
{ skill: "REST APIs", value: 70 },
{ skill: "MongoDB", value: 50 }
]
},
{
title: "Development Tools",
skills: [
{ skill: "Git & GitHub", value: 90 },
{ skill: "Visual Studio Code", value: 75 },
{ skill: "Vite", value: 80 },
{ skill: "NPM", value: 85 }
]
},
{
title: "Testing & Quality",
skills: [
{ skill: "Cypress", value: 85 },
{ skill: "Jasmine/Karma", value: 75 },
{ skill: "ESLint", value: 90 },
{ skill: "Prettier", value: 95 }
]
},
{
title: "AI-Tools",
skills: [
{ skill: "GitHub Copilot", value: 85 },
{ skill: "Prompt Engineering", value: 45 },
{ skill: "Prototyping / MVP-Building with AI", value: 45 }
]
}
]
} = props;
return (
<section id="skills" className="skills-section">
<div className="skills-section__container">

View File

@ -1,47 +1,43 @@
export const personalConfig = {
// Basic Information
name: "Sascha Bach",
title: "Freelance Software Developer",
name: 'Sascha Bach',
title: 'Freelance Software Developer',
// Contact Information
email: {
user: "freelancer",
domain: "sascha-bach.de",
full: "freelancer@sascha-bach.de"
user: 'freelancer',
domain: 'sascha-bach.de',
full: 'freelancer [at] sascha-bach.de',
},
// Social Links
social: {
github: {
url: "https://github.com/LuciusShadow",
username: "LuciusShadow"
url: 'https://github.com/LuciusShadow',
username: 'LuciusShadow',
},
linkedin: {
url: "https://www.linkedin.com/in/saschabach/",
username: "saschabach"
}
url: 'https://www.linkedin.com/in/saschabach/',
username: 'saschabach',
},
},
// Project Repository
projects: {
portfolio: "https://github.com/LuciusShadow/portfolio-page",
ergoVR: "https://github.com/LuciusShadow/ErgoVR"
portfolio: 'https://github.com/LuciusShadow/portfolio-page',
ergoVR: 'https://github.com/LuciusShadow/ErgoVR',
},
// Professional Details
location: "Germany",
timezone: "CET/CEST",
// Branding
brand: {
tagline: "Building digital solutions with passion and precision",
description: "Freelance software developer specializing in modern web technologies and user experience."
}
location: 'Germany',
timezone: 'CET/CEST',
} as const;
// Helper functions
export const getObfuscatedEmail = () => {
return personalConfig.email.full.replace('@', ' [at] ').replace('.', ' [dot] ');
return personalConfig.email.full
.replace('@', ' [at] ')
.replace('.', ' [dot] ');
};
export const createEmailLink = () => {

View File

@ -1,26 +1,3 @@
// Centralized text configuration for the portfolio
// This file contains all configurable text used throughout the application
//
// Usage Examples:
//
// 1. Basic usage in components:
// const texts = getTexts();
// <h1>{texts.hero.title}</h1>
//
// 2. Using specific section texts:
// const contactTexts = getTexts().contact;
// <h2>{contactTexts.title}</h2>
//
// 3. Future internationalization ready:
// const texts = getTexts('en'); // or 'de', 'fr', etc.
//
// Benefits:
// - All text in one place for easy management
// - Ready for internationalization
// - Type-safe text references
// - Easy bulk text updates
// - Consistent text formatting
export interface TextConfig {
// Navigation
navigation: {
@ -51,23 +28,18 @@ export interface TextConfig {
description: string;
primaryButtonText: string;
secondaryButtonText: string;
statItems: Array<{
label: string;
value: string;
}>;
};
// About Section
about: {
title: string;
subtitle: string;
description: string;
profileAlt: string;
experienceLabel: string;
projectsLabel: string;
clientsLabel: string;
features: {
responsive: { title: string; description: string };
performance: { title: string; description: string };
modern: { title: string; description: string };
accessible: { title: string; description: string };
};
name: string;
bio: string;
};
// Services Section
@ -111,18 +83,6 @@ export interface TextConfig {
preferredContactValue: string;
locationLabel: string;
locationValue: string;
nameLabel: string;
emailLabel: string;
subjectLabel: string;
messageLabel: string;
sendButtonText: string;
successMessage: string;
errorMessage: string;
requiredFieldError: string;
invalidEmailError: string;
githubText: string;
linkedinText: string;
emailText: string;
};
// Imprint Page
@ -135,12 +95,62 @@ export interface TextConfig {
disclaimerTitle: string;
contentLiabilityTitle: string;
contentLiabilityText: string[];
linksLiabilityTitle: string;
linksLiabilityText: string[];
copyrightTitle: string;
copyrightText: string[];
privacyTitle: string;
privacyText: string;
detailedPrivacyPolicy: {
title: string;
introduction: string;
responsibleTitle: string;
responsibleText: string;
responsibleContact: string;
dataProtectionOfficerTitle: string;
dataProtectionOfficerText: string;
rightsTitle: string;
rightsIntro: string;
rights: Array<{ title: string; text: string }>;
revocationTitle: string;
revocationText: string;
objectionTitle: string;
objectionText: string;
objectionHighlight: string;
objectionContact: string;
dataDeletionTitle: string;
dataDeletionIntro: string;
dataDeletionReasons: string[];
retentionText: string;
webhostingTitle: string;
webhostingText: string;
webhostingDataTypes: string[];
webhostingPurpose: string;
webhostingDataCategories: {
affectedData: string;
affectedDataList: string[];
affectedPersons: string;
processingPurpose: string;
legalBasis: string;
provider: string;
};
hostingProvider: {
name: string;
address: string;
website: string;
};
contactTitle: string;
contactText: string;
contactDataCategories: {
affectedData: string[];
affectedPersons: string;
processingPurpose: string;
legalBasis: string;
};
securityTitle: string;
securityText: string;
changesTitle: string;
changesText: string;
disclaimer: string;
};
address: {
street: string;
city: string;
@ -197,7 +207,7 @@ export const defaultTexts: TextConfig = {
},
footer: {
imprintText: 'Impressum',
imprintText: 'Impressum und Datenschutz',
copyrightText: 'All rights reserved.',
githubAriaLabel: 'GitHub Profile',
linkedinAriaLabel: 'LinkedIn Profile',
@ -207,43 +217,23 @@ export const defaultTexts: TextConfig = {
hero: {
title: 'Building Digital Experiences',
description:
'Full-Stack Developer specializing in modern web technologies and user-centered design.',
'Freelance software developer specializing in implementing the European Accessibility Act, modern web technologies and user experience.',
primaryButtonText: 'View Projects',
secondaryButtonText: 'Get in Touch',
statItems: [
{ label: 'Years of Experience', value: '10+' },
{ label: 'Responsive Design', value: '100%' },
{ label: 'Technologies Used', value: '10+' },
{ label: 'Frameworks', value: '3+' },
],
},
about: {
title: 'About Me',
subtitle:
'Passionate developer with a focus on clean code and innovative solutions',
description:
'With years of experience in web development, I create robust and scalable applications using modern technologies. My approach combines technical expertise with user-centered design principles.',
profileAlt: 'Sascha Bach Profile',
experienceLabel: 'Years Experience',
projectsLabel: 'Projects Completed',
clientsLabel: 'Happy Clients',
features: {
responsive: {
title: 'Responsive Design',
description:
'Mobile-first approach ensuring optimal experience across all devices',
},
performance: {
title: 'Performance Optimized',
description:
'Fast loading times and smooth interactions for better user experience',
},
modern: {
title: 'Modern Technologies',
description:
'Latest frameworks and tools for cutting-edge web applications',
},
accessible: {
title: 'Accessibility Focused',
description:
'Inclusive design principles ensuring usability for everyone',
},
},
name: 'Sascha Bach',
bio: "As a dedicated web developer, I specialize in creating modern, responsive and barrier free web applications. I'm passionate about writing clean, maintainable code and ensuring exceptional user experiences across all devices and browsers. ",
},
services: {
@ -283,39 +273,20 @@ export const defaultTexts: TextConfig = {
preferredContactValue: 'Email',
locationLabel: 'Location',
locationValue: 'Germany',
nameLabel: 'Your Name',
emailLabel: 'Your Email',
subjectLabel: 'Subject',
messageLabel: 'Your Message',
sendButtonText: 'Send Message',
successMessage: "Message sent successfully! I'll get back to you soon.",
errorMessage:
'Failed to send message. Please try again or contact me directly.',
requiredFieldError: 'This field is required',
invalidEmailError: 'Please enter a valid email address',
githubText: 'Follow my work and contribute to open source projects',
linkedinText:
'Connect with me for professional networking and opportunities',
emailText: 'Send me a direct message for inquiries and collaborations',
},
imprint: {
title: 'Impressum',
title: 'Impressum und Datenschutz',
subtitle: 'Legal Information',
companyInfoTitle: 'Angaben gemäß § 5 TMG',
companyInfoTitle: 'Angaben gemäß § 5 DDG',
contactTitle: 'Kontakt',
responsibilityTitle: 'Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV',
responsibilityTitle: 'Vertretungsberechtigt',
disclaimerTitle: 'Haftungsausschluss',
contentLiabilityTitle: 'Haftung für Inhalte',
contentLiabilityText: [
'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.',
'Als Diensteanbieter sind wir gemäß § 7 Abs.1 DDG 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.',
'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.',
],
linksLiabilityTitle: 'Haftung für Links',
linksLiabilityText: [
'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.',
'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.',
],
copyrightTitle: 'Urheberrecht',
copyrightText: [
'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.',
@ -324,6 +295,131 @@ export const defaultTexts: TextConfig = {
privacyTitle: 'Datenschutz',
privacyText:
'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.',
detailedPrivacyPolicy: {
title: 'Datenschutzerklärung',
introduction:
'Mit dieser Datenschutzerklärung möchten wir Sie über Art, Umfang und Zweck der Verarbeitung von personenbezogenen Daten (im Folgenden auch nur als "Daten" bezeichnet) aufklären. Personenbezogene Daten sind alle Daten, die einen persönlichen Bezug zu Ihnen aufweisen, z. B. Name, Adresse, E-Mail-Adresse oder Ihr Nutzerverhalten. Die Datenschutzerklärung gilt für alle von uns vorgenommene Daten-Verarbeitungsvorgänge sowohl im Rahmen unserer Kerntätigkeit als auch für die von uns vorgehaltenen Online-Medien.',
responsibleTitle:
'Wer bei uns für die Datenverarbeitung verantwortlich ist',
responsibleText: 'Verantwortlich für die Datenverarbeitung ist:',
responsibleContact:
'Sascha Bach\nAlt-Tempelhof 15\n12099 Berlin\nDE\n01799364867\ninfo@sascha-bach.de',
dataProtectionOfficerTitle:
'Kontaktdaten unseres Datenschutzbeauftragten',
dataProtectionOfficerText:
'Unseren Datenschutzbeauftragten können Sie per E-Mail unter info [at] sascha-bach.de oder unter unserer Postadresse mit dem Zusatz „an den Datenschutzbeauftragten" erreichen.',
rightsTitle: 'Ihre Rechte nach der DSGVO',
rightsIntro:
'Nach der DSGVO stehen Ihnen die nachfolgend aufgeführten Rechte zu, die Sie jederzeit bei dem in Ziffer 1. dieser Datenschutzerklärung genannten Verantwortlichen geltend machen können:',
rights: [
{
title: 'Recht auf Auskunft',
text: 'Sie haben das Recht, von uns Auskunft darüber zu verlangen, ob und welche Daten wir von Ihnen verarbeiten.',
},
{
title: 'Recht auf Berichtigung',
text: 'Sie haben das Recht, die Berichtigung unrichtiger oder Vervollständigung unvollständiger Daten zu verlangen.',
},
{
title: 'Recht auf Löschung',
text: 'Sie haben das Recht, die Löschung Ihrer Daten zu verlangen.',
},
{
title: 'Recht auf Einschränkung',
text: 'Sie haben in bestimmten Fällen das Recht zu verlangen, dass wir Ihre Daten nur noch eingeschränkt bearbeiten.',
},
{
title: 'Recht auf Datenübertragbarkeit',
text: 'Sie haben das Recht zu verlangen, dass wir Ihnen oder einem anderen Verantwortlichen Ihre Daten in einem strukturierten, gängigen und maschinenlesebaren Format übermitteln.',
},
{
title: 'Beschwerderecht',
text: 'Sie haben das Recht, sich bei einer Aufsichtsbehörde zu beschweren. Zuständig ist die Aufsichtsbehörde Ihres üblichen Aufenthaltsortes, Ihres Arbeitsplatzes oder unseres Firmensitzes.',
},
],
revocationTitle: 'Widerrufsrecht',
revocationText:
'Sie haben das Recht, die von Ihnen erteilte Einwilligung zur Datenverarbeitung jederzeit zu widerrufen.',
objectionTitle: 'Widerspruchsrecht',
objectionText:
'Sie haben das Recht, jederzeit gegen die Verarbeitung Ihrer Daten, die wir auf unser berechtigtes Interesse nach Art. 6 Abs. 1 lit. f DSGVO stützen, Widerspruch einzulegen. Sofern Sie von Ihrem Widerspruchsrecht Gebrauch machen, bitten wir Sie um die Darlegung der Gründe. Wir werden Ihre personenbezogenen Daten dann nicht mehr verarbeiten, es sei denn, wir können Ihnen gegenüber nachweisen, dass zwingende schutzwürdige Gründe an der Datenverarbeitung Ihre Interessen und Rechte überwiegen.',
objectionHighlight:
'Unabhängig vom vorstehend Gesagten, haben Sie das jederzeitige Recht, der Verarbeitung Ihrer personenbezogenen Daten für Zwecke der Werbung und Datenanalyse zu widersprechen.',
objectionContact:
'Ihren Widerspruch richten Sie bitte an die oben angegebene Kontaktadresse des Verantwortlichen.',
dataDeletionTitle: 'Wann löschen wir Ihre Daten?',
dataDeletionIntro:
'Wir löschen Ihre Daten dann, wenn wir diese nicht mehr brauchen oder Sie uns dies vorgeben. Das bedeutet, dass - sofern sich aus den einzelnen Datenschutzhinweisen dieser Datenschutzerklärung nichts anderes ergibt - wir Ihre Daten löschen,',
dataDeletionReasons: [
'wenn der Zweck der Datenverarbeitung weggefallen ist und damit die jeweilige in den einzelnen Datenschutzhinweisen genannte Rechtsgrundlage nicht mehr besteht, also bspw.',
'nach Beendigung der zwischen uns bestehenden vertraglichen oder mitgliedschaftlichen Beziehungen (Art. 6 Abs. 1 lit. a DSGVO) oder',
'nach Wegfall unseres berechtigten Interesses an der weiteren Verarbeitung oder Speicherung Ihrer Daten (Art. 6 Abs. 1 lit. f DSGVO),',
'wenn Sie von Ihrem Widerrufsrecht Gebrauch machen und keine anderweitige gesetzliche Rechtsgrundlage für die Verarbeitung im Sinne von Art. 6 Abs. 1 lit. b-f DSGVO eingreift,',
'wenn Sie vom Ihrem Widerspruchsrecht Gebrauch machen und der Löschung keine zwingenden schutzwürdigen Gründe entgegenstehen.',
],
retentionText:
'Sofern wir (bestimmte Teile) Ihre(r) Daten jedoch noch für andere Zwecke vorhalten müssen, weil dies etwa steuerliche Aufbewahrungsfristen (in der Regel 6 Jahre für Geschäftskorrespondenz bzw. 10 Jahre für Buchungsbelege) oder die Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen aus vertraglichen Beziehungen (bis zu vier Jahren) erforderlich machen oder die Daten zum Schutz der Rechte einer anderen natürlichen oder juristischen Person gebraucht werden, löschen wir (den Teil) Ihre(r) Daten erst nach Ablauf dieser Fristen. Bis zum Ablauf dieser Fristen beschränken wir die Verarbeitung dieser Daten jedoch auf diese Zwecke (Erfüllung der Aufbewahrungspflichten).',
webhostingTitle: 'Webhosting',
webhostingText:
'Wir bedienen uns zum Vorhalten unserer Internetseiten eines Anbieters, auf dessen Server unsere Internetseiten gespeichert und für den Abruf im Internet verfügbar gemacht werden (Hosting). Hierbei können von dem Anbieter all diejenigen über den von Ihnen genutzten Browser übertragenen Daten verarbeitet werden, die bei der Nutzung unserer Internetseiten anfallen. Hierzu gehören insbesondere Ihre IP-Adresse, die der Anbieter benötigt, um unser Online-Angebot an den von Ihnen genutzten Browser ausliefern zu können sowie sämtliche von Ihnen über unsere Internetseite getätigten Eingaben. Daneben kann der von uns genutzte Anbieter',
webhostingDataTypes: [
'das Datum und die Uhrzeit des Zugriffs auf unsere Internetseite',
'Zeitzonendifferenz zur Greenwich Mean Time (GMT)',
'Zugriffsstatus (HTTP-Status)',
'die übertragene Datenmenge',
'der Internet-Service-Provider des zugreifenden Systems',
'der von Ihnen verwendete Browsertyp und dessen Version',
'das von Ihnen verwendete Betriebssystem',
'die Internetseite, von welcher Sie gegebenenfalls auf unsere Internetseite gelangt sind',
'die Seiten bzw. Unterseiten, welche Sie auf unserer Internetseite besuchen.',
],
webhostingPurpose:
'erheben. Die vorgenannten Daten werden als Logfiles auf den Servern unseres Anbieters gespeichert. Dies ist erforderlich, um die Stabilität und Sicherheit des Betriebs unserer Internetseite zu gewährleisten.',
webhostingDataCategories: {
affectedData: 'Betroffene Daten:',
affectedDataList: [
'Inhaltsdaten (bspw. Posts, Fotos, Videos)',
'Nutzungsdaten (bspw. Zugriffszeiten, angeklickte Webseiten)',
'Kommunikationsdaten (bspw. Informationen über das genutzte Gerät, IP-Adresse)',
],
affectedPersons: 'Betroffene Personen: Nutzer unserer Internetpräsenz',
processingPurpose:
'Verarbeitungszweck: Ausspielen unserer Internetseiten, Gewährleistung des Betriebs unserer Internetseiten',
legalBasis:
'Rechtsgrundlage: Berechtigtes Interesse, Art. 6 Abs. 1 lit. f DSGVO',
provider: 'Von uns beauftragte(r) Webhoster:',
},
hostingProvider: {
name: 'Bitpalast GmbH',
address: 'Postfach 19 15 64, D-14005 Berlin, Deutschland',
website: 'https://preiswerter-webserver-de.bitpalast.net/',
},
contactTitle: 'Kontaktaufnahme',
contactText:
'Soweit Sie uns über E-Mail, Soziale Medien, Telefon, Fax, Post, unser Kontaktformular oder sonstwie ansprechen und uns hierbei personenbezogene Daten wie Ihren Namen, Ihre Telefonnummer oder Ihre E-Mail-Adresse zur Verfügung stellen oder weitere Angaben zur Ihrer Person oder Ihrem Anliegen machen, verarbeiten wir diese Daten zur Beantwortung Ihrer Anfrage im Rahmen des zwischen uns bestehenden vorvertraglichen oder vertraglichen Beziehungen.',
contactDataCategories: {
affectedData: [
'Bestandsdaten (bspw. Namen, Adressen)',
'Kontakdaten (bspw. E-Mail-Adresse, Telefonnummer, Postanschrift)',
'Inhaltsdaten (Texte, Fotos, Videos)',
'Vertragsdaten (bspw. Vertragsgegenstand, Vertragsdauer)',
],
affectedPersons:
'Interessenten, Kunden, Geschäfts- und Vertragspartner',
processingPurpose:
'Kommunikation sowie Beantwortung von Kontaktanfragen, Büro und Organisationsverfahren',
legalBasis:
'Vertragserfüllung und vorvertragliche Anfragen, Art. 6 Abs. 1 lit. b DSGVO, berechtigtes Interesse, Art. 6 Abs. 1 lit. f DSGVO',
},
securityTitle: 'Sicherheitsmaßnahmen',
securityText:
'Wir treffen im Übrigen technische und organisatorische Sicherheitsmaßnahmen nach dem Stand der Technik, um die Vorschriften der Datenschutzgesetze einzuhalten und Ihre Daten gegen zufällige oder vorsätzliche Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder gegen den unbefugten Zugriff Dritter zu schützen.',
changesTitle: 'Aktualität und Änderung dieser Datenschutzerklärung',
changesText:
'Diese Datenschutzerklärung ist aktuell gültig und hat den Stand September 2025. Aufgrund geänderter gesetzlicher bzw. behördlicher Vorgaben kann es notwendig werden, diese Datenschutzerklärung anzupassen.',
disclaimer:
'Diese Datenschutzerklärung wurde mit Hilfe des Datenschutz-Generators von SOS Recht erstellt. SOS Recht ist ein Angebot der Mueller.legal Rechtsanwälte Partnerschaft mit Sitz in Berlin.',
},
address: {
street: 'Alt-Tempelhof 15',
city: '12099 Berlin',
@ -366,7 +462,7 @@ export const defaultTexts: TextConfig = {
contactTitle: 'Contact',
contactText:
'For any privacy-related questions or to exercise your rights, contact:',
contactEmail: 'info@sascha-bach.de',
contactEmail: 'info [at] sascha-bach.de',
legalBasisTitle: 'Legal Basis',
legalBasisText:
'Processing is based on your explicit consent (Art. 6(1)(a) GDPR).',

View File

@ -52,15 +52,6 @@ export default function ImprintPage() {
))}
</div>
<div className="imprint-page__subsection">
<h3 className="imprint-page__subsection-title">{texts.linksLiabilityTitle}</h3>
{texts.linksLiabilityText.map((text, index) => (
<p key={index} className="imprint-page__text">
{text}
</p>
))}
</div>
<div className="imprint-page__subsection">
<h3 className="imprint-page__subsection-title">{texts.copyrightTitle}</h3>
{texts.copyrightText.map((text, index) => (
@ -74,9 +65,147 @@ export default function ImprintPage() {
<div className="imprint-page__section">
<h2 className="imprint-page__section-title">{texts.privacyTitle}</h2>
<div className="imprint-page__info">
<p className="imprint-page__text">
{texts.privacyText}
</p>
<p>{texts.privacyText}</p>
</div>
</div>
<div className="imprint-page__section">
<h2 className="imprint-page__section-title">{texts.detailedPrivacyPolicy.title}</h2>
<div className="imprint-page__privacy-content">
{/* Introduction */}
<div className="imprint-page__privacy-section">
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.introduction}</p>
</div>
{/* Responsible for Data Processing */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.responsibleTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.responsibleText}</p>
<div className="imprint-page__contact-info">
{texts.detailedPrivacyPolicy.responsibleContact.split('\n').map((line, index) => (
<p key={index} className="imprint-page__contact-line">{line}</p>
))}
</div>
</div>
{/* Data Protection Officer */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.dataProtectionOfficerTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.dataProtectionOfficerText}</p>
</div>
{/* Rights under GDPR */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.rightsTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.rightsIntro}</p>
<ul className="imprint-page__rights-list">
{texts.detailedPrivacyPolicy.rights.map((right, index) => (
<li key={index} className="imprint-page__rights-item">
<strong>{right.title}:</strong> {right.text}
</li>
))}
</ul>
<h4 className="imprint-page__privacy-subsubtitle">{texts.detailedPrivacyPolicy.revocationTitle}</h4>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.revocationText}</p>
<h4 className="imprint-page__privacy-subsubtitle">{texts.detailedPrivacyPolicy.objectionTitle}</h4>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.objectionText}</p>
<p className="imprint-page__privacy-highlight">{texts.detailedPrivacyPolicy.objectionHighlight}</p>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.objectionContact}</p>
</div>
{/* Data Deletion */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.dataDeletionTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.dataDeletionIntro}</p>
<ul className="imprint-page__deletion-list">
{texts.detailedPrivacyPolicy.dataDeletionReasons.map((reason, index) => (
<li key={index} className="imprint-page__deletion-item">{reason}</li>
))}
</ul>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.retentionText}</p>
</div>
{/* Webhosting */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.webhostingTitle}</h3>
<p className="imprint-page__privacy-text">
{texts.detailedPrivacyPolicy.webhostingText}
</p>
<ul className="imprint-page__webhosting-list">
{texts.detailedPrivacyPolicy.webhostingDataTypes.map((dataType, index) => (
<li key={index} className="imprint-page__webhosting-item">{dataType}</li>
))}
</ul>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.webhostingPurpose}</p>
<div className="imprint-page__data-categories">
<p className="imprint-page__category-title">
<strong>{texts.detailedPrivacyPolicy.webhostingDataCategories.affectedData}</strong>
</p>
<ul className="imprint-page__category-list">
{texts.detailedPrivacyPolicy.webhostingDataCategories.affectedDataList.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<p><strong>{texts.detailedPrivacyPolicy.webhostingDataCategories.affectedPersons}</strong></p>
<p><strong>{texts.detailedPrivacyPolicy.webhostingDataCategories.processingPurpose}</strong></p>
<p><strong>{texts.detailedPrivacyPolicy.webhostingDataCategories.legalBasis}</strong></p>
<p><strong>{texts.detailedPrivacyPolicy.webhostingDataCategories.provider}</strong></p>
</div>
<div className="imprint-page__hosting-provider">
<p><strong>{texts.detailedPrivacyPolicy.hostingProvider.name}</strong></p>
<p>{texts.detailedPrivacyPolicy.hostingProvider.address}</p>
<p>
Datenschutzerklärung: {' '}
<a
href={texts.detailedPrivacyPolicy.hostingProvider.website}
target="_blank"
rel="noopener noreferrer"
className="imprint-page__external-link"
>
{texts.detailedPrivacyPolicy.hostingProvider.website}
</a>
</p>
</div>
</div>
{/* Contact */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.contactTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.contactText}</p>
<div className="imprint-page__data-categories">
<p className="imprint-page__category-title"><strong>Betroffene Daten:</strong></p>
<ul className="imprint-page__category-list">
{texts.detailedPrivacyPolicy.contactDataCategories.affectedData.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<p><strong>Betroffene Personen: </strong>{texts.detailedPrivacyPolicy.contactDataCategories.affectedPersons}</p>
<p><strong>Verarbeitungszweck: </strong>{texts.detailedPrivacyPolicy.contactDataCategories.processingPurpose}</p>
<p><strong>Rechtsgrundlage: </strong>{texts.detailedPrivacyPolicy.contactDataCategories.legalBasis}</p>
</div>
</div>
{/* Security Measures */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.securityTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.securityText}</p>
</div>
{/* Changes to Privacy Policy */}
<div className="imprint-page__privacy-section">
<h3 className="imprint-page__privacy-subtitle">{texts.detailedPrivacyPolicy.changesTitle}</h3>
<p className="imprint-page__privacy-text">{texts.detailedPrivacyPolicy.changesText}</p>
<p className="imprint-page__disclaimer">
<strong>{texts.detailedPrivacyPolicy.disclaimer}</strong>
</p>
</div>
</div>
</div>
</div>

View File

@ -156,7 +156,7 @@
gap: 2rem;
@include globals.desktop-only {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
}
}
@ -184,6 +184,10 @@
&--tertiary {
background: var(--feature-card-tertiary-bg);
}
&--quaternary {
background: var(--feature-card-quaternary-bg);
}
}
&__feature-header {
@ -206,6 +210,10 @@
&--tertiary {
color: var(--feature-icon-tertiary);
}
&--quaternary {
color: var(--feature-icon-quaternary);
}
}
&__feature-title {
@ -224,6 +232,10 @@
&--tertiary {
color: var(--feature-title-tertiary);
}
&--quaternary {
color: var(--feature-title-quaternary);
}
}
&__feature-content {
@ -244,6 +256,10 @@
&--tertiary {
color: var(--feature-description-tertiary);
}
&--quaternary {
color: var(--feature-description-quaternary);
}
}
}

View File

@ -102,6 +102,167 @@
}
}
// Privacy Policy Specific Styles
&__privacy-content {
display: flex;
flex-direction: column;
gap: 2rem;
}
&__privacy-section {
margin-bottom: 2rem;
&:last-child {
margin-bottom: 0;
}
}
&__privacy-subtitle {
font-size: 1.375rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
&__privacy-subsubtitle {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text);
margin: 1.5rem 0 0.75rem 0;
}
&__privacy-text {
line-height: 1.7;
color: var(--color-text-muted);
margin-bottom: 1rem;
text-align: justify;
&:last-child {
margin-bottom: 0;
}
}
&__privacy-highlight {
line-height: 1.7;
color: var(--color-text);
margin-bottom: 1rem;
padding: 1rem;
background: var(--bg-primary);
border-left: 4px solid var(--color-primary);
border-radius: 0 0.5rem 0.5rem 0;
font-weight: 500;
text-decoration: underline;
}
&__contact-info {
background: var(--bg-primary);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
margin: 1rem 0;
}
&__contact-line {
margin-bottom: 0.25rem;
color: var(--color-text);
font-family: monospace;
&:last-child {
margin-bottom: 0;
}
}
&__rights-list,
&__deletion-list,
&__webhosting-list,
&__category-list {
list-style: disc;
margin-left: 1.5rem;
margin-bottom: 1rem;
li {
color: var(--color-text-muted);
line-height: 1.6;
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
strong {
color: var(--color-text);
font-weight: 600;
}
}
// Nested lists
ul {
list-style: circle;
margin-left: 1rem;
margin-top: 0.5rem;
}
}
&__rights-item {
margin-bottom: 0.75rem !important;
}
&__data-categories {
background: var(--bg-primary);
padding: 1.5rem;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
margin: 1.5rem 0;
}
&__category-title {
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.75rem;
}
&__hosting-provider {
background: var(--bg-primary);
padding: 1.25rem;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
margin: 1rem 0;
p {
margin-bottom: 0.5rem;
color: var(--color-text);
&:last-child {
margin-bottom: 0;
}
}
}
&__external-link {
color: var(--color-primary);
text-decoration: none;
transition: color 0.2s ease;
margin-left: 0.5rem;
&:hover {
color: var(--color-primary-hover);
text-decoration: underline;
}
}
&__disclaimer {
background: var(--bg-primary);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
font-style: italic;
margin-top: 1.5rem;
color: var(--color-text-muted);
font-size: 0.9rem;
}
// Responsive design
@include globals.mobile-only {
padding: 5rem 0 3rem;
@ -121,5 +282,26 @@
&__subsection-title {
font-size: 1.125rem;
}
&__privacy-subtitle {
font-size: 1.25rem;
}
&__privacy-subsubtitle {
font-size: 1rem;
}
&__contact-info,
&__data-categories,
&__hosting-provider {
padding: 1rem;
}
&__rights-list,
&__deletion-list,
&__webhosting-list,
&__category-list {
margin-left: 1rem;
}
}
}

View File

@ -2,12 +2,17 @@
@use '../variables' as *;
.services-section {
min-height: 100vh;
min-height: auto;
display: flex;
align-items: center;
padding: $section-padding-y 0;
background: var(--services-background);
// Only apply min-height on larger screens where we have more space
@media (min-width: 1024px) and (min-height: 800px) {
min-height: 100vh;
}
&__container {
max-width: 80rem;
margin: 0 auto;
@ -61,15 +66,11 @@
&__grid {
display: grid;
gap: $services-grid-gap;
max-height: calc(100vh - 12rem);
overflow: hidden;
// Mobile: 1 card per row (320px - 767px)
@media (max-width: 767px) {
grid-template-columns: 1fr;
gap: $services-grid-gap-mobile;
max-height: none;
overflow: visible;
}
// Tablet Portrait: 2 cards per row (768px - 1023px)
@ -112,17 +113,22 @@
box-shadow: $shadow-card-hover;
}
// Responsive card heights
// Responsive card heights - more flexible approach
@media (min-width: 768px) {
height: 280px; // Fixed height for consistent rows
min-height: 280px; // Use min-height instead of fixed height
}
@media (min-width: 1024px) {
height: 300px;
min-height: 300px;
}
@media (min-width: 1280px) {
height: 320px;
min-height: 320px;
}
// Ensure cards don't get too tall on large screens
@media (min-width: 1920px) {
max-height: 400px;
}
&--primary {