diff --git a/.gitignore b/.gitignore index 18f7eade..6f2ace6d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ docs/dependency-metrics.json /backend/users.json /backend/node_modules /backend/frontend/portfolio* +/backend/frontend/github-repos.json # SonarQube /.scannerwork/ diff --git a/backend/pages/api/github-repos.ts b/backend/pages/api/github-repos.ts new file mode 100644 index 00000000..3061f263 --- /dev/null +++ b/backend/pages/api/github-repos.ts @@ -0,0 +1,350 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import fs from "fs"; +import path from "path"; + +const GITHUB_REPOS_FILE = path.join( + process.cwd(), + "frontend", + "github-repos.json", +); +const PORTFOLIO_FILE = path.join(process.cwd(), "frontend", "portfolio.json"); +const DEFAULT_REPO_PER_PAGE = 4; + +type Repository = { + id: number; + name: string; + description: string; + html_url: string; + homepage?: string; + stargazers_count: number; + forks_count: number; + language: string; + topics: string[]; + updated_at: string; +}; + +type SortOption = "updated" | "stars"; + +/** + * Gets the user's configured GitHub repos count from portfolio data + * @returns The number of repos to fetch per request + */ +function getReposCount(): number { + try { + if (!fs.existsSync(PORTFOLIO_FILE)) { + return DEFAULT_REPO_PER_PAGE; + } + + const portfolioContent = fs.readFileSync(PORTFOLIO_FILE, "utf8"); + const portfolioData = JSON.parse(portfolioContent); + + return portfolioData?.social?.githubReposCount || DEFAULT_REPO_PER_PAGE; + } catch (error) { + console.error("Error reading portfolio configuration:", error); + return DEFAULT_REPO_PER_PAGE; + } +} + +type GitHubReposData = { + updated: Repository[]; + stars: Repository[]; + lastUpdated: string | null; + metadata: { + version: string; + description: string; + username: string; + }; + fetchConfig: { + intervalHours: number; + reposPerPage: number; + }; +}; + +/** + * Fetches GitHub repositories for a given user and sort option + * @param githubUsername - The GitHub username + * @param sort - The sort option (updated or stars) + * @param reposCount - Number of repositories to fetch + * @returns Promise resolving to array of repositories + */ +async function fetchReposForSort( + githubUsername: string, + sort: SortOption, + reposCount: number, +): Promise { + try { + let response; + + if (sort === "stars") { + response = await fetch( + `https://api.github.com/search/repositories?q=user:${githubUsername}&sort=stars&order=desc&per_page=${reposCount}`, + ); + } else { + response = await fetch( + `https://api.github.com/users/${githubUsername}/repos?sort=updated&per_page=${reposCount}`, + ); + } + + if (!response.ok) { + throw new Error(`Failed to fetch repositories: ${response.status}`); + } + + const data = await response.json(); + return sort === "stars" ? data.items : data; + } catch (err) { + console.error(`Error fetching ${sort} repos for ${githubUsername}:`, err); + throw err; + } +} + +/** + * Ensures the GitHub repositories file exists and returns its data + * @returns The GitHub repositories data structure + */ +function ensureReposFileExists(): GitHubReposData { + if (!fs.existsSync(GITHUB_REPOS_FILE)) { + const initialData: GitHubReposData = { + updated: [], + stars: [], + lastUpdated: null, + metadata: { + version: "1.0.0", + description: "GitHub repositories data cache for EU compliance", + username: "", + }, + fetchConfig: { + intervalHours: 1, + reposPerPage: DEFAULT_REPO_PER_PAGE, + }, + }; + + const dir = path.dirname(GITHUB_REPOS_FILE); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(GITHUB_REPOS_FILE, JSON.stringify(initialData, null, 2)); + return initialData; + } + + try { + const fileContent = fs.readFileSync(GITHUB_REPOS_FILE, "utf8"); + return JSON.parse(fileContent); + } catch (error) { + console.error("Error reading github-repos.json:", error); + // Return default structure if file is corrupted + return { + updated: [], + stars: [], + lastUpdated: null, + metadata: { + version: "1.0.0", + description: "GitHub repositories data cache for EU compliance", + username: "", + }, + fetchConfig: { + intervalHours: 1, + reposPerPage: DEFAULT_REPO_PER_PAGE, + }, + }; + } +} + +/** + * Updates GitHub repositories for a specific user + * @param githubUsername - The GitHub username to update repositories for + * @param reposCount - Optional specific repos count to use + * @returns Promise that resolves when update is complete + */ +async function updateReposForUser( + githubUsername: string, + reposCount?: number, +): Promise { + try { + // Get the configured repos count (use provided count or read from portfolio) + const actualReposCount = reposCount || getReposCount(); + + // Fetch both updated and starred repos + const [updatedRepos, starredRepos] = await Promise.all([ + fetchReposForSort(githubUsername, "updated", actualReposCount), + fetchReposForSort(githubUsername, "stars", actualReposCount), + ]); + + const now = new Date().toISOString(); + const reposData: GitHubReposData = { + updated: updatedRepos, + stars: starredRepos, + lastUpdated: now, + metadata: { + version: "1.0.0", + description: "GitHub repositories data cache for EU compliance", + username: githubUsername, + }, + fetchConfig: { + intervalHours: 1, + reposPerPage: actualReposCount, + }, + }; + + // Ensure directory exists + const dir = path.dirname(GITHUB_REPOS_FILE); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(GITHUB_REPOS_FILE, JSON.stringify(reposData, null, 2)); + } catch (error) { + console.error(`Failed to update repos for user ${githubUsername}:`, error); + throw error; + } +} + +/** + * Determines if repositories should be updated based on last update time + * @param lastUpdated - ISO string of last update time or null + * @param intervalHours - Hours between updates + * @returns True if repositories should be updated + */ +function shouldUpdateRepos( + lastUpdated: string | null, + intervalHours: number, +): boolean { + if (!lastUpdated) return true; + + const lastUpdateTime = new Date(lastUpdated).getTime(); + const now = Date.now(); + const intervalMs = intervalHours * 60 * 60 * 1000; + + return now - lastUpdateTime >= intervalMs; +} + +/** + * Next.js API handler for GitHub repositories management + * @param req - The API request object + * @param res - The API response object + * @returns Promise resolving to void + */ +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method === "GET") { + try { + const { + username, + sort = "updated", + force = "false", + reposCount, + } = req.query; + + if (!username || typeof username !== "string") { + return res.status(400).json({ error: "GitHub username is required" }); + } + + const reposData = ensureReposFileExists(); + const sortOption = sort as SortOption; + + // Check if we should update the repos + const forceUpdate = force === "true"; + const shouldUpdate = + forceUpdate || + shouldUpdateRepos( + reposData.lastUpdated, + reposData.fetchConfig.intervalHours, + ); + + // Get current repos count configuration from parameter or portfolio file + const requestedReposCount = reposCount + ? parseInt(reposCount as string) + : null; + const currentReposCount = requestedReposCount || getReposCount(); + + // Check if we need to update (forced, stale data, different user, or repos count changed) + const needsUpdate = + shouldUpdate || + reposData.metadata.username !== username || + reposData[sortOption].length === 0 || + reposData.fetchConfig.reposPerPage !== currentReposCount; + + if (needsUpdate) { + try { + await updateReposForUser(username, currentReposCount); + // Re-read the updated data + const updatedData = ensureReposFileExists(); + return res.status(200).json({ + repos: updatedData[sortOption] || [], + lastUpdated: updatedData.lastUpdated, + fromCache: false, + }); + } catch { + // If update fails and current cached data is for the same user, return it + if ( + reposData.metadata.username === username && + reposData[sortOption].length > 0 + ) { + return res.status(200).json({ + repos: reposData[sortOption], + lastUpdated: reposData.lastUpdated, + fromCache: true, + warning: "Using cached data due to fetch error", + }); + } else { + return res.status(503).json({ + error: + "Failed to fetch GitHub repositories and no cached data available", + repos: [], + lastUpdated: null, + fromCache: false, + }); + } + } + } else { + // Return cached data for same user + return res.status(200).json({ + repos: reposData[sortOption] || [], + lastUpdated: reposData.lastUpdated, + fromCache: true, + }); + } + } catch (error) { + console.error("GitHub repos API error:", error); + return res.status(500).json({ + error: "Internal server error", + repos: [], + lastUpdated: null, + fromCache: false, + }); + } + } + + if (req.method === "POST") { + try { + const { username, action } = req.body; + + if (!username || typeof username !== "string") { + return res.status(400).json({ error: "GitHub username is required" }); + } + + if (action === "refresh") { + await updateReposForUser(username); + const reposData = ensureReposFileExists(); + + return res.status(200).json({ + message: "GitHub repositories updated successfully", + lastUpdated: reposData.lastUpdated, + }); + } else { + return res.status(400).json({ error: "Invalid action" }); + } + } catch (error) { + console.error("GitHub repos refresh error:", error); + return res + .status(500) + .json({ error: "Failed to refresh GitHub repositories" }); + } + } + + res.setHeader("Allow", ["GET", "POST"]); + res.status(405).json({ error: "Method not allowed" }); +} diff --git a/src/App.tsx b/src/App.tsx index 53d6b6d7..d193a217 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,8 @@ import { Route, Routes } from "react-router-dom"; import IndexPage from "@/pages/index"; import EditPage from "@/pages/edit"; +import ImprintPage from "@/pages/imprint"; +import PrivacyPage from "@/pages/privacy"; /** * Main application component with routing @@ -12,6 +14,8 @@ function App() { } path="/" /> } path="/edit" /> + } path="/imprint" /> + } path="/privacy" /> ); } diff --git a/src/components/footer.tsx b/src/components/footer.tsx new file mode 100644 index 00000000..177b6d9f --- /dev/null +++ b/src/components/footer.tsx @@ -0,0 +1,36 @@ +import { Link } from "@heroui/link"; + +/** + * Footer component with legal links for EU compliance + * @returns Footer component + */ +export const Footer = () => { + return ( +
+
+
+
+ © {new Date().getFullYear()} Developer Portfolio. All rights + reserved. +
+
+ + Imprint + + + Privacy Policy + +
+
+
+
+ ); +}; diff --git a/src/components/portfolio/github-integration.tsx b/src/components/portfolio/github-integration.tsx index a058729d..cf96e927 100644 --- a/src/components/portfolio/github-integration.tsx +++ b/src/components/portfolio/github-integration.tsx @@ -16,8 +16,6 @@ import { usePortfolioData } from "@/hooks/usePortfolioData"; import { getLanguageColor } from "@/lib/language-colors"; import { GithubIntegrationSkeleton } from "@/components/ui/skeleton"; -const REPO_PER_PAGE = 4; - type Repository = { id: number; name: string; @@ -60,27 +58,66 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { githubUsername: string, sort: SortOption, ) => { + // Get the current repos count from portfolio data (including draft changes) + const currentReposCount = portfolioData?.social?.githubReposCount || 4; + let cachedData = null; + try { - let response; + // First try to read from local JSON file + try { + const localResponse = await fetch("/github-repos.json"); + if (localResponse.ok) { + const localData = await localResponse.json(); + // Check if cached data is for the requested user + if ( + localData.metadata?.username === githubUsername && + localData[sort] && + localData[sort].length > 0 + ) { + cachedData = localData; + + // If cache is fresh and configuration matches, use it immediately + if (localData.lastUpdated && localData.fetchConfig?.intervalHours) { + const lastUpdateTime = new Date(localData.lastUpdated).getTime(); + const now = Date.now(); + const intervalMs = + localData.fetchConfig.intervalHours * 60 * 60 * 1000; + const isStale = now - lastUpdateTime >= intervalMs; + + // Check if repos count configuration has changed + const cachedReposCount = localData.fetchConfig?.reposPerPage || 4; + const configChanged = currentReposCount !== cachedReposCount; + + if (!isStale && !configChanged) { + return localData[sort]; + } + } + } + } + } catch { + // Continue to backend API attempt + } - if (sort === "stars") { - // The users API does not support sorting by stars - response = await fetch( - `https://api.github.com/search/repositories?q=user:${githubUsername}&sort=stars&order=desc&per_page=${REPO_PER_PAGE}`, - ); - } else { - response = await fetch( - `https://api.github.com/users/${githubUsername}/repos?sort=updated&per_page=${REPO_PER_PAGE}`, + // If cache is stale, missing, or config changed, try backend API to fetch fresh data + try { + const response = await fetch( + `/api/github-repos?username=${githubUsername}&sort=${sort}&reposCount=${currentReposCount}`, ); + if (response.ok) { + const data = await response.json(); + return data.repos || []; + } + } catch { + // Backend is unavailable, fall back to cached data if available } - if (!response.ok) { - throw new Error("Failed to fetch repositories"); + // Fallback to stale cached data if backend is unavailable + if (cachedData && cachedData[sort]) { + return cachedData[sort]; } - const data = await response.json(); - - return sort === "stars" ? data.items : data; + // No data available at all + throw new Error("No repository data available"); } catch (err) { console.error(`Error fetching ${sort} repos:`, err); throw err; @@ -121,7 +158,11 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { }; initializeRepos().catch(console.error); - }, [portfolioData?.social?.github, portfolioLoading]); + }, [ + portfolioData?.social?.github, + portfolioData?.social?.githubReposCount, + portfolioLoading, + ]); useEffect(() => { const loadReposForSort = async () => { @@ -163,7 +204,11 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { }; loadReposForSort().catch(console.error); - }, [sortBy, portfolioData?.social?.github]); + }, [ + sortBy, + portfolioData?.social?.github, + portfolioData?.social?.githubReposCount, + ]); if (portfolioLoading || !portfolioData) { return ; diff --git a/src/components/portfolioEditor/index.tsx b/src/components/portfolioEditor/index.tsx index adaf6cdc..d3507f63 100644 --- a/src/components/portfolioEditor/index.tsx +++ b/src/components/portfolioEditor/index.tsx @@ -11,6 +11,7 @@ import WorkExperienceForm from "./work-experience-form"; import EducationForm from "./education-form"; import { ImportExportControls } from "./import-export-controls"; import { ContributorForm } from "./contributor-form"; +import { LegalInfoForm } from "./legal-info-form"; import { usePortfolioEditor } from "@/lib/use-portfolio-editor.ts"; import { subtitle, title } from "@/components/primitives"; @@ -80,6 +81,8 @@ export function PortfolioEditor() { handleEducationDragEnd, // Contributor functions handleContributorChange, + // Legal info functions + handleLegalInfoChange, // Import/Export handleImportPortfolioData, } = usePortfolioEditor(); @@ -90,6 +93,7 @@ export function PortfolioEditor() { { key: "skills", label: "Skills" }, { key: "experience", label: "Work Experience" }, { key: "education", label: "Education" }, + { key: "legal", label: "Legal Info" }, ...(isContributor(portfolioData?.social?.github) ? [{ key: "contributor", label: "Contributor" }] : []), @@ -166,6 +170,26 @@ export function PortfolioEditor() { portfolioData={portfolioData} /> ); + case "legal": + return ( + + ); case "contributor": return ( void; +} + +/** + * Legal information form component for EU compliance + * @param props - Component props + * @param props.legalInfo - Current legal information + * @param props.onLegalInfoChange - Callback for legal info changes + * @returns Legal info form component + */ +export function LegalInfoForm({ + legalInfo, + onLegalInfoChange, +}: LegalInfoFormProps) { + const handleInputChange = (field: keyof LegalInfo, value: string) => { + onLegalInfoChange({ + ...legalInfo, + [field]: value, + }); + }; + + return ( + + +
+

Legal Information

+

+ Configure your legal information for Imprint and Privacy Policy + pages (EU compliance) +

+
+
+ + +
+ handleInputChange("fullName", value)} + /> + handleInputChange("email", value)} + /> + handleInputChange("streetAddress", value)} + /> + handleInputChange("phone", value)} + /> + handleInputChange("zipCode", value)} + /> + handleInputChange("city", value)} + /> + handleInputChange("country", value)} + /> + handleInputChange("vatId", value)} + /> +
+ + + +
+

+ Content Responsibility (German law § 55 Abs. 2 RStV) +

+
+ + handleInputChange("responsiblePerson", value) + } + /> + + handleInputChange("responsibleAddress", value) + } + /> +
+
+ +
+

+ Note: Fill in all required fields to automatically + remove warning banners from your Imprint and Privacy Policy pages. + This information is required for EU legal compliance. +

+
+
+
+ ); +} diff --git a/src/components/portfolioEditor/social-links-form.tsx b/src/components/portfolioEditor/social-links-form.tsx index 9b37bf1e..76743e52 100644 --- a/src/components/portfolioEditor/social-links-form.tsx +++ b/src/components/portfolioEditor/social-links-form.tsx @@ -28,18 +28,32 @@ export function SocialLinksForm({

Social Media Profiles

- - github.com/ - - } - value={portfolioData.social.github} - onChange={onSocialChange} - /> +
+ + github.com/ + + } + value={portfolioData.social.github} + onChange={onSocialChange} + /> + +
{ + const k = Array.from(keys as Set)[0]; + if (k === "en" || k === "de") setLang(k); + }} + > + English + Deutsch + +
+ + + +
+

{t.intro}

+
+ +
+
+

{t.ownerTitle}

+

+ {displayName} +

+
+ +
+

{t.addressTitle}

+

+ {displayStreetAddress} +
+ {displayZipCity} +
+ {displayCountry} +

+
+ +
+

+ {t.contactEmailTitle} +

+

+ {t.email} {displayEmail} +

+
+ + {(legal.phone || !hasRequiredFields) && ( +
+

+ {t.telephoneTitle} +

+ {legal.phone ? ( +

+ {t.phone} {displayPhone} +

+ ) : ( +

{displayPhone}

+ )} +

+ {t.telephoneNote} +

+
+ )} + + {(legal.vatId || !hasRequiredFields) && ( +
+

{t.vatTitle}

+

{displayVatId}

+

+ {t.vatNote} +

+
+ )} + +
+

+ {t.responsibleTitle} +

+

+ {displayResponsiblePerson} +
+ {displayResponsibleAddress} +

+
+ +
+

{t.purposeTitle}

+

{t.purposeText}

+
+
+ +
+

{t.disclaimerTitle}

+ +
+
+

+ {t.contentLiabilityTitle} +

+

{t.contentLiabilityText}

+
+ +
+

+ {t.externalLinksTitle} +

+

{t.externalLinksText}

+
+ +
+

+ {t.copyrightTitle} +

+

{t.copyrightText}

+
+ +
+

+ {t.noWarningTitle} +

+

{t.noWarningText}

+
+
+
+ + +

+ {t.lastUpdated}{" "} + {new Date().toLocaleDateString( + lang === "de" ? "de-DE" : "en-US", + { + year: "numeric", + month: "long", + day: "numeric", + }, + )} +

+
+ + + + + ); +} diff --git a/src/pages/privacy.tsx b/src/pages/privacy.tsx new file mode 100644 index 00000000..3e4f7836 --- /dev/null +++ b/src/pages/privacy.tsx @@ -0,0 +1,669 @@ +import { Alert } from "@heroui/alert"; +import { Card, CardBody, CardHeader } from "@heroui/card"; +import { Divider } from "@heroui/divider"; +import { Select, SelectItem } from "@heroui/select"; +import { useEffect, useMemo, useState } from "react"; + +import DefaultLayout from "@/layouts/default"; +import { usePortfolioData } from "@/hooks/usePortfolioData"; + +/** + * Privacy Policy page component for EU legal compliance (GDPR) + * @returns Privacy Policy page component + */ +export default function PrivacyPage() { + const { portfolioData, isLoading } = usePortfolioData(); + const getInitialLang = (): "en" | "de" => { + try { + const saved = localStorage.getItem("legalLang"); + if (saved === "de" || saved === "en") return saved; + const nav = + (navigator.languages && navigator.languages[0]) || + navigator.language || + ""; + return nav.toLowerCase().startsWith("de") ? "de" : "en"; + } catch { + return "en"; + } + }; + const [lang, setLang] = useState<"en" | "de">(getInitialLang); + useEffect(() => { + try { + localStorage.setItem("legalLang", lang); + } catch { + // ignore persistence errors + } + }, [lang]); + + const t = useMemo(() => { + if (lang === "de") { + return { + displayTitle: "Datenschutzerklärung", + noticeTitle: "Hinweis Vorlage", + noticeDesc: + 'Bitte prüfen und passen Sie diese Datenschutzerklärung an Ihre konkreten Verarbeitungstätigkeiten und rechtlichen Anforderungen an. Konfigurieren Sie Ihre rechtlichen Informationen im Editor unter "Legal Info".', + intro: + "Wir nehmen den Schutz der Privatsphäre unserer Website-Besucher ernst. Diese Datenschutzerklärung erklärt, wie wir personenbezogene Daten erheben, verwenden und schützen, wenn Sie diese Website nutzen, in Übereinstimmung mit der EU-Datenschutz-Grundverordnung (DSGVO). Auch wenn unsere Website keine Benutzerregistrierung erfordert oder aktiv personenbezogene Daten verfolgt, kann allein der Besuch der Website die Verarbeitung bestimmter personenbezogener Informationen (z. B. IP-Adressen) beinhalten. Im Folgenden stellen wir alle erforderlichen Informationen gemäß DSGVO Artikel 13 zur Verfügung.", + controllerTitle: "1. Verantwortlicher und Kontaktinformationen", + controllerText: + 'Der "Verantwortliche" (die für die Datenverarbeitung verantwortliche Person) für diese Website ist die Person, die die Website betreibt (siehe Impressum unten für vollständige Kontaktdaten). Für alle datenschutzbezogenen Anfragen können Sie den Website-Eigentümer über die im Impressum angegebene E-Mail-Adresse kontaktieren.', + dataCollectionTitle: "2. Datenerfassung und -verwendung", + dataCollectionIntro: + "Wir erheben und verarbeiten personenbezogene Daten nur in dem minimal notwendigen Umfang, um diese Portfolio-Website zu betreiben und ihre Funktionen bereitzustellen. Es gibt keine Funktionen für allgemeine Besucher, personenbezogene Informationen zu übermitteln (keine öffentlichen Benutzerkonten, Kommentare oder Tracker). Insbesondere werden Daten in den folgenden Szenarien verarbeitet:", + websiteAccessTitle: "Website-Zugriff (Server-Logs)", + websiteAccessText: + "Wenn Sie die Website besuchen, verarbeitet unser Webserver automatisch bestimmte Informationen in Server-Log-Dateien. Dazu gehören Ihre IP-Adresse, Datum und Uhrzeit des Zugriffs, Browser-Typ und aufgerufene Seiten. Wir speichern diese Daten nicht dauerhaft; sie werden nur vorübergehend verwendet, um die Verbindung herzustellen und sicherzustellen, dass die Website Ihnen Inhalte korrekt und sicher liefert. Diese Verarbeitung ist technisch notwendig für die Bereitstellung der Website und den Schutz vor Missbrauch (Rechtsgrundlage: DSGVO Art. 6(1)(f), berechtigtes Interesse am Betrieb einer sicheren Website).", + contactEmailTitle: "Kontakt per E-Mail", + contactEmailText: + "Wenn Sie sich dafür entscheiden, den Website-Eigentümer über die angegebene E-Mail-Adresse zu kontaktieren (zum Beispiel, um sich nach dem Portfolio oder den Dienstleistungen zu erkundigen), erhalten wir die personenbezogenen Daten, die Sie in dieser E-Mail angeben (wie Ihre E-Mail-Adresse und andere Kontaktinformationen oder den Nachrichteninhalt). Diese Daten werden ausschließlich verwendet, um auf Ihre Anfrage zu antworten und sie zu bearbeiten. Die Angabe Ihrer E-Mail oder anderer Daten erfolgt freiwillig, aber wir können Ihnen ohne diese nicht antworten. Die Rechtsgrundlage für die Verarbeitung dieser Daten ist Ihre Einwilligung und/oder die Durchführung vorvertraglicher Maßnahmen auf Ihre Anfrage hin (DSGVO Art. 6(1)(a) oder Art. 6(1)(b)). Wir bewahren solche Kommunikation nur so lange auf, wie es zur Erfüllung Ihrer Anfrage oder gesetzlich erforderlich ist.", + noAccountsTitle: "Keine Benutzerkonten für Besucher", + noAccountsText: + "Diese Website bietet keine Kontoregistrierung oder Anmeldung für allgemeine Besucher. Nur der Website-Eigentümer (Administrator) hat ein passwortgeschütztes Konto zur Bearbeitung des Portfolios. Daher erheben wir keine Registrierungsdaten von Besuchern.", + thirdPartyTitle: "3. Drittanbieter-Dienste und Integrationen", + thirdPartyIntro: + "Wir nutzen einige externe Dienste, um bestimmte Funktionen dieser Website bereitzustellen. In allen Fällen werden diese Dienste datenschutzbewusst eingesetzt, und keine personenbezogenen Daten von Besuchern werden mit ihnen geteilt. Wir legen diese Integrationen aus Transparenzgründen offen:", + githubApiTitle: "GitHub API (Repository-Anzeige)", + githubApiText: + "Unsere Portfolio-Website zeigt Informationen über die öffentlichen GitHub-Repositories des Website-Eigentümers an (wie Projektnamen und Sterne). Dazu ruft unser Server regelmäßig öffentliche Daten von der GitHub API ab (ein Dienst von GitHub, Inc. in den USA). Die abgerufenen Daten umfassen Repository-Namen, Beschreibungen, Update-Daten und Stern-Anzahlen – alles Informationen, die öffentlich auf GitHub verfügbar sind. Diese Informationen werden auf unserem Server (in einer lokalen Datei) zwischengespeichert, um direkte Anfragen von jedem Besucher-Browser an GitHub zu vermeiden. Wenn Sie die Website besuchen, sehen Sie die zwischengespeicherten Daten; Ihr Browser überträgt keine personenbezogenen Daten an GitHub. Wir senden keine Besucher-Informationen an die GitHub API. (Weitere Informationen zu GitHubs Umgang mit Daten finden Sie in GitHubs eigener Datenschutzerklärung auf ihrer Website.)", + googleGeminiTitle: "Google Gemini AI (Content-Generierung)", + googleGeminiText: + 'Der Website-Eigentümer nutzt Googles Generative Language API (Teil von Google Clouds AI-Services), um bei der Generierung von Portfolio-Inhalten zu helfen (zum Beispiel ein autobiographischer "Über mich"-Text basierend auf den Eingaben des Eigentümers). Diese Funktion ist nur für den Website-Eigentümer (Administrator) zugänglich und nicht für öffentliche Besucher. Bei der Nutzung dieses Dienstes werden bestimmte personenbezogene Informationen des Website-Eigentümers – wie Profildetails wie Name, Berufsbezeichnung, Fähigkeiten und Erfahrungen – an Googles Server gesendet, um den Text zu generieren. Der Service wird von Google LLC bereitgestellt, die Daten auf Servern außerhalb der EU verarbeiten kann (z. B. in den Vereinigten Staaten). Google hat sich verpflichtet, dass Daten, die für generative KI-Verarbeitung übermittelt werden, nicht ohne Erlaubnis zur Schulung von Googles Modellen verwendet werden und nur vorübergehend zur Bereitstellung des Dienstes aufbewahrt werden. Keine Besucherdaten werden über diese Funktion an Google gesendet. Wir haben sichergestellt, dass diese Integration in Übereinstimmung mit geltenden Datenschutzbestimmungen verwendet wird (zum Beispiel durch Zustimmung zu Googles Datenverarbeitungsbestimmungen). Weitere Informationen finden Sie in Googles Datenschutzerklärung und ihren KI-Datennutzungsbedingungen.', + externalLinksTitle: "Hinweis zu externen Links", + externalLinksText: + "Diese Website kann Links zu den Profilen des Website-Eigentümers auf externen Plattformen enthalten (zum Beispiel Links zu GitHub, LinkedIn oder anderen sozialen Medien). Wenn Sie solche Links anklicken, werden Sie zu Drittanbieter-Websites weitergeleitet, die außerhalb unserer Kontrolle liegen. Diese Plattformen können personenbezogene Daten verarbeiten (zum Beispiel durch Protokollierung Ihres Besuchs oder wenn Sie in deren Service eingeloggt sind). Wir sind nicht verantwortlich für den Inhalt oder die Datenschutzpraktiken externer Websites. Wir empfehlen Ihnen, die Datenschutzerklärungen aller Drittanbieter-Websites zu überprüfen, die Sie über Links auf unserem Portfolio besuchen.", + cookiesTitle: "4. Verwendung von Cookies und lokalem Speicher", + cookiesIntro: + "Diese Website verwendet keine Cookies oder Tracking-Skripte. Wir setzen keine Cookies, die eine Einwilligung nach EU-Recht erfordern würden. Die Anwendung verwendet jedoch lokalen Speicher in Ihrem Webbrowser für bestimmte Funktionalitäten:", + localStorageTitle: "Lokaler Speicher", + localStorageText: + "Die Bearbeitungsschnittstelle des Portfolios (nur für den Website-Eigentümer zugänglich) verwendet den lokalen Speicher des Browsers, um vorübergehend Entwurfs-Portfolio-Daten und Benutzereinstellungen zu speichern. Diese Daten werden lokal im Browser des Website-Eigentümers gespeichert und nicht an unseren Server oder Dritte übertragen. Besucher, die das Portfolio betrachten, haben keine Daten, die über lokalen Speicher von unserer Website in ihren Browsern gespeichert werden (abgesehen von standardmäßigem Browser-Caching statischer Dateien).", + sessionStorageTitle: "Session-Speicher", + sessionStorageText: + "Wenn sich der Website-Eigentümer in die Admin-Schnittstelle einloggt, wird ein Session-Token (JWT) im Session-Speicher des Browsers gespeichert, um die Login-Sitzung aufrechtzuerhalten. Dies ist eine notwendige technische Maßnahme für die Authentifizierung. Es läuft nach einer festgelegten Zeit ab und ist für Dritte nicht zugänglich oder wird von ihnen verwendet. Normale Besucher erhalten keine Session-Tokens, da es für sie keine Anmeldung gibt.", + cookieBannerNote: + "Da wir keine Cookies oder ähnliche Tracker für Besucher verwenden, zeigen wir kein Cookie-Einverständnis-Banner an. Sollten wir in Zukunft nicht-essenzielle Cookies oder Tracking-Tools einführen, werden wir einen Einverständnismechanismus implementieren, wie gesetzlich vorgeschrieben.", + securityTitle: "5. Datensicherheit", + securityText: + "Wir setzen angemessene technische und organisatorische Maßnahmen um, um die von uns verarbeiteten personenbezogenen Daten vor unbefugtem Zugriff, Änderung, Offenlegung oder Zerstörung zu schützen. Zum Beispiel werden sensible Informationen (wie die Admin-Anmeldedaten des Website-Eigentümers oder API-Schlüssel für die oben genannten Dienste) verschlüsselt auf dem Server gespeichert. Die Website wird ausschließlich über HTTPS bereitgestellt, was bedeutet, dass zwischen Ihrem Browser und unserem Server übertragene Daten während der Übertragung verschlüsselt sind. Obwohl kein System perfekte Sicherheit garantieren kann, streben wir danach, bewährte Praktiken zum Schutz von Daten zu befolgen.", + retentionTitle: "6. Datenspeicherung", + retentionIntro: + "Wir minimieren unsere Datenspeicherzeiten auf das Notwendige:", + serverLogsTitle: "Server-Logs", + serverLogsText: + "Routinemäßige Server-Log-Daten (IP-Adressen und Besuchsdetails wie oben beschrieben) werden normalerweise nur für kurze Zeit zur Fehlerbehebung und Sicherheitsüberwachung aufbewahrt. Sie werden automatisch auf rollierender Basis gelöscht, normalerweise innerhalb weniger Tage bis höchstens weniger Wochen. Wir archivieren oder bewahren diese Daten nicht langfristig auf, es sei denn, sie werden für Sicherheitsuntersuchungen benötigt.", + emailInquiriesTitle: "E-Mail-Anfragen", + emailInquiriesText: + "Wenn Sie uns per E-Mail kontaktieren, bewahren wir Ihre Nachricht und Kontaktdaten so lange auf, wie es zur Antwort und Lösung Ihrer Anfrage erforderlich ist. Normalerweise löschen wir die Korrespondenz, sobald Ihre Anfrage vollständig bearbeitet wurde. In einigen Fällen können wir die Kommunikation für einen längeren Zeitraum aufbewahren, wenn dies aus rechtlichen Gründen oder zur Aufzeichnung erforderlich ist (zum Beispiel, wenn Ihre Anfrage zu einer vertraglichen Beziehung führen könnte oder wenn sie für die Rechenschaftspflicht benötigt wird).", + portfolioContentTitle: "Portfolio-Inhalte", + portfolioContentText: + "Alle im Portfolio angezeigten personenbezogenen Informationen (wie der Name des Website-Eigentümers, Biographie, Fähigkeiten usw.) werden gespeichert, bis der Website-Eigentümer sie aktualisiert oder entfernt. Diese Informationen unterliegen der Kontrolle des Website-Eigentümers. Es ist wichtig zu beachten, dass dies Informationen sind, die der Website-Eigentümer über sich selbst zu veröffentlichen gewählt hat, und sie bleiben veröffentlicht, bis er sie zu ändern beschließt.", + legalBasesTitle: "7. Rechtsgrundlagen für die Verarbeitung", + legalBasesIntro: + "Wir verarbeiten personenbezogene Daten nur in Übereinstimmung mit der DSGVO. Die Rechtsgrundlagen für unsere Verarbeitungstätigkeiten sind wie folgt:", + consentTitle: "Einwilligung (DSGVO Art. 6(1)(a))", + consentText: + "Wenn Sie uns freiwillig kontaktieren und personenbezogene Informationen bereitstellen, gehen wir davon aus, dass Sie der Verwendung dieser Daten zur Antwort einwilligen. Sie können die Einwilligung jederzeit durch Benachrichtigung an uns widerrufen, woraufhin wir die Verarbeitung einstellen und Ihre Daten löschen (es sei denn, eine andere Rechtsgrundlage gilt für die weitere Aufbewahrung).", + contractualTitle: + "Vertragliche oder vorvertragliche Notwendigkeit (Art. 6(1)(b))", + contractualText: + "Wenn Ihr Kontakt oder Ihre Anfrage in Vorbereitung auf eine Dienstleistung oder Zusammenarbeit erfolgt (zum Beispiel erkundigen Sie sich nach der Beauftragung des Website-Eigentümers für ein Projekt), kann die Verarbeitung Ihrer Kontaktdaten notwendig sein, um auf Ihre Anfrage vor Vertragsabschluss zu reagieren.", + legitimateInterestsTitle: "Berechtigte Interessen (Art. 6(1)(f))", + legitimateInterestsText: + "Für alle technischen Verarbeitungen, die zum Betrieb der Website notwendig sind (wie Server-Logs, Sicherheitsmaßnahmen und Integration von Drittanbieter-Inhalten wie beschrieben), stützen wir uns auf unser berechtigtes Interesse an der Bereitstellung einer sicheren, funktionalen und effizienten Website. Wir haben diese Interessen gegen Ihre Datenschutzrechte abgewogen und festgestellt, dass diese Datenverarbeitung in minimaler, datenschutzschonender Weise Ihre Rechte oder Freiheiten nicht beeinträchtigt. Sie haben das Recht, der Verarbeitung aufgrund berechtigter Interessen zu widersprechen (siehe Abschnitt 9 über Ihre Rechte).", + legalObligationNote: + "Wir verarbeiten keine Daten basierend auf rechtlicher Verpflichtung (Art. 6(1)(c)) oder lebenswichtigen Interessen (Art. 6(1)(d)) im Kontext des normalen Website-Betriebs. Wenn wir jemals gesetzlich verpflichtet wären, Daten aufzubewahren oder offenzulegen (zum Beispiel eine gerichtliche Anordnung zur Offenlegung von Informationen an Behörden), würden wir dies unter rechtlicher Verpflichtung tun und Sie informieren, wenn erlaubt.", + transfersTitle: "8. Internationale Datenübertragungen", + transfersIntro: + "Wie bereits erwähnt, befinden sich einige unserer Dienstleister außerhalb der Europäischen Union:", + googleTransferTitle: "Google (Generative AI API)", + googleTransferText: + "Google LLC hat ihren Sitz in den Vereinigten Staaten. Wenn der Website-Eigentümer die Content-Generierungsfunktion nutzt, werden Daten an Googles Server gesendet und von diesen verarbeitet, die sich in den USA oder anderen Ländern außerhalb der EU befinden können. Google ist unter dem EU-US Data Privacy Framework zertifiziert (oder stützt sich auf Standardvertragsklauseln und andere Schutzmaßnahmen), um ein angemessenes Schutzniveau für personenbezogene Daten zu gewährleisten, die aus der EU übertragen werden. Wir senden nur die minimal notwendigen Daten und haben Googles Datenverarbeitungsbestimmungen zugestimmt, um den Datenschutz zu gewährleisten.", + githubTransferTitle: "GitHub", + githubTransferText: + "GitHub, Inc. hat ihren Sitz in den Vereinigten Staaten (mit möglicherweise globalen Servern). Die Daten, die wir von GitHubs API abrufen, sind öffentlich verfügbare Informationen über die Repositories des Website-Eigentümers. In diesem Fall werden keine personenbezogenen Daten von Nutzern oder Besuchern übertragen – es sind die eigenen öffentlichen Daten des Website-Eigentümers. Wenn Sie jedoch einen GitHub-Link anklicken, wird Ihr Browser direkt mit GitHubs Servern verbunden. Solche Interaktionen unterliegen GitHubs Bedingungen und können die Übertragung Ihrer Daten (wie IP-Adresse) in die USA beinhalten. Wir erinnern Sie daran, GitHubs Datenschutzerklärung zu überprüfen, wenn Sie ihre Website besuchen.", + noOtherTransfers: + "Wir übertragen oder teilen anderweitig keine personenbezogenen Daten mit Drittländern oder internationalen Organisationen. Unser Website-Hosting basiert in der EU (Deutschland), sodass Besucherdaten (wie IP-Adressen in Logs) innerhalb der EU verarbeitet werden.", + rightsTitle: "9. Ihre Rechte als betroffene Person", + rightsIntro: + "Unter der DSGVO haben Sie bestimmte Rechte bezüglich Ihrer personenbezogenen Daten. Da wir personenbezogene Daten über Sie nur in sehr begrenzten Szenarien verarbeiten, müssen diese Rechte hier selten ausgeübt werden – aber es ist wichtig, dass Sie sie kennen. Sie haben das Recht auf:", + accessTitle: "Zugang zu Ihren Daten", + accessText: + "Sie können eine Bestätigung anfordern, ob wir personenbezogene Daten über Sie verarbeiten, und wenn ja, eine Kopie dieser Daten anfordern (DSGVO Art. 15).", + rectificationTitle: "Berichtigung", + rectificationText: + "Wenn Sie glauben, dass die personenbezogenen Daten, die wir über Sie haben, ungenau oder unvollständig sind, haben Sie das Recht zu verlangen, dass wir sie korrigieren oder aktualisieren (Art. 16).", + erasureTitle: "Löschung", + erasureText: + 'Sie haben das "Recht auf Vergessenwerden". Unter bestimmten Umständen können Sie verlangen, dass wir personenbezogene Daten löschen, die wir über Sie haben (Art. 17). Zum Beispiel, wenn Sie uns kontaktiert haben und nun wünschen, dass Ihre Korrespondenz gelöscht wird, werden wir dieser Anfrage nachkommen, vorausgesetzt, wir haben keinen übergeordneten rechtlichen Grund, sie aufzubewahren.', + restrictionTitle: "Einschränkung der Verarbeitung", + restrictionText: + "Sie können uns bitten, die Verarbeitung Ihrer Daten einzuschränken (Art. 18) in bestimmten Fällen, etwa wenn Sie die Richtigkeit der Daten bestreiten oder die Verarbeitung rechtswidrig ist, aber Sie die Daten nicht gelöscht haben möchten.", + objectionTitle: "Widerspruch", + objectionText: + "Wenn wir Daten basierend auf unseren berechtigten Interessen verarbeiten, haben Sie das Recht, dieser Verarbeitung zu widersprechen (Art. 21). Wenn Sie Widerspruch einlegen, werden wir unsere Gründe für die Verarbeitung überprüfen und entweder die Verarbeitung einstellen oder unsere zwingenden berechtigten Gründe erklären, je nach Situation. Sie können auch jeder Direktwerbung widersprechen (obwohl wir keine betreiben).", + portabilityTitle: "Datenübertragbarkeit", + portabilityText: + "Für alle Daten, die Sie uns bereitgestellt haben und die wir durch automatisierte Mittel basierend auf Einwilligung oder Vertrag verarbeiten, können Sie verlangen, sie in einem gängigen maschinenlesbaren Format zu erhalten (Art. 20). (In der Praxis wären die einzigen Daten, die Sie uns bereitstellen könnten, eine E-Mail-Anfrage; wenn Sie diese jemals in einem übertragbaren Format benötigen würden, könnten wir den E-Mail-Verlauf oder relevante Daten bereitstellen.)", + withdrawConsentTitle: "Einwilligung widerrufen", + withdrawConsentText: + "Wenn wir Daten basierend auf Ihrer Einwilligung verarbeiten, haben Sie das Recht, diese Einwilligung jederzeit zu widerrufen (Art. 7(3)). Dies beeinträchtigt nicht die Rechtmäßigkeit der vor dem Widerruf durchgeführten Verarbeitung. (Zum Beispiel, wenn Sie eingewilligt haben, dass wir Ihre E-Mail zur Beantwortung einer Anfrage verwenden, können Sie diese Einwilligung später widerrufen – dann würden wir aufhören und Ihre Kontaktinformationen löschen.)", + complaintTitle: "Recht auf Beschwerde", + complaintText: + "Wenn Sie glauben, dass wir Ihre personenbezogenen Daten unter Verletzung geltender Gesetze verarbeitet haben, haben Sie das Recht, eine Beschwerde bei einer Aufsichtsdatenschutzbehörde einzureichen (Art. 77). Sie können dies bei der Behörde in dem EU-Land tun, in dem Sie leben, oder wo die angebliche Verletzung aufgetreten ist. In Deutschland können Sie sich an die Datenschutzbehörde des Bundeslandes wenden, in dem Sie wohnen oder wo der Website-Betreiber sich befindet. (Zum Beispiel Nordrhein-Westfalens Beauftragte für Datenschutz für eine in NRW gehostete Website usw.) Wir würden es schätzen, wenn Sie uns die Möglichkeit geben würden, Ihre Bedenken direkt zu klären, bevor Sie sich an eine Regulierungsbehörde wenden, also kontaktieren Sie uns gerne mit allen Problemen.", + exerciseRights: + "Sie können Ihre Rechte ausüben, indem Sie uns kontaktieren (siehe Impressum für Kontaktdaten). Wir werden auf Anfragen innerhalb der gesetzlichen Fristen und kostenlos antworten. Aus Sicherheitsgründen müssen wir möglicherweise Ihre Identität überprüfen, bevor wir bestimmte Anfragen erfüllen.", + automatedDecisionTitle: "10. Keine automatisierte Entscheidungsfindung", + automatedDecisionText: + "Wir verwenden keine personenbezogenen Daten für automatisierte Entscheidungsfindung oder Profiling, das rechtliche oder ähnlich bedeutsame Auswirkungen auf Sie hat (Art. 22 DSGVO). Der Besuch unserer Website und die Kontaktaufnahme mit uns beinhalten immer eine menschliche Behandlung aller personenbezogenen Daten.", + changesTitle: "11. Änderungen an dieser Datenschutzerklärung", + changesText: + 'Wir können diese Datenschutzerklärung von Zeit zu Zeit aktualisieren, um Änderungen an unserer Website oder rechtlichen Verpflichtungen widerzuspiegeln. Wenn wir Änderungen vornehmen, werden wir das Datum "zuletzt aktualisiert" am Ende der Erklärung aktualisieren. Wir empfehlen Ihnen, diese Erklärung regelmäßig beim Besuch unserer Website zu überprüfen, um über den Schutz Ihrer Daten informiert zu bleiben.', + contactInfoTitle: "12. Kontaktinformationen", + contactInfoText: + "Wenn Sie Fragen oder Bedenken zu dieser Datenschutzerklärung oder unserem Umgang mit personenbezogenen Daten haben, kontaktieren Sie bitte den Website-Eigentümer. Sie finden Namen und vollständige Kontaktinformationen im unten stehenden Impressum.", + phone: "Telefon:", + email: "E-Mail:", + policyNote: + "Diese Datenschutzerklärung wurde unter Berücksichtigung der EU-Datenschutz-Grundverordnung (DSGVO) erstellt.", + lastUpdated: "Zuletzt aktualisiert:", + } as const; + } + return { + displayTitle: "Privacy Policy", + noticeTitle: "Template Notice", + noticeDesc: + 'Please review and customize this privacy policy according to your specific data processing activities and legal requirements. Configure your legal information in the editor under "Legal Info".', + intro: + "We take the privacy of our website visitors seriously. This Privacy Policy explains how we collect, use, and protect personal data when you use this website, in compliance with the EU General Data Protection Regulation (GDPR). Even if our site does not require user registration or actively track personal data, simply visiting the site can involve processing certain personal information (e.g. IP addresses). Below we provide all required information in accordance with GDPR Article 13.", + controllerTitle: "1. Data Controller and Contact Information", + controllerText: + 'The "data controller" (the person responsible for data processing) for this website is the individual operating the site (see Imprint below for full contact details). For any privacy-related inquiries, you can contact the site owner via the email provided in the Imprint.', + dataCollectionTitle: "2. Data Collection and Use", + dataCollectionIntro: + "We only collect and process personal data to the minimal extent necessary to operate this portfolio website and provide its features. There are no features for general visitors to submit personal information (no public user accounts, comments, or trackers). Specifically, data is processed in the following scenarios:", + websiteAccessTitle: "Website Access (Server Logs)", + websiteAccessText: + "When you visit the website, our web server automatically processes certain information in server log files. This includes your IP address, date and time of access, browser type, and pages accessed. We do not store this data permanently; it is used only transiently to establish the connection and ensure the website delivers content to you correctly and securely. This processing is technically necessary for delivering the site and protecting against misuse (legal basis: GDPR Art. 6(1)(f), legitimate interest in operating a secure website).", + contactEmailTitle: "Contact via Email", + contactEmailText: + "If you choose to contact the site owner using the email address provided (for example, to inquire about the portfolio or services), we will receive the personal data you provide in that email (such as your email address and any other contact information or message content). This data will be used solely to respond to and manage your inquiry. Providing your email or other data is voluntary, but we cannot respond to you without it. The legal basis for processing this data is your consent and/or to take steps at your request (GDPR Art. 6(1)(a) or Art. 6(1)(b)). We will keep such communications only as long as necessary to fulfill your request or as required by law.", + noAccountsTitle: "No User Accounts for Visitors", + noAccountsText: + "This site does not offer account registration or login for general visitors. Only the site owner (administrator) has a password-protected account to edit the portfolio. As a result, we do not collect any registration data from visitors.", + thirdPartyTitle: "3. Third-Party Services and Integrations", + thirdPartyIntro: + "We use a few external services to provide certain features of this website. In all cases, these services are used in a privacy-conscious manner, and no personal data from visitors is shared with them. We disclose these integrations for transparency:", + githubApiTitle: "GitHub API (Repository Display)", + githubApiText: + "Our portfolio site displays information about the site owner's public GitHub repositories (such as project names and stars). To do this, our server periodically fetches public data from the GitHub API (a service provided by GitHub, Inc. in the USA). The data retrieved includes repository names, descriptions, update dates, and star counts – all of which are publicly available on GitHub. This information is cached on our server (in a local file) to avoid direct requests from each visitor's browser to GitHub. When you visit the site, you are viewing the cached data; your browser does not transmit any personal data to GitHub. We do not send any visitor information to the GitHub API. (For more on GitHub's handling of data, see GitHub's own privacy policy on their website.)", + googleGeminiTitle: "Google Gemini AI (Content Generation)", + googleGeminiText: + "The site owner uses Google's Generative Language API (part of Google Cloud's AI services) to help generate portions of the portfolio content (for example, an autobiographical \"About Me\" text based on the owner's input). This feature is only accessible to the site owner (administrator) and not to public visitors. In using this service, certain personal information provided by the site owner – such as profile details like name, professional title, skills and experience – is sent to Google's servers to generate the text. The service is provided by Google LLC, which may process data on servers outside the EU (e.g. in the United States). Google has committed that data submitted for generative AI processing is not used to train Google's models without permission and is only retained temporarily to provide the service. No visitor data is sent to Google through this feature. We have ensured that this integration is used in compliance with applicable data protection requirements (for example, by agreeing to Google's data processing terms). For more information, please refer to Google's Privacy Policy and their AI data usage terms.", + externalLinksTitle: "Note on External Links", + externalLinksText: + "This website may contain links to the site owner's profiles on external platforms (for example, links to GitHub, LinkedIn, or other social media). If you click such links, you will be directed to third-party websites which are outside of our control. Those platforms may process personal data (for instance, by logging your visit or if you are logged into their service). We are not responsible for the content or privacy practices of external sites. We recommend you review the privacy policies of any third-party sites you visit via links on our portfolio.", + cookiesTitle: "4. Use of Cookies and Local Storage", + cookiesIntro: + "This site does not use any cookies or tracking scripts. We do not set any cookies that would require consent under EU law. The application does, however, use local storage in your web browser for certain functionality:", + localStorageTitle: "Local Storage", + localStorageText: + "The portfolio's editing interface (accessible only to the site owner) uses the browser's local storage to temporarily save draft portfolio data and user preferences. This data is stored locally on the site owner's browser and is not transmitted to our server or any third party. Visitors viewing the portfolio do not have any data stored in their browsers via local storage by our site (aside from standard browser caching of static files).", + sessionStorageTitle: "Session Storage", + sessionStorageText: + "If the site owner logs into the admin interface, a session token (JWT) is stored in the browser's session storage to maintain the login session. This is a necessary technical measure for authentication. It expires after a set time and is not accessible to or used by any third party. Regular visitors do not receive any session tokens since there is no login for them.", + cookieBannerNote: + "Because we do not use cookies or similar trackers for visitors, we do not display a cookie consent banner. If in the future we introduce any non-essential cookies or tracking tools, we will implement a consent mechanism as required.", + securityTitle: "5. Data Security", + securityText: + "We implement appropriate technical and organizational measures to protect the personal data we process from unauthorized access, alteration, disclosure, or destruction. For instance, any sensitive information (such as the site owner's admin credentials or API keys for the services above) is stored in encrypted form on the server. The website is served exclusively over HTTPS, which means data transmitted between your browser and our server is encrypted in transit. While no system can guarantee perfect security, we strive to follow best practices to safeguard data.", + retentionTitle: "6. Data Retention", + retentionIntro: + "We minimize our data retention periods to only what is necessary:", + serverLogsTitle: "Server Logs", + serverLogsText: + "Routine server log data (IP addresses and visit details as described above) are typically retained only for a short duration for troubleshooting and security monitoring. They are automatically deleted on a rolling basis, usually within a few days to a few weeks at most. We do not archive or retain this data long-term unless required for security investigations.", + emailInquiriesTitle: "Email Inquiries", + emailInquiriesText: + "If you contact us via email, we will retain your message and contact details for as long as needed to respond and resolve your inquiry. Typically, once your request is fully addressed, we will delete the correspondence. In some cases, we may retain communications for a longer period if necessary for legal reasons or record-keeping (for example, if your inquiry could lead to a contractual relationship or if needed for accountability).", + portfolioContentTitle: "Portfolio Content", + portfolioContentText: + "All personal information displayed on the portfolio (such as the site owner's name, biography, skills, etc.) is stored until the site owner updates or removes it. This information is under the site owner's control. It is important to note that this is information the site owner has chosen to publish about themselves, and it remains published until they decide to change it.", + legalBasesTitle: "7. Legal Bases for Processing", + legalBasesIntro: + "We only process personal data in accordance with the GDPR. The legal grounds for our processing activities are as follows:", + consentTitle: "Consent (GDPR Art. 6(1)(a))", + consentText: + "If you voluntarily contact us and provide personal information, we assume you consent to our use of that data to respond. You may withdraw consent at any time by notifying us, in which case we will stop processing and delete your data (unless another legal basis applies for continued retention).", + contractualTitle: + "Contractual or Pre-contractual Necessity (Art. 6(1)(b))", + contractualText: + "If your contact or inquiry is in preparation for a service or collaboration (for example, you're inquiring about hiring the site owner for a project), processing your contact data may be necessary to take steps at your request prior to entering into a contract.", + legitimateInterestsTitle: "Legitimate Interests (Art. 6(1)(f))", + legitimateInterestsText: + "For all technical processing necessary to operate the website (such as server logs, security measures, and integration of third-party content as described), we rely on our legitimate interest in providing a safe, functional, and efficient website. We have balanced these interests against your privacy rights and have determined that this data processing in a minimal, privacy-preserving manner does not adversely affect your rights or freedoms. You have the right to object to processing based on legitimate interests (see Section 9 on your rights).", + legalObligationNote: + "We do not process any data based on legal obligation (Art. 6(1)(c)) or vital interests (Art. 6(1)(d)) in the context of normal website operation. If we ever were required by law to retain or disclose data (for example, a legal order to disclose information to authorities), we would do so under legal obligation, and we would inform you if permitted.", + transfersTitle: "8. International Data Transfers", + transfersIntro: + "As noted, some of our service providers are located outside the European Union:", + googleTransferTitle: "Google (Generative AI API)", + googleTransferText: + "Google LLC is based in the United States. When the site owner uses the content generation feature, data is sent to and processed by Google's servers which may be in the U.S. or other countries outside the EU. Google is certified under the EU-U.S. Data Privacy Framework (or relies on Standard Contractual Clauses and other safeguards) to ensure an adequate level of protection for personal data transferred from the EU. We only send the minimal necessary data and have agreed to Google's data processing terms to safeguard privacy.", + githubTransferTitle: "GitHub", + githubTransferText: + "GitHub, Inc. is based in the United States (with servers possibly globally). The data we retrieve from GitHub's API is publicly available information about the site owner's repositories. In this case, no personal data of users or visitors is being transferred – it's the site owner's own public data. However, if you click a GitHub link, your browser will connect to GitHub's servers directly. Such interactions are subject to GitHub's terms and may involve transfer of your data (like IP address) to the U.S. We remind you to check GitHub's privacy policy when visiting their site.", + noOtherTransfers: + "We do not otherwise transfer or share personal data with third countries or international organizations. Our website hosting is based in the EU (Germany), so visitor data (like IP addresses in logs) is handled within the EU.", + rightsTitle: "9. Your Rights as a Data Subject", + rightsIntro: + "Under the GDPR, you have certain rights regarding your personal data. Since we process personal data about you only in very limited scenarios, these rights may rarely need to be exercised here – but it's important you know them. You have the right to:", + accessTitle: "Access Your Data", + accessText: + "You can request confirmation of whether we are processing any personal data about you, and if so, request a copy of that data (GDPR Art. 15).", + rectificationTitle: "Rectification", + rectificationText: + "If you believe the personal data we hold about you is inaccurate or incomplete, you have the right to request that we correct or update it (Art. 16).", + erasureTitle: "Erasure", + erasureText: + 'You have the "right to be forgotten." In certain circumstances, you can request that we delete personal data we hold about you (Art. 17). For example, if you contacted us and now wish your correspondence to be deleted, we will honor that request, provided we have no overriding legal reason to keep it.', + restrictionTitle: "Restriction of Processing", + restrictionText: + "You can ask us to restrict the processing of your data (Art. 18) in certain cases, such as if you contest the accuracy of the data or the processing is unlawful but you do not want the data deleted.", + objectionTitle: "Objection", + objectionText: + "If we process data based on our legitimate interests, you have the right to object to that processing (Art. 21). If you lodge an objection, we will review our reasons for processing and either stop processing or explain our compelling legitimate grounds, depending on the situation. You can also object to any direct marketing (though we do none).", + portabilityTitle: "Data Portability", + portabilityText: + "For any data you provided to us and which we process by automated means based on consent or contract, you can request to receive it in a common machine-readable format (Art. 20). (In practice, the only data you might provide us would be an email inquiry; if you ever needed this in a portable format, we could provide the email thread or relevant data.)", + withdrawConsentTitle: "Withdraw Consent", + withdrawConsentText: + "If we process any data based on your consent, you have the right to withdraw that consent at any time (Art. 7(3)). This will not affect the lawfulness of processing done before the withdrawal. (For example, if you consented to us using your email to respond to a query, you can later withdraw that consent – then we would stop and delete your contact info.)", + complaintTitle: "Right to Lodge a Complaint", + complaintText: + "If you believe we have processed your personal data in violation of applicable laws, you have the right to file a complaint with a supervisory data protection authority (Art. 77). You can do this with the authority in the EU country where you live, or where the alleged infringement occurred. In Germany, you may contact the data protection authority of the federal state (Land) in which you reside or where the website operator is located. (For example, Nordrhein-Westfalen's Commissioner for Data Protection for a site hosted in NRW, etc.) We would appreciate the chance to address your concerns directly before you approach a regulator, so please feel free to contact us with any issues.", + exerciseRights: + "You can exercise your rights by contacting us (see Imprint for contact details). We will respond to requests within the statutory timeframes and free of charge. For security, we may need to verify your identity before fulfilling certain requests.", + automatedDecisionTitle: "10. No Automated Decision-Making", + automatedDecisionText: + "We do not use any personal data for automated decision-making or profiling that produces legal or similarly significant effects on you (Art. 22 GDPR). Visiting our site and contacting us will always involve human handling of any personal data.", + changesTitle: "11. Changes to this Privacy Policy", + changesText: + 'We may update this Privacy Policy from time to time to reflect changes in our website or legal obligations. When we make changes, we will update the "last updated" date at the bottom of the policy. We encourage you to review this policy periodically when visiting our site to stay informed about how we protect your data.', + contactInfoTitle: "12. Contact Information", + contactInfoText: + "If you have any questions or concerns about this Privacy Policy or how we handle personal data, please contact the site owner. You can find the name and full contact information in the Imprint below.", + phone: "Phone:", + email: "Email:", + policyNote: + "This privacy policy was created with consideration for the EU General Data Protection Regulation (GDPR).", + lastUpdated: "Last updated:", + } as const; + }, [lang]); + + if (isLoading) { + return ( + +
+
+ + +

+ Privacy Policy / Datenschutzerklärung +

+
+ + +

Loading…

+
+
+
+
+
+ ); + } + + const legal = portfolioData?.legal || {}; + const hasRequiredFields = + legal.fullName && + legal.streetAddress && + legal.city && + legal.country && + legal.email; + + const displayName = legal.fullName || "[Your Full Name]"; + const displayStreetAddress = legal.streetAddress || "[Your Street Address]"; + const displayZipCity = + legal.zipCode && legal.city + ? `${legal.zipCode} ${legal.city}` + : "[Your ZIP Code and City]"; + const displayCountry = legal.country || "[Your Country]"; + const displayPhone = legal.phone || "[Your Phone Number]"; + const displayEmail = legal.email || "[Your Email Address]"; + return ( + +
+
+ {!hasRequiredFields && ( +
+ +
+ )} + + + +
+

{t.displayTitle}

+ +
+
+ + +
+

{t.intro}

+
+ +
+

{t.controllerTitle}

+

{t.controllerText}

+

+ {displayName} +
+ {displayStreetAddress} +
+ {displayZipCity} +
+ {displayCountry} +

+

+ {legal.phone && ( + <> + {t.phone} {displayPhone} +
+ + )} + {t.email} {displayEmail} +

+
+ +
+

+ {t.dataCollectionTitle} +

+

{t.dataCollectionIntro}

+ +
+
+

+ {t.websiteAccessTitle} +

+

{t.websiteAccessText}

+
+ +
+

+ {t.contactEmailTitle} +

+

{t.contactEmailText}

+
+ +
+

+ {t.noAccountsTitle} +

+

{t.noAccountsText}

+
+
+
+ +
+

{t.thirdPartyTitle}

+

{t.thirdPartyIntro}

+ +
+
+

+ {t.githubApiTitle} +

+

{t.githubApiText}

+
+ +
+

+ {t.googleGeminiTitle} +

+

{t.googleGeminiText}

+
+ +
+

+ {t.externalLinksTitle} +

+

{t.externalLinksText}

+
+
+
+ +
+

{t.cookiesTitle}

+

{t.cookiesIntro}

+ +
+
+

+ {t.localStorageTitle} +

+

{t.localStorageText}

+
+ +
+

+ {t.sessionStorageTitle} +

+

{t.sessionStorageText}

+
+
+ +

{t.cookieBannerNote}

+
+ +
+

{t.securityTitle}

+

{t.securityText}

+
+ +
+

{t.retentionTitle}

+

{t.retentionIntro}

+ +
+
+

+ {t.serverLogsTitle} +

+

{t.serverLogsText}

+
+ +
+

+ {t.emailInquiriesTitle} +

+

{t.emailInquiriesText}

+
+ +
+

+ {t.portfolioContentTitle} +

+

{t.portfolioContentText}

+
+
+
+ +
+

{t.legalBasesTitle}

+

{t.legalBasesIntro}

+ +
+
+

{t.consentTitle}

+

{t.consentText}

+
+ +
+

+ {t.contractualTitle} +

+

{t.contractualText}

+
+ +
+

+ {t.legitimateInterestsTitle} +

+

{t.legitimateInterestsText}

+
+
+ +

{t.legalObligationNote}

+
+ +
+

{t.transfersTitle}

+

{t.transfersIntro}

+ +
+
+

+ {t.googleTransferTitle} +

+

{t.googleTransferText}

+
+ +
+

+ {t.githubTransferTitle} +

+

{t.githubTransferText}

+
+
+ +

{t.noOtherTransfers}

+
+ +
+

{t.rightsTitle}

+

{t.rightsIntro}

+ +
+
+

{t.accessTitle}

+

{t.accessText}

+
+ +
+

+ {t.rectificationTitle} +

+

{t.rectificationText}

+
+ +
+

{t.erasureTitle}

+

{t.erasureText}

+
+ +
+

+ {t.restrictionTitle} +

+

{t.restrictionText}

+
+ +
+

+ {t.objectionTitle} +

+

{t.objectionText}

+
+ +
+

+ {t.portabilityTitle} +

+

{t.portabilityText}

+
+ +
+

+ {t.withdrawConsentTitle} +

+

{t.withdrawConsentText}

+
+ +
+

+ {t.complaintTitle} +

+

{t.complaintText}

+
+
+ +

{t.exerciseRights}

+
+ +
+

+ {t.automatedDecisionTitle} +

+

{t.automatedDecisionText}

+
+ +
+

{t.changesTitle}

+

{t.changesText}

+
+ +
+

{t.contactInfoTitle}

+

{t.contactInfoText}

+
+ + +
+

{t.policyNote}

+

+ {t.lastUpdated}{" "} + {new Date().toLocaleDateString( + lang === "de" ? "de-DE" : "en-US", + { + year: "numeric", + month: "long", + day: "numeric", + }, + )} +

+
+
+
+
+
+
+ ); +}