From c18c880e0b0be80895c52d8386652632d25c3ac7 Mon Sep 17 00:00:00 2001 From: RBN-Apps <80348653+RBN-Apps@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:58:29 +0200 Subject: [PATCH 1/7] Add Imprint and Privacy Policy pages for EU compliance and integrate legal info management --- src/App.tsx | 4 + src/components/footer.tsx | 36 +++ src/components/portfolioEditor/index.tsx | 24 ++ .../portfolioEditor/legal-info-form.tsx | 152 ++++++++++ src/config/site.ts | 12 + src/layouts/default.tsx | 4 +- src/lib/use-portfolio-editor.ts | 13 + src/pages/imprint.tsx | 167 +++++++++++ src/pages/privacy.tsx | 263 ++++++++++++++++++ 9 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 src/components/footer.tsx create mode 100644 src/components/portfolioEditor/legal-info-form.tsx create mode 100644 src/pages/imprint.tsx create mode 100644 src/pages/privacy.tsx 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 ( + + ); +}; 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/config/site.ts b/src/config/site.ts index 7a3cccf9..191ee878 100644 --- a/src/config/site.ts +++ b/src/config/site.ts @@ -61,6 +61,18 @@ export const siteConfig = { enableContributorStatus: false, showGoldenBoxShadow: false, }, + legal: { + fullName: "", + streetAddress: "", + zipCode: "", + city: "", + country: "", + phone: "", + email: "", + vatId: "", + responsiblePerson: "", + responsibleAddress: "", + }, }, navItems: [ { diff --git a/src/layouts/default.tsx b/src/layouts/default.tsx index f059a6d5..05bd3f59 100644 --- a/src/layouts/default.tsx +++ b/src/layouts/default.tsx @@ -1,4 +1,5 @@ import { Navbar } from "@/components/navbar"; +import { Footer } from "@/components/footer"; /** * Default layout component with navbar and main content area @@ -12,11 +13,12 @@ export default function DefaultLayout({ children: React.ReactNode; }) { return ( -
+
{children}
+
); } diff --git a/src/lib/use-portfolio-editor.ts b/src/lib/use-portfolio-editor.ts index 04a4c458..6a5d81a8 100644 --- a/src/lib/use-portfolio-editor.ts +++ b/src/lib/use-portfolio-editor.ts @@ -10,6 +10,7 @@ import { saveDraftToCookies, } from "@/lib/cookie-persistence.ts"; import { Education, Experience, Skill, SkillLevel } from "@/types"; +import { LegalInfo } from "@/components/portfolioEditor/legal-info-form"; /** * Custom hook for managing portfolio editor state and operations @@ -557,6 +558,16 @@ export function usePortfolioEditor() { }); }; + const handleLegalInfoChange = (legalInfo: LegalInfo) => { + setPortfolioData((prev: any) => { + if (!prev) return prev; + return { + ...prev, + legal: legalInfo, + }; + }); + }; + const handleImportPortfolioData = (data: any) => { setPortfolioData(data); clearDraftFromCookies(); @@ -621,6 +632,8 @@ export function usePortfolioEditor() { handleEducationDragEnd, // Contributor functions handleContributorChange, + // Legal info functions + handleLegalInfoChange, // Import/Export handleImportPortfolioData, }; diff --git a/src/pages/imprint.tsx b/src/pages/imprint.tsx new file mode 100644 index 00000000..4d84f6d3 --- /dev/null +++ b/src/pages/imprint.tsx @@ -0,0 +1,167 @@ +import DefaultLayout from "@/layouts/default"; +import { usePortfolioData } from "@/hooks/usePortfolioData"; + +/** + * Imprint page component for EU legal compliance + * @returns Imprint page component + */ +export default function ImprintPage() { + const { portfolioData, isLoading } = usePortfolioData(); + + if (isLoading) { + return ( + +
+
+

Imprint / Impressum

+

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]"; + const displayVatId = legal.vatId || "[Your VAT ID - if applicable]"; + const displayResponsiblePerson = + legal.responsiblePerson || legal.fullName || "[Your Full Name]"; + const displayResponsibleAddress = + legal.responsibleAddress || + (legal.streetAddress && legal.city + ? `${legal.streetAddress}, ${legal.zipCode ? legal.zipCode + " " : ""}${legal.city}` + : "[Your Address]"); + + return ( + +
+
+

Imprint / Impressum

+ + {!hasRequiredFields && ( +
+
+
+ ! +
+
+

+ Template Notice +

+

+ Please replace the placeholder information below with your + actual contact details before using this website publicly. + Configure this in the editor under "Legal Info". +

+
+
+
+ )} + +
+

Information according to § 5 TMG

+ +

Contact Information

+

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

+ +

Contact

+

+ {legal.phone && ( + <> + Phone: {displayPhone} +
+ + )} + Email: {displayEmail} +

+ + {(legal.vatId || !hasRequiredFields) && ( + <> +

VAT ID

+

+ Sales tax identification number according to § 27a of the + Sales Tax Law: +
+ {displayVatId} +

+ + )} + +

Responsible for the content according to § 55 Abs. 2 RStV

+

+ {displayResponsiblePerson} +
+ {displayResponsibleAddress} +

+ +

Disclaimer

+ +

Accountability for content

+

+ The contents of our pages have been created with the utmost care. + However, we cannot guarantee the contents' accuracy, + completeness or topicality. According to statutory provisions, we + are furthermore responsible for our own content on these web + pages. In this context, please note that we are accordingly not + under obligation to monitor merely the transmitted or saved + information of third parties, or investigate circumstances + pointing to illegal activity. +

+ +

Accountability for links

+

+ Responsibility for the content of external links (to web pages of + third parties) lies solely with the operators of the linked pages. + No violations were evident to us at the time of linking. Should + any legal infringement become known to us, we will remove the + respective link immediately. +

+ +

Copyright

+

+ Our web pages and their contents are subject to German copyright + law. Unless expressly permitted by law (§ 44a et seq. of the + copyright law), every form of utilizing, reproducing or processing + works subject to copyright protection on our web pages requires + the prior consent of the respective owner of the rights. +

+ +
+

+ Last updated:{" "} + {new Date().toLocaleDateString("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..71837cd1 --- /dev/null +++ b/src/pages/privacy.tsx @@ -0,0 +1,263 @@ +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(); + + 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 ( + +
+
+

+ Privacy Policy / Datenschutzerklärung +

+ + {!hasRequiredFields && ( +
+
+
+ ! +
+
+

+ Template Notice +

+

+ 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". +

+
+
+
+ )} + +
+

1. Data Protection at a Glance

+ +

General Information

+

+ The following information provides a simple overview of what + happens to your personal data when you visit this website. + Personal data is any data that can personally identify you. For + detailed information on data protection, please refer to our + privacy policy listed below this text. +

+ +

Who is responsible for data collection on this website?

+

+ Data processing on this website is carried out by the website + operator. You can find their contact details in the + "Responsible Party" section of this privacy policy. +

+ +

How do we collect your data?

+

+ Your data is collected when you provide it to us. This could, for + example, be data you enter on a contact form. +

+

+ Other data is collected automatically by our IT systems when you + visit the website. This is mainly technical data (such as internet + browser, operating system, or time of the page call). The + collection of this data takes place automatically as soon as you + enter our website. +

+ +

2. Responsible Party for Data Processing

+

The responsible party for data processing on this website is:

+

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

+

+ {legal.phone && ( + <> + Phone: {displayPhone} +
+ + )} + Email: {displayEmail} +

+ +

3. Your Rights under the GDPR

+

You have the following rights regarding your personal data:

+
    +
  • + Right to information - You have the right to + receive information about your data stored with us and its + processing (Art. 15 GDPR). +
  • +
  • + Right to correction - You have the right to + correction of incorrect personal data concerning you (Art. 16 + GDPR). +
  • +
  • + Right to deletion - You have the right to + deletion of your data stored with us, unless we are legally + obliged to store it (Art. 17 GDPR). +
  • +
  • + Right to restrict processing - You have the + right to request the restriction of processing of your personal + data (Art. 18 GDPR). +
  • +
  • + Right to data portability - You have the right + to receive your data in a structured, commonly used and + machine-readable format (Art. 20 GDPR). +
  • +
  • + Right to object - You have the right to object + to the processing of your personal data (Art. 21 GDPR). +
  • +
+ +

4. Data Collection on This Website

+ +

Cookies

+

+ Our internet pages use so-called "cookies". Cookies are + small text files and do not cause any damage to your device. They + are stored either temporarily for the duration of a session + (session cookies) or permanently (permanent cookies) on your + device. Session cookies are automatically deleted after your + visit. Permanent cookies remain stored on your device until you + delete them yourself or until they are automatically deleted by + your web browser. +

+ +

Server Log Files

+

+ The website provider automatically collects and stores information + in so-called server log files, which your browser transmits to us + automatically. These are: +

+
    +
  • Browser type and browser version
  • +
  • Operating system used
  • +
  • Referrer URL
  • +
  • Host name of the accessing computer
  • +
  • Time of the server request
  • +
  • IP address
  • +
+

+ This data is not combined with other data sources. The collection + of this data is based on Art. 6 (1) lit. f GDPR. The website + operator has a legitimate interest in the technically error-free + presentation and optimization of his website. +

+ +

Contact Forms

+

+ If you send us inquiries via contact forms, your details from the + inquiry form, including the contact details you provided there, + will be stored by us for the purpose of processing the inquiry and + in the event of follow-up questions. We do not pass on this data + without your consent. +

+ +

5. External Services and Hosting

+ +

External Hosting

+

+ This website is hosted by a third-party service provider (hoster). + The personal data collected on this website is stored on the + hoster's servers. This may include IP addresses, contact + requests, meta and communication data, contract data, contact + details, names, website accesses, and other data generated via a + website. +

+

+ The use of the hoster is for the purpose of fulfilling the + contract with our potential and existing customers (Art. 6 para. 1 + lit. b GDPR) and in the interest of secure, fast, and efficient + provision of our online service by a professional provider (Art. 6 + para. 1 lit. f GDPR). +

+ +

GitHub Integration

+

+ This website may integrate with GitHub to display repository + information. When you view pages that display GitHub data, your + browser may connect directly to GitHub's servers. Please + refer to GitHub's privacy policy at + https://docs.github.com/en/github/site-policy/github-privacy-statement + for information about their data processing practices. +

+ +

6. Data Storage Duration

+

+ Unless a more specific storage period has been specified in this + privacy policy, your personal data will remain with us until the + purpose for which it was collected no longer applies. If you + assert a justified request for deletion or revoke your consent to + data processing, your data will be deleted unless we have other + legally permissible reasons for storing your personal data (e.g., + tax or commercial law retention periods); in the latter case, the + deletion takes place after these reasons cease to apply. +

+ +
+

+ This privacy policy was created with consideration for the EU + General Data Protection Regulation (GDPR). +

+

+ Last updated:{" "} + {new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })} +

+
+
+
+
+
+ ); +} From 46d6fc766021f3a4de6eeb4c519d64e85a8f569c Mon Sep 17 00:00:00 2001 From: RBN-Apps <80348653+RBN-Apps@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:52:01 +0200 Subject: [PATCH 2/7] Refactor Imprint and Privacy Policy pages to enhance layout and user experience with card components and alerts --- src/pages/imprint.tsx | 227 ++++++++++++--------- src/pages/privacy.tsx | 462 ++++++++++++++++++++++++------------------ 2 files changed, 393 insertions(+), 296 deletions(-) diff --git a/src/pages/imprint.tsx b/src/pages/imprint.tsx index 4d84f6d3..2b00f546 100644 --- a/src/pages/imprint.tsx +++ b/src/pages/imprint.tsx @@ -1,3 +1,7 @@ +import { Alert } from "@heroui/alert"; +import { Card, CardBody, CardHeader } from "@heroui/card"; +import { Divider } from "@heroui/divider"; + import DefaultLayout from "@/layouts/default"; import { usePortfolioData } from "@/hooks/usePortfolioData"; @@ -13,8 +17,15 @@ export default function ImprintPage() {
-

Imprint / Impressum

-

Loading...

+ + +

Imprint / Impressum

+
+ + +

Loading…

+
+
@@ -51,106 +62,126 @@ export default function ImprintPage() {
-

Imprint / Impressum

- {!hasRequiredFields && ( -
-
-
- ! +
+ +
+ )} + + + +

Imprint / Impressum

+
+ + +
+

+ Information according to § 5 TMG +

+ +
+

Contact Information

+

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

-
-

- Template Notice + +
+

Contact

+

+ {legal.phone && ( + <> + Phone: {displayPhone} +
+ + )} + Email: {displayEmail} +

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

VAT ID

+

+ Sales tax identification number according to § 27a of the + Sales Tax Law: +
+ {displayVatId} +

+
+ )} + +
+

+ Responsible for the content according to § 55 Abs. 2 RStV

-

- Please replace the placeholder information below with your - actual contact details before using this website publicly. - Configure this in the editor under "Legal Info". +

+ {displayResponsiblePerson} +
+ {displayResponsibleAddress}

-

-
- )} +
+ +
+

Disclaimer

-
-

Information according to § 5 TMG

- -

Contact Information

-

- {displayName} -
- {displayStreetAddress} -
- {displayZipCity} -
- {displayCountry} -

- -

Contact

-

- {legal.phone && ( - <> - Phone: {displayPhone} -
- - )} - Email: {displayEmail} -

- - {(legal.vatId || !hasRequiredFields) && ( - <> -

VAT ID

-

- Sales tax identification number according to § 27a of the - Sales Tax Law: -
- {displayVatId} -

- - )} - -

Responsible for the content according to § 55 Abs. 2 RStV

-

- {displayResponsiblePerson} -
- {displayResponsibleAddress} -

- -

Disclaimer

- -

Accountability for content

-

- The contents of our pages have been created with the utmost care. - However, we cannot guarantee the contents' accuracy, - completeness or topicality. According to statutory provisions, we - are furthermore responsible for our own content on these web - pages. In this context, please note that we are accordingly not - under obligation to monitor merely the transmitted or saved - information of third parties, or investigate circumstances - pointing to illegal activity. -

- -

Accountability for links

-

- Responsibility for the content of external links (to web pages of - third parties) lies solely with the operators of the linked pages. - No violations were evident to us at the time of linking. Should - any legal infringement become known to us, we will remove the - respective link immediately. -

- -

Copyright

-

- Our web pages and their contents are subject to German copyright - law. Unless expressly permitted by law (§ 44a et seq. of the - copyright law), every form of utilizing, reproducing or processing - works subject to copyright protection on our web pages requires - the prior consent of the respective owner of the rights. -

- -
-

+

+

+ Accountability for content +

+

+ The contents of our pages have been created with the utmost + care. However, we cannot guarantee the contents' + accuracy, completeness or topicality. According to statutory + provisions, we are furthermore responsible for our own + content on these web pages. In this context, please note + that we are accordingly not under obligation to monitor + merely the transmitted or saved information of third + parties, or investigate circumstances pointing to illegal + activity. +

+
+ +
+

+ Accountability for links +

+

+ Responsibility for the content of external links (to web + pages of third parties) lies solely with the operators of + the linked pages. No violations were evident to us at the + time of linking. Should any legal infringement become known + to us, we will remove the respective link immediately. +

+
+ +
+

Copyright

+

+ Our web pages and their contents are subject to German + copyright law. Unless expressly permitted by law (§ 44a et + seq. of the copyright law), every form of utilizing, + reproducing or processing works subject to copyright + protection on our web pages requires the prior consent of + the respective owner of the rights. +

+
+
+ + +

Last updated:{" "} {new Date().toLocaleDateString("en-US", { year: "numeric", @@ -158,8 +189,8 @@ export default function ImprintPage() { day: "numeric", })}

-
- + + diff --git a/src/pages/privacy.tsx b/src/pages/privacy.tsx index 71837cd1..19d0593b 100644 --- a/src/pages/privacy.tsx +++ b/src/pages/privacy.tsx @@ -1,3 +1,8 @@ +import { Alert } from "@heroui/alert"; +import { Card, CardBody, CardHeader } from "@heroui/card"; +import { Divider } from "@heroui/divider"; +import { Link } from "@heroui/link"; + import DefaultLayout from "@/layouts/default"; import { usePortfolioData } from "@/hooks/usePortfolioData"; @@ -12,11 +17,18 @@ export default function PrivacyPage() { return (
-
-

- Privacy Policy / Datenschutzerklärung -

-

Loading...

+
+ + +

+ Privacy Policy / Datenschutzerklärung +

+
+ + +

Loading…

+
+
@@ -44,218 +56,272 @@ export default function PrivacyPage() {
-

- Privacy Policy / Datenschutzerklärung -

- {!hasRequiredFields && ( -
-
-
- ! -
-
-

- Template Notice -

-

- 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". -

-
-
+
+
)} -
-

1. Data Protection at a Glance

+ + +

+ Privacy Policy / Datenschutzerklärung +

+
+ + +
+

+ 1. Data Protection at a Glance +

-

General Information

-

- The following information provides a simple overview of what - happens to your personal data when you visit this website. - Personal data is any data that can personally identify you. For - detailed information on data protection, please refer to our - privacy policy listed below this text. -

+
+

General Information

+

+ The following information provides a simple overview of what + happens to your personal data when you visit this website. + Personal data is any data that can personally identify you. + For detailed information on data protection, please refer to + our privacy policy listed below this text. +

+
-

Who is responsible for data collection on this website?

-

- Data processing on this website is carried out by the website - operator. You can find their contact details in the - "Responsible Party" section of this privacy policy. -

+
+

+ Who is responsible for data collection on this website? +

+

+ Data processing on this website is carried out by the + website operator. You can find their contact details in the + "Responsible Party" section of this privacy + policy. +

+
-

How do we collect your data?

-

- Your data is collected when you provide it to us. This could, for - example, be data you enter on a contact form. -

-

- Other data is collected automatically by our IT systems when you - visit the website. This is mainly technical data (such as internet - browser, operating system, or time of the page call). The - collection of this data takes place automatically as soon as you - enter our website. -

+
+

+ How do we collect your data? +

+

+ Your data is collected when you provide it to us. This + could, for example, be data you enter on a contact form. +

+

+ Other data is collected automatically by our IT systems when + you visit the website. This is mainly technical data (such + as internet browser, operating system, or time of the page + call). The collection of this data takes place automatically + as soon as you enter our website. +

+
+
-

2. Responsible Party for Data Processing

-

The responsible party for data processing on this website is:

-

- {displayName} -
- {displayStreetAddress} -
- {displayZipCity} -
- {displayCountry} -

-

- {legal.phone && ( - <> - Phone: {displayPhone} +

+

+ 2. Responsible Party for Data Processing +

+

+ The responsible party for data processing on this website is: +

+

+ {displayName} +
+ {displayStreetAddress}
- - )} - Email: {displayEmail} -

+ {displayZipCity} +
+ {displayCountry} +

+

+ {legal.phone && ( + <> + Phone: {displayPhone} +
+ + )} + Email: {displayEmail} +

+
-

3. Your Rights under the GDPR

-

You have the following rights regarding your personal data:

-
    -
  • - Right to information - You have the right to - receive information about your data stored with us and its - processing (Art. 15 GDPR). -
  • -
  • - Right to correction - You have the right to - correction of incorrect personal data concerning you (Art. 16 - GDPR). -
  • -
  • - Right to deletion - You have the right to - deletion of your data stored with us, unless we are legally - obliged to store it (Art. 17 GDPR). -
  • -
  • - Right to restrict processing - You have the - right to request the restriction of processing of your personal - data (Art. 18 GDPR). -
  • -
  • - Right to data portability - You have the right - to receive your data in a structured, commonly used and - machine-readable format (Art. 20 GDPR). -
  • -
  • - Right to object - You have the right to object - to the processing of your personal data (Art. 21 GDPR). -
  • -
+
+

+ 3. Your Rights under the GDPR +

+

+ You have the following rights regarding your personal data: +

+
    +
  • + Right to information - You have the right + to receive information about your data stored with us and + its processing (Art. 15 GDPR). +
  • +
  • + Right to correction - You have the right to + correction of incorrect personal data concerning you (Art. + 16 GDPR). +
  • +
  • + Right to deletion - You have the right to + deletion of your data stored with us, unless we are legally + obliged to store it (Art. 17 GDPR). +
  • +
  • + Right to restrict processing - You have the + right to request the restriction of processing of your + personal data (Art. 18 GDPR). +
  • +
  • + Right to data portability - You have the + right to receive your data in a structured, commonly used + and machine-readable format (Art. 20 GDPR). +
  • +
  • + Right to object - You have the right to + object to the processing of your personal data (Art. 21 + GDPR). +
  • +
+
-

4. Data Collection on This Website

+
+

+ 4. Data Collection on This Website +

-

Cookies

-

- Our internet pages use so-called "cookies". Cookies are - small text files and do not cause any damage to your device. They - are stored either temporarily for the duration of a session - (session cookies) or permanently (permanent cookies) on your - device. Session cookies are automatically deleted after your - visit. Permanent cookies remain stored on your device until you - delete them yourself or until they are automatically deleted by - your web browser. -

+
+

Cookies

+

+ Our internet pages use so-called "cookies". + Cookies are small text files and do not cause any damage to + your device. They are stored either temporarily for the + duration of a session (session cookies) or permanently + (permanent cookies) on your device. Session cookies are + automatically deleted after your visit. Permanent cookies + remain stored on your device until you delete them yourself + or until they are automatically deleted by your web browser. +

+
-

Server Log Files

-

- The website provider automatically collects and stores information - in so-called server log files, which your browser transmits to us - automatically. These are: -

-
    -
  • Browser type and browser version
  • -
  • Operating system used
  • -
  • Referrer URL
  • -
  • Host name of the accessing computer
  • -
  • Time of the server request
  • -
  • IP address
  • -
-

- This data is not combined with other data sources. The collection - of this data is based on Art. 6 (1) lit. f GDPR. The website - operator has a legitimate interest in the technically error-free - presentation and optimization of his website. -

+
+

Server Log Files

+

+ The website provider automatically collects and stores + information in so-called server log files, which your + browser transmits to us automatically. These are: +

+
    +
  • Browser type and browser version
  • +
  • Operating system used
  • +
  • Referrer URL
  • +
  • Host name of the accessing computer
  • +
  • Time of the server request
  • +
  • IP address
  • +
+

+ This data is not combined with other data sources. The + collection of this data is based on Art. 6 (1) lit. f GDPR. + The website operator has a legitimate interest in the + technically error-free presentation and optimization of his + website. +

+
-

Contact Forms

-

- If you send us inquiries via contact forms, your details from the - inquiry form, including the contact details you provided there, - will be stored by us for the purpose of processing the inquiry and - in the event of follow-up questions. We do not pass on this data - without your consent. -

+
+

Contact Forms

+

+ If you send us inquiries via contact forms, your details + from the inquiry form, including the contact details you + provided there, will be stored by us for the purpose of + processing the inquiry and in the event of follow-up + questions. We do not pass on this data without your consent. +

+
+
-

5. External Services and Hosting

+
+

+ 5. External Services and Hosting +

-

External Hosting

-

- This website is hosted by a third-party service provider (hoster). - The personal data collected on this website is stored on the - hoster's servers. This may include IP addresses, contact - requests, meta and communication data, contract data, contact - details, names, website accesses, and other data generated via a - website. -

-

- The use of the hoster is for the purpose of fulfilling the - contract with our potential and existing customers (Art. 6 para. 1 - lit. b GDPR) and in the interest of secure, fast, and efficient - provision of our online service by a professional provider (Art. 6 - para. 1 lit. f GDPR). -

+
+

External Hosting

+

+ This website is hosted by a third-party service provider + (hoster). The personal data collected on this website is + stored on the hoster's servers. This may include IP + addresses, contact requests, meta and communication data, + contract data, contact details, names, website accesses, and + other data generated via a website. +

+

+ The use of the hoster is for the purpose of fulfilling the + contract with our potential and existing customers (Art. 6 + para. 1 lit. b GDPR) and in the interest of secure, fast, + and efficient provision of our online service by a + professional provider (Art. 6 para. 1 lit. f GDPR). +

+
-

GitHub Integration

-

- This website may integrate with GitHub to display repository - information. When you view pages that display GitHub data, your - browser may connect directly to GitHub's servers. Please - refer to GitHub's privacy policy at - https://docs.github.com/en/github/site-policy/github-privacy-statement - for information about their data processing practices. -

+
+

GitHub Integration

+

+ This website may integrate with GitHub to display repository + information. When you view pages that display GitHub data, + your browser may connect directly to GitHub's servers. + Please refer to + + + GitHub's Privacy Statement + + + for information about their data processing practices. +

+
+
-

6. Data Storage Duration

-

- Unless a more specific storage period has been specified in this - privacy policy, your personal data will remain with us until the - purpose for which it was collected no longer applies. If you - assert a justified request for deletion or revoke your consent to - data processing, your data will be deleted unless we have other - legally permissible reasons for storing your personal data (e.g., - tax or commercial law retention periods); in the latter case, the - deletion takes place after these reasons cease to apply. -

+
+

+ 6. Data Storage Duration +

+

+ Unless a more specific storage period has been specified in + this privacy policy, your personal data will remain with us + until the purpose for which it was collected no longer + applies. If you assert a justified request for deletion or + revoke your consent to data processing, your data will be + deleted unless we have other legally permissible reasons for + storing your personal data (e.g., tax or commercial law + retention periods); in the latter case, the deletion takes + place after these reasons cease to apply. +

+
-
-

- This privacy policy was created with consideration for the EU - General Data Protection Regulation (GDPR). -

-

- Last updated:{" "} - {new Date().toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - })} -

-
-
+ +
+

+ This privacy policy was created with consideration for the EU + General Data Protection Regulation (GDPR). +

+

+ Last updated:{" "} + {new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })} +

+
+ +
From d9a40ed922ad8e3894967d75ea1787de35a7acd5 Mon Sep 17 00:00:00 2001 From: RBN-Apps <80348653+RBN-Apps@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:04:58 +0200 Subject: [PATCH 3/7] Add language selection and localization to Imprint and Privacy Policy pages --- src/pages/imprint.tsx | 215 +++++++++++++++------- src/pages/privacy.tsx | 413 +++++++++++++++++++++++++----------------- 2 files changed, 394 insertions(+), 234 deletions(-) diff --git a/src/pages/imprint.tsx b/src/pages/imprint.tsx index 2b00f546..072d7cb2 100644 --- a/src/pages/imprint.tsx +++ b/src/pages/imprint.tsx @@ -1,6 +1,8 @@ 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"; @@ -12,26 +14,6 @@ import { usePortfolioData } from "@/hooks/usePortfolioData"; export default function ImprintPage() { const { portfolioData, isLoading } = usePortfolioData(); - if (isLoading) { - return ( - -
-
- - -

Imprint / Impressum

-
- - -

Loading…

-
-
-
-
-
- ); - } - const legal = portfolioData?.legal || {}; const hasRequiredFields = legal.fullName && @@ -58,6 +40,107 @@ export default function ImprintPage() { ? `${legal.streetAddress}, ${legal.zipCode ? legal.zipCode + " " : ""}${legal.city}` : "[Your Address]"); + 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: "Impressum", + noticeTitle: "Hinweis Vorlage", + noticeDesc: + 'Bitte ersetzen Sie die Platzhalter unten durch Ihre tatsächlichen Kontaktdaten, bevor Sie diese Website öffentlich verwenden. Konfigurieren Sie dies im Editor unter "Legal Info".', + tmgTitle: "Angaben gemäß § 5 TMG", + contactInfo: "Anschrift", + contact: "Kontakt", + phone: "Telefon:", + email: "E-Mail:", + vatTitle: "USt-IdNr.", + vatLine: "Umsatzsteuer-Identifikationsnummer gemäß § 27a UStG:", + responsibleTitle: "Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV", + disclaimerTitle: "Haftungsausschluss", + contentAccountabilityTitle: "Haftung für Inhalte", + contentAccountabilityText: + "Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Nach den gesetzlichen Bestimmungen sind wir für eigene Inhalte auf diesen Seiten verantwortlich. In diesem Zusammenhang weisen wir darauf hin, dass wir nicht verpflichtet sind, übermittelte oder gespeicherte fremde Informationen zu überwachen oder Umstände zu erforschen, die auf eine rechtswidrige Tätigkeit hinweisen.", + linkAccountabilityTitle: "Haftung für Links", + linkAccountabilityText: + "Für Inhalte externer Links (zu Webseiten Dritter) ist ausschließlich der jeweilige Betreiber verantwortlich. Zum Zeitpunkt der Verlinkung waren Rechtsverstöße für uns nicht erkennbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.", + copyrightTitle: "Urheberrecht", + copyrightText: + "Diese Webseiten und deren Inhalte unterliegen dem deutschen Urheberrecht. Soweit nicht gesetzlich ausdrücklich erlaubt (§§ 44a ff. UrhG), bedarf jede Verwertung, Vervielfältigung oder Verarbeitung urheberrechtlich geschützter Werke auf unseren Seiten der vorherigen Zustimmung des jeweiligen Rechteinhabers.", + lastUpdated: "Zuletzt aktualisiert:", + } as const; + } + return { + displayTitle: "Imprint / Impressum", + noticeTitle: "Template Notice", + noticeDesc: + 'Please replace the placeholder information below with your actual contact details before using this website publicly. Configure this in the editor under "Legal Info".', + tmgTitle: "Information according to § 5 TMG", + contactInfo: "Contact Information", + contact: "Contact", + phone: "Phone:", + email: "Email:", + vatTitle: "VAT ID", + vatLine: + "Sales tax identification number according to § 27a of the Sales Tax Law:", + responsibleTitle: + "Responsible for the content according to § 55 Abs. 2 RStV", + disclaimerTitle: "Disclaimer", + contentAccountabilityTitle: "Accountability for content", + contentAccountabilityText: + "The contents of our pages have been created with the utmost care. However, we cannot guarantee the contents' accuracy, completeness or topicality. According to statutory provisions, we are furthermore responsible for our own content on these web pages. In this context, please note that we are accordingly not under obligation to monitor merely the transmitted or saved information of third parties, or investigate circumstances pointing to illegal activity.", + linkAccountabilityTitle: "Accountability for links", + linkAccountabilityText: + "Responsibility for the content of external links (to web pages of third parties) lies solely with the operators of the linked pages. No violations were evident to us at the time of linking. Should any legal infringement become known to us, we will remove the respective link immediately.", + copyrightTitle: "Copyright", + copyrightText: + "Our web pages and their contents are subject to German copyright law. Unless expressly permitted by law (§ 44a et seq. of the copyright law), every form of utilizing, reproducing or processing works subject to copyright protection on our web pages requires the prior consent of the respective owner of the rights.", + lastUpdated: "Last updated:", + } as const; + }, [lang]); + + // Ensure hooks run before any early return + if (isLoading) { + return ( + +
+
+ + +

{t.displayTitle}

+
+ + +

Loading…

+
+
+
+
+
+ ); + } + return (
@@ -66,8 +149,8 @@ export default function ImprintPage() {
@@ -75,17 +158,30 @@ export default function ImprintPage() { -

Imprint / Impressum

+
+

{t.displayTitle}

+ +
-

- Information according to § 5 TMG -

+

{t.tmgTitle}

-

Contact Information

+

{t.contactInfo}

{displayName}
@@ -98,24 +194,23 @@ export default function ImprintPage() {

-

Contact

+

{t.contact}

{legal.phone && ( <> - Phone: {displayPhone} + {t.phone} {displayPhone}
)} - Email: {displayEmail} + {t.email} {displayEmail}

{(legal.vatId || !hasRequiredFields) && (
-

VAT ID

+

{t.vatTitle}

- Sales tax identification number according to § 27a of the - Sales Tax Law: + {t.vatLine}
{displayVatId}

@@ -124,7 +219,7 @@ export default function ImprintPage() {

- Responsible for the content according to § 55 Abs. 2 RStV + {t.responsibleTitle}

{displayResponsiblePerson} @@ -135,59 +230,39 @@ export default function ImprintPage() {

-

Disclaimer

+

{t.disclaimerTitle}

- Accountability for content + {t.contentAccountabilityTitle}

-

- The contents of our pages have been created with the utmost - care. However, we cannot guarantee the contents' - accuracy, completeness or topicality. According to statutory - provisions, we are furthermore responsible for our own - content on these web pages. In this context, please note - that we are accordingly not under obligation to monitor - merely the transmitted or saved information of third - parties, or investigate circumstances pointing to illegal - activity. -

+

{t.contentAccountabilityText}

- Accountability for links + {t.linkAccountabilityTitle}

-

- Responsibility for the content of external links (to web - pages of third parties) lies solely with the operators of - the linked pages. No violations were evident to us at the - time of linking. Should any legal infringement become known - to us, we will remove the respective link immediately. -

+

{t.linkAccountabilityText}

-

Copyright

-

- Our web pages and their contents are subject to German - copyright law. Unless expressly permitted by law (§ 44a et - seq. of the copyright law), every form of utilizing, - reproducing or processing works subject to copyright - protection on our web pages requires the prior consent of - the respective owner of the rights. -

+

{t.copyrightTitle}

+

{t.copyrightText}

- Last updated:{" "} - {new Date().toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - })} + {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 index 19d0593b..22a61a76 100644 --- a/src/pages/privacy.tsx +++ b/src/pages/privacy.tsx @@ -2,6 +2,8 @@ import { Alert } from "@heroui/alert"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Divider } from "@heroui/divider"; import { Link } from "@heroui/link"; +import { Select, SelectItem } from "@heroui/select"; +import { useEffect, useMemo, useState } from "react"; import DefaultLayout from "@/layouts/default"; import { usePortfolioData } from "@/hooks/usePortfolioData"; @@ -12,6 +14,179 @@ import { usePortfolioData } from "@/hooks/usePortfolioData"; */ 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".', + dpGlanceTitle: "1. Datenschutz auf einen Blick", + generalInfoTitle: "Allgemeine Hinweise", + generalInfoText: + "Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer nachstehenden Datenschutzerklärung.", + responsibleQuestionTitle: + "Wer ist verantwortlich für die Datenerfassung auf dieser Website?", + responsibleQuestionText: + 'Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten entnehmen Sie dem Abschnitt "Verantwortliche Stelle" dieser Datenschutzerklärung.', + collectHowTitle: "Wie erfassen wir Ihre Daten?", + collectHowText1: + "Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein Kontaktformular eingeben.", + collectHowText2: + "Andere Daten werden automatisch beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie unsere Website betreten.", + responsibleSectionTitle: + "2. Verantwortliche Stelle für die Datenverarbeitung", + responsibleIntro: + "Verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:", + phone: "Telefon:", + email: "E-Mail:", + rightsTitle: "3. Ihre Rechte gemäß DSGVO", + rightsIntro: + "Sie haben bezüglich Ihrer personenbezogenen Daten insbesondere folgende Rechte:", + rights: [ + "Auskunft (Art. 15 DSGVO) – Auskunft über die bei uns gespeicherten Daten und deren Verarbeitung", + "Berichtigung (Art. 16 DSGVO) – Berichtigung unrichtiger personenbezogener Daten", + "Löschung (Art. 17 DSGVO) – Löschung Ihrer bei uns gespeicherten Daten, sofern keine gesetzliche Aufbewahrungspflicht besteht", + "Einschränkung der Verarbeitung (Art. 18 DSGVO) – Einschränkung der Verarbeitung Ihrer personenbezogenen Daten", + "Datenübertragbarkeit (Art. 20 DSGVO) – Herausgabe Ihrer Daten in einem strukturierten, gängigen und maschinenlesbaren Format", + "Widerspruch (Art. 21 DSGVO) – Widerspruch gegen die Verarbeitung Ihrer personenbezogenen Daten", + ], + dataCollectionTitle: "4. Datenerfassung auf dieser Website", + cookiesTitle: "Cookies", + cookiesText: + 'Unsere Internetseiten verwenden sogenannte "Cookies". Cookies richten auf Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung (Session-Cookies) oder dauerhaft (persistente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies werden nach Ende Ihres Besuchs automatisch gelöscht. Persistente Cookies bleiben auf Ihrem Endgerät gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.', + serverLogsTitle: "Server-Log-Dateien", + serverLogsIntro: + "Der Provider der Seiten erhebt und speichert automatisch Informationen in sogenannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:", + serverLogItems: [ + "Browsertyp und Browserversion", + "verwendetes Betriebssystem", + "Referrer URL", + "Hostname des zugreifenden Rechners", + "Uhrzeit der Serveranfrage", + "IP-Adresse", + ], + serverLogsFoot: + "Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen. Die Erfassung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an der technisch fehlerfreien Darstellung und der Optimierung seiner Website.", + contactFormsTitle: "Kontaktformular", + contactFormsText: + "Wenn Sie uns per Kontaktformular Anfragen zukommen lassen, werden Ihre Angaben aus dem Anfrageformular inklusive der von Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.", + externalServicesTitle: "5. Externe Dienste und Hosting", + externalHostingTitle: "Externes Hosting", + externalHostingText1: + "Diese Website wird bei einem externen Dienstleister (Hoster) gehostet. Personenbezogene Daten, die auf dieser Website erfasst werden, werden auf den Servern des Hosters gespeichert. Hierbei kann es sich insbesondere um IP-Adressen, Kontaktanfragen, Meta- und Kommunikationsdaten, Vertragsdaten, Kontaktdaten, Namen, Websitezugriffe und sonstige Daten handeln, die über eine Website generiert werden.", + externalHostingText2: + "Der Einsatz des Hosters erfolgt zum Zwecke der Vertragserfüllung gegenüber unseren potenziellen und bestehenden Kunden (Art. 6 Abs. 1 lit. b DSGVO) und im Interesse einer sicheren, schnellen und effizienten Bereitstellung unseres Online-Angebots durch einen professionellen Anbieter (Art. 6 Abs. 1 lit. f DSGVO).", + githubIntegrationTitle: "GitHub-Integration", + githubIntegrationTextPrefix: + "Diese Website kann GitHub integrieren, um Repository-Informationen anzuzeigen. Beim Aufruf entsprechender Seiten kann Ihr Browser direkt eine Verbindung zu GitHub herstellen. Bitte beachten Sie", + githubLinkLabel: "GitHubs Datenschutzerklärung", + githubIntegrationTextSuffix: + "für Informationen zu deren Datenverarbeitung.", + storageDurationTitle: "6. Speicherdauer", + storageDurationText: + "Sofern in dieser Datenschutzerklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck der Datenverarbeitung entfällt. Wenn Sie ein berechtigtes Löschersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung widerrufen, werden Ihre Daten gelöscht, sofern keine anderen rechtlich zulässigen Gründe für die Speicherung Ihrer personenbezogenen Daten vorliegen (z. B. steuer- oder handelsrechtliche Aufbewahrungsfristen); in letzterem Fall erfolgt die Löschung nach Wegfall dieser Gründe.", + policyNote: + "Diese Datenschutzerklärung wurde unter Berücksichtigung der EU-Datenschutz-Grundverordnung (DSGVO) erstellt.", + lastUpdated: "Zuletzt aktualisiert:", + } as const; + } + return { + displayTitle: "Privacy Policy / Datenschutzerklärung", + 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".', + dpGlanceTitle: "1. Data Protection at a Glance", + generalInfoTitle: "General Information", + generalInfoText: + "The following information provides a simple overview of what happens to your personal data when you visit this website. Personal data is any data that can personally identify you. For detailed information on data protection, please refer to our privacy policy listed below this text.", + responsibleQuestionTitle: + "Who is responsible for data collection on this website?", + responsibleQuestionText: + 'Data processing on this website is carried out by the website operator. You can find their contact details in the "Responsible Party" section of this privacy policy.', + collectHowTitle: "How do we collect your data?", + collectHowText1: + "Your data is collected when you provide it to us. This could, for example, be data you enter on a contact form.", + collectHowText2: + "Other data is collected automatically by our IT systems when you visit the website. This is mainly technical data (such as internet browser, operating system, or time of the page call). The collection of this data takes place automatically as soon as you enter our website.", + responsibleSectionTitle: "2. Responsible Party for Data Processing", + responsibleIntro: + "The responsible party for data processing on this website is:", + phone: "Phone:", + email: "Email:", + rightsTitle: "3. Your Rights under the GDPR", + rightsIntro: + "You have the following rights regarding your personal data:", + rights: [ + "Right to information – You have the right to receive information about your data stored with us and its processing (Art. 15 GDPR).", + "Right to correction – You have the right to correction of incorrect personal data concerning you (Art. 16 GDPR).", + "Right to deletion – You have the right to deletion of your data stored with us, unless we are legally obliged to store it (Art. 17 GDPR).", + "Right to restrict processing – You have the right to request the restriction of processing of your personal data (Art. 18 GDPR).", + "Right to data portability – You have the right to receive your data in a structured, commonly used and machine-readable format (Art. 20 GDPR).", + "Right to object – You have the right to object to the processing of your personal data (Art. 21 GDPR).", + ], + dataCollectionTitle: "4. Data Collection on This Website", + cookiesTitle: "Cookies", + cookiesText: + 'Our internet pages use so-called "cookies". Cookies are small text files and do not cause any damage to your device. They are stored either temporarily for the duration of a session (session cookies) or permanently (permanent cookies) on your device. Session cookies are automatically deleted after your visit. Permanent cookies remain stored on your device until you delete them yourself or until they are automatically deleted by your web browser.', + serverLogsTitle: "Server Log Files", + serverLogsIntro: + "The website provider automatically collects and stores information in so-called server log files, which your browser transmits to us automatically. These are:", + serverLogItems: [ + "Browser type and browser version", + "Operating system used", + "Referrer URL", + "Host name of the accessing computer", + "Time of the server request", + "IP address", + ], + serverLogsFoot: + "This data is not combined with other data sources. The collection of this data is based on Art. 6 (1) lit. f GDPR. The website operator has a legitimate interest in the technically error-free presentation and optimization of his website.", + contactFormsTitle: "Contact Forms", + contactFormsText: + "If you send us inquiries via contact forms, your details from the inquiry form, including the contact details you provided there, will be stored by us for the purpose of processing the inquiry and in the event of follow-up questions. We do not pass on this data without your consent.", + externalServicesTitle: "5. External Services and Hosting", + externalHostingTitle: "External Hosting", + externalHostingText1: + "This website is hosted by a third-party service provider (hoster). The personal data collected on this website is stored on the hoster's servers. This may include IP addresses, contact requests, meta and communication data, contract data, contact details, names, website accesses, and other data generated via a website.", + externalHostingText2: + "The use of the hoster is for the purpose of fulfilling the contract with our potential and existing customers (Art. 6 para. 1 lit. b GDPR) and in the interest of secure, fast, and efficient provision of our online service by a professional provider (Art. 6 para. 1 lit. f GDPR).", + githubIntegrationTitle: "GitHub Integration", + githubIntegrationTextPrefix: + "This website may integrate with GitHub to display repository information. When you view pages that display GitHub data, your browser may connect directly to GitHub's servers. Please refer to", + githubLinkLabel: "GitHub's Privacy Statement", + githubIntegrationTextSuffix: + "for information about their data processing practices.", + storageDurationTitle: "6. Data Storage Duration", + storageDurationText: + "Unless a more specific storage period has been specified in this privacy policy, your personal data will remain with us until the purpose for which it was collected no longer applies. If you assert a justified request for deletion or revoke your consent to data processing, your data will be deleted unless we have other legally permissible reasons for storing your personal data (e.g., tax or commercial law retention periods); in the latter case, the deletion takes place after these reasons cease to apply.", + 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 ( @@ -60,8 +235,8 @@ export default function PrivacyPage() {
@@ -69,65 +244,54 @@ export default function PrivacyPage() { -

- Privacy Policy / Datenschutzerklärung -

+
+

{t.displayTitle}

+ +
-

- 1. Data Protection at a Glance -

+

{t.dpGlanceTitle}

-

General Information

-

- The following information provides a simple overview of what - happens to your personal data when you visit this website. - Personal data is any data that can personally identify you. - For detailed information on data protection, please refer to - our privacy policy listed below this text. -

+

+ {t.generalInfoTitle} +

+

{t.generalInfoText}

- Who is responsible for data collection on this website? + {t.responsibleQuestionTitle}

-

- Data processing on this website is carried out by the - website operator. You can find their contact details in the - "Responsible Party" section of this privacy - policy. -

+

{t.responsibleQuestionText}

-

- How do we collect your data? -

-

- Your data is collected when you provide it to us. This - could, for example, be data you enter on a contact form. -

-

- Other data is collected automatically by our IT systems when - you visit the website. This is mainly technical data (such - as internet browser, operating system, or time of the page - call). The collection of this data takes place automatically - as soon as you enter our website. -

+

{t.collectHowTitle}

+

{t.collectHowText1}

+

{t.collectHowText2}

- 2. Responsible Party for Data Processing + {t.responsibleSectionTitle}

-

- The responsible party for data processing on this website is: -

+

{t.responsibleIntro}

{displayName}
@@ -140,184 +304,105 @@ export default function PrivacyPage() {

{legal.phone && ( <> - Phone: {displayPhone} + {t.phone} {displayPhone}
)} - Email: {displayEmail} + {t.email} {displayEmail}

-

- 3. Your Rights under the GDPR -

-

- You have the following rights regarding your personal data: -

+

{t.rightsTitle}

+

{t.rightsIntro}

    -
  • - Right to information - You have the right - to receive information about your data stored with us and - its processing (Art. 15 GDPR). -
  • -
  • - Right to correction - You have the right to - correction of incorrect personal data concerning you (Art. - 16 GDPR). -
  • -
  • - Right to deletion - You have the right to - deletion of your data stored with us, unless we are legally - obliged to store it (Art. 17 GDPR). -
  • -
  • - Right to restrict processing - You have the - right to request the restriction of processing of your - personal data (Art. 18 GDPR). -
  • -
  • - Right to data portability - You have the - right to receive your data in a structured, commonly used - and machine-readable format (Art. 20 GDPR). -
  • -
  • - Right to object - You have the right to - object to the processing of your personal data (Art. 21 - GDPR). -
  • + {t.rights.map((item, i) => ( +
  • {item}
  • + ))}

- 4. Data Collection on This Website + {t.dataCollectionTitle}

-

Cookies

-

- Our internet pages use so-called "cookies". - Cookies are small text files and do not cause any damage to - your device. They are stored either temporarily for the - duration of a session (session cookies) or permanently - (permanent cookies) on your device. Session cookies are - automatically deleted after your visit. Permanent cookies - remain stored on your device until you delete them yourself - or until they are automatically deleted by your web browser. -

+

{t.cookiesTitle}

+

{t.cookiesText}

-

Server Log Files

-

- The website provider automatically collects and stores - information in so-called server log files, which your - browser transmits to us automatically. These are: -

+

{t.serverLogsTitle}

+

{t.serverLogsIntro}

    -
  • Browser type and browser version
  • -
  • Operating system used
  • -
  • Referrer URL
  • -
  • Host name of the accessing computer
  • -
  • Time of the server request
  • -
  • IP address
  • + {t.serverLogItems.map((item, i) => ( +
  • {item}
  • + ))}
-

- This data is not combined with other data sources. The - collection of this data is based on Art. 6 (1) lit. f GDPR. - The website operator has a legitimate interest in the - technically error-free presentation and optimization of his - website. -

+

{t.serverLogsFoot}

-

Contact Forms

-

- If you send us inquiries via contact forms, your details - from the inquiry form, including the contact details you - provided there, will be stored by us for the purpose of - processing the inquiry and in the event of follow-up - questions. We do not pass on this data without your consent. -

+

+ {t.contactFormsTitle} +

+

{t.contactFormsText}

- 5. External Services and Hosting + {t.externalServicesTitle}

-

External Hosting

-

- This website is hosted by a third-party service provider - (hoster). The personal data collected on this website is - stored on the hoster's servers. This may include IP - addresses, contact requests, meta and communication data, - contract data, contact details, names, website accesses, and - other data generated via a website. -

-

- The use of the hoster is for the purpose of fulfilling the - contract with our potential and existing customers (Art. 6 - para. 1 lit. b GDPR) and in the interest of secure, fast, - and efficient provision of our online service by a - professional provider (Art. 6 para. 1 lit. f GDPR). -

+

+ {t.externalHostingTitle} +

+

{t.externalHostingText1}

+

{t.externalHostingText2}

-

GitHub Integration

+

+ {t.githubIntegrationTitle} +

- This website may integrate with GitHub to display repository - information. When you view pages that display GitHub data, - your browser may connect directly to GitHub's servers. - Please refer to + {t.githubIntegrationTextPrefix} - GitHub's Privacy Statement + {t.githubLinkLabel} - for information about their data processing practices. + {t.githubIntegrationTextSuffix}

- 6. Data Storage Duration + {t.storageDurationTitle}

-

- Unless a more specific storage period has been specified in - this privacy policy, your personal data will remain with us - until the purpose for which it was collected no longer - applies. If you assert a justified request for deletion or - revoke your consent to data processing, your data will be - deleted unless we have other legally permissible reasons for - storing your personal data (e.g., tax or commercial law - retention periods); in the latter case, the deletion takes - place after these reasons cease to apply. -

+

{t.storageDurationText}

+

{t.policyNote}

- This privacy policy was created with consideration for the EU - General Data Protection Regulation (GDPR). -

-

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

From 06d6113fe35305192bc7b1cdfe8de7d0ecc85920 Mon Sep 17 00:00:00 2001 From: RBN-Apps <80348653+RBN-Apps@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:44:32 +0200 Subject: [PATCH 4/7] Implement GitHub repository caching and update mechanism for improved performance and compliance --- .gitignore | 1 + backend/pages/api/github-repos.ts | 311 ++++++++++++++++++ .../portfolio/github-integration.tsx | 44 +-- 3 files changed, 337 insertions(+), 19 deletions(-) create mode 100644 backend/pages/api/github-repos.ts 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..6c4201e8 --- /dev/null +++ b/backend/pages/api/github-repos.ts @@ -0,0 +1,311 @@ +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 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"; + +type GitHubReposData = { + updated: Repository[]; + stars: Repository[]; + lastUpdated: string | null; + metadata: { + version: string; + description: string; + updated_at: string | null; + 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) + * @returns Promise resolving to array of repositories + */ +async function fetchReposForSort( + githubUsername: string, + sort: SortOption, +): 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=${REPO_PER_PAGE}`, + ); + } else { + response = await fetch( + `https://api.github.com/users/${githubUsername}/repos?sort=updated&per_page=${REPO_PER_PAGE}`, + ); + } + + 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", + updated_at: null, + username: "", + }, + fetchConfig: { + intervalHours: 1, + reposPerPage: 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", + updated_at: null, + username: "", + }, + fetchConfig: { + intervalHours: 1, + reposPerPage: REPO_PER_PAGE, + }, + }; + } +} + +/** + * Updates GitHub repositories for a specific user + * @param githubUsername - The GitHub username to update repositories for + * @returns Promise that resolves when update is complete + */ +async function updateReposForUser(githubUsername: string): Promise { + try { + // Fetch both updated and starred repos + const [updatedRepos, starredRepos] = await Promise.all([ + fetchReposForSort(githubUsername, "updated"), + fetchReposForSort(githubUsername, "stars"), + ]); + + 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", + updated_at: now, + username: githubUsername, + }, + fetchConfig: { + intervalHours: 1, + reposPerPage: REPO_PER_PAGE, + }, + }; + + // 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 + */ +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method === "GET") { + try { + const { username, sort = "updated", force = "false" } = 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, + ); + + // Check if we need to update (forced, stale data, or different user) + const needsUpdate = + shouldUpdate || + reposData.metadata.username !== username || + reposData[sortOption].length === 0; + + if (needsUpdate) { + try { + await updateReposForUser(username); + // 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/components/portfolio/github-integration.tsx b/src/components/portfolio/github-integration.tsx index a058729d..23dc2b5a 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; @@ -61,26 +59,34 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { sort: SortOption, ) => { try { - let response; - - 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}`, - ); + // First try to read from local JSON file (fastest) + 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 + ) { + return localData[sort]; + } + } + } catch { + // Silently continue to backend API fallback } - if (!response.ok) { - throw new Error("Failed to fetch repositories"); + // If no cached data, try backend API to fetch and cache + const response = await fetch( + `/api/github-repos?username=${githubUsername}&sort=${sort}`, + ); + if (response.ok) { + const data = await response.json(); + return data.repos || []; + } else { + throw new Error("Failed to fetch repositories from backend API"); } - - const data = await response.json(); - - return sort === "stars" ? data.items : data; } catch (err) { console.error(`Error fetching ${sort} repos:`, err); throw err; From 0e01d9ea8073d45bbfa87ae936689aa206fe870d Mon Sep 17 00:00:00 2001 From: RBN-Apps <80348653+RBN-Apps@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:01:20 +0200 Subject: [PATCH 5/7] Enhance GitHub repository caching to include stale data check for improved compliance and performance --- backend/pages/api/github-repos.ts | 4 ---- src/components/portfolio/github-integration.tsx | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/pages/api/github-repos.ts b/backend/pages/api/github-repos.ts index 6c4201e8..f00eb841 100644 --- a/backend/pages/api/github-repos.ts +++ b/backend/pages/api/github-repos.ts @@ -32,7 +32,6 @@ type GitHubReposData = { metadata: { version: string; description: string; - updated_at: string | null; username: string; }; fetchConfig: { @@ -89,7 +88,6 @@ function ensureReposFileExists(): GitHubReposData { metadata: { version: "1.0.0", description: "GitHub repositories data cache for EU compliance", - updated_at: null, username: "", }, fetchConfig: { @@ -120,7 +118,6 @@ function ensureReposFileExists(): GitHubReposData { metadata: { version: "1.0.0", description: "GitHub repositories data cache for EU compliance", - updated_at: null, username: "", }, fetchConfig: { @@ -152,7 +149,6 @@ async function updateReposForUser(githubUsername: string): Promise { metadata: { version: "1.0.0", description: "GitHub repositories data cache for EU compliance", - updated_at: now, username: githubUsername, }, fetchConfig: { diff --git a/src/components/portfolio/github-integration.tsx b/src/components/portfolio/github-integration.tsx index 23dc2b5a..be5a6169 100644 --- a/src/components/portfolio/github-integration.tsx +++ b/src/components/portfolio/github-integration.tsx @@ -64,13 +64,22 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { const localResponse = await fetch("/github-repos.json"); if (localResponse.ok) { const localData = await localResponse.json(); - // Check if cached data is for the requested user + // Check if cached data is for the requested user and not stale if ( localData.metadata?.username === githubUsername && localData[sort] && - localData[sort].length > 0 + localData[sort].length > 0 && + localData.lastUpdated && + localData.fetchConfig?.intervalHours ) { - return localData[sort]; + const lastUpdateTime = new Date(localData.lastUpdated).getTime(); + const now = Date.now(); + const intervalMs = localData.fetchConfig.intervalHours * 60 * 60 * 1000; + const isStale = now - lastUpdateTime >= intervalMs; + + if (!isStale) { + return localData[sort]; + } } } } catch { From a1bb3972432c863f712006a531f4c8e8399688fe Mon Sep 17 00:00:00 2001 From: RBN-Apps <80348653+RBN-Apps@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:05:56 +0200 Subject: [PATCH 6/7] Improve GitHub repository caching logic to handle stale data and fallback scenarios --- .../portfolio/github-integration.tsx | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/components/portfolio/github-integration.tsx b/src/components/portfolio/github-integration.tsx index be5a6169..efc0b301 100644 --- a/src/components/portfolio/github-integration.tsx +++ b/src/components/portfolio/github-integration.tsx @@ -58,44 +58,60 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { githubUsername: string, sort: SortOption, ) => { + let cachedData = null; + try { - // First try to read from local JSON file (fastest) + // 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 and not stale + // Check if cached data is for the requested user if ( localData.metadata?.username === githubUsername && localData[sort] && - localData[sort].length > 0 && - localData.lastUpdated && - localData.fetchConfig?.intervalHours + localData[sort].length > 0 ) { - const lastUpdateTime = new Date(localData.lastUpdated).getTime(); - const now = Date.now(); - const intervalMs = localData.fetchConfig.intervalHours * 60 * 60 * 1000; - const isStale = now - lastUpdateTime >= intervalMs; - - if (!isStale) { - return localData[sort]; + cachedData = localData; + + // If cache is fresh, 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; + + if (!isStale) { + return localData[sort]; + } } } } } catch { - // Silently continue to backend API fallback + // Continue to backend API attempt } - // If no cached data, try backend API to fetch and cache - const response = await fetch( - `/api/github-repos?username=${githubUsername}&sort=${sort}`, - ); - if (response.ok) { - const data = await response.json(); - return data.repos || []; - } else { - throw new Error("Failed to fetch repositories from backend API"); + // If cache is stale or missing, try backend API to fetch fresh data + try { + const response = await fetch( + `/api/github-repos?username=${githubUsername}&sort=${sort}`, + ); + if (response.ok) { + const data = await response.json(); + return data.repos || []; + } + } catch { + // Backend is unavailable, fall back to cached data if available + } + + // Fallback to stale cached data if backend is unavailable + if (cachedData && cachedData[sort]) { + return cachedData[sort]; } + + // No data available at all + throw new Error("No repository data available"); } catch (err) { console.error(`Error fetching ${sort} repos:`, err); throw err; From c7dbe31b8e002c5e8a0b214f77d4346c30b4f1da Mon Sep 17 00:00:00 2001 From: RBN-Apps <80348653+RBN-Apps@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:24:41 +0200 Subject: [PATCH 7/7] Add GitHub repositories count input and update Imprint for legal compliance --- backend/pages/api/github-repos.ts | 69 +- .../portfolio/github-integration.tsx | 26 +- .../portfolioEditor/social-links-form.tsx | 38 +- src/config/site.ts | 1 + src/lib/use-portfolio-editor.ts | 2 +- src/pages/imprint.tsx | 187 +++-- src/pages/privacy.tsx | 667 ++++++++++++------ 7 files changed, 690 insertions(+), 300 deletions(-) diff --git a/backend/pages/api/github-repos.ts b/backend/pages/api/github-repos.ts index f00eb841..3061f263 100644 --- a/backend/pages/api/github-repos.ts +++ b/backend/pages/api/github-repos.ts @@ -8,7 +8,8 @@ const GITHUB_REPOS_FILE = path.join( "frontend", "github-repos.json", ); -const REPO_PER_PAGE = 4; +const PORTFOLIO_FILE = path.join(process.cwd(), "frontend", "portfolio.json"); +const DEFAULT_REPO_PER_PAGE = 4; type Repository = { id: number; @@ -25,6 +26,26 @@ type Repository = { 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[]; @@ -44,22 +65,24 @@ type GitHubReposData = { * 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=${REPO_PER_PAGE}`, + `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=${REPO_PER_PAGE}`, + `https://api.github.com/users/${githubUsername}/repos?sort=updated&per_page=${reposCount}`, ); } @@ -92,7 +115,7 @@ function ensureReposFileExists(): GitHubReposData { }, fetchConfig: { intervalHours: 1, - reposPerPage: REPO_PER_PAGE, + reposPerPage: DEFAULT_REPO_PER_PAGE, }, }; @@ -122,7 +145,7 @@ function ensureReposFileExists(): GitHubReposData { }, fetchConfig: { intervalHours: 1, - reposPerPage: REPO_PER_PAGE, + reposPerPage: DEFAULT_REPO_PER_PAGE, }, }; } @@ -131,14 +154,21 @@ function ensureReposFileExists(): GitHubReposData { /** * 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): Promise { +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"), - fetchReposForSort(githubUsername, "stars"), + fetchReposForSort(githubUsername, "updated", actualReposCount), + fetchReposForSort(githubUsername, "stars", actualReposCount), ]); const now = new Date().toISOString(); @@ -153,7 +183,7 @@ async function updateReposForUser(githubUsername: string): Promise { }, fetchConfig: { intervalHours: 1, - reposPerPage: REPO_PER_PAGE, + reposPerPage: actualReposCount, }, }; @@ -193,6 +223,7 @@ function shouldUpdateRepos( * 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, @@ -200,7 +231,12 @@ export default async function handler( ) { if (req.method === "GET") { try { - const { username, sort = "updated", force = "false" } = req.query; + const { + username, + sort = "updated", + force = "false", + reposCount, + } = req.query; if (!username || typeof username !== "string") { return res.status(400).json({ error: "GitHub username is required" }); @@ -218,15 +254,22 @@ export default async function handler( reposData.fetchConfig.intervalHours, ); - // Check if we need to update (forced, stale data, or different user) + // 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[sortOption].length === 0 || + reposData.fetchConfig.reposPerPage !== currentReposCount; if (needsUpdate) { try { - await updateReposForUser(username); + await updateReposForUser(username, currentReposCount); // Re-read the updated data const updatedData = ensureReposFileExists(); return res.status(200).json({ diff --git a/src/components/portfolio/github-integration.tsx b/src/components/portfolio/github-integration.tsx index efc0b301..cf96e927 100644 --- a/src/components/portfolio/github-integration.tsx +++ b/src/components/portfolio/github-integration.tsx @@ -58,6 +58,8 @@ 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 { @@ -74,7 +76,7 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { ) { cachedData = localData; - // If cache is fresh, use it immediately + // 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(); @@ -82,7 +84,11 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { localData.fetchConfig.intervalHours * 60 * 60 * 1000; const isStale = now - lastUpdateTime >= intervalMs; - if (!isStale) { + // Check if repos count configuration has changed + const cachedReposCount = localData.fetchConfig?.reposPerPage || 4; + const configChanged = currentReposCount !== cachedReposCount; + + if (!isStale && !configChanged) { return localData[sort]; } } @@ -92,10 +98,10 @@ export function GithubIntegration({ refreshTrigger }: GithubIntegrationProps) { // Continue to backend API attempt } - // If cache is stale or missing, try backend API to fetch fresh data + // 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}`, + `/api/github-repos?username=${githubUsername}&sort=${sort}&reposCount=${currentReposCount}`, ); if (response.ok) { const data = await response.json(); @@ -152,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 () => { @@ -194,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/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} + /> + +