feat(legal): integrate legal pages in-app with header support
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m21s

This commit is contained in:
Chneemann 2026-09-29 04:45:04 +02:00
parent 342dbc549f
commit a957991bee
No known key found for this signature in database
6 changed files with 290 additions and 281 deletions

View file

@ -0,0 +1,110 @@
/**
* @file app/(legal)/imprint/page.tsx
* @description Server component rendering the imprint/legal notice page tailored to the app layout design.
*/
import AppFooter from "@/components/layout/AppFooter";
import { AppHeader } from "@/components/layout/AppHeader";
export default async function ImprintPage() {
return (
<div className="flex flex-col h-full w-full bg-background p-4 min-h-0 overflow-hidden">
<AppHeader isLegalImprint />
<div className="flex-1 overflow-y-auto flex flex-col gap-4 py-4 min-h-0 pr-1">
{/* Information pursuant to § 5 DDG */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Information Pursuant to § 5
DDG
</h2>
<div className="font-mono text-xs bg-background border border-surface/80 rounded-xl p-4 text-foreground space-y-1">
<p className="font-semibold text-white">André Kempf</p>
<p className="text-accent">Full-Stack Web Developer</p>
<p className="text-muted">Großschneidersweg 2a</p>
<p className="text-muted">76149 Karlsruhe, Germany</p>
</div>
<p className="text-xs text-muted">
Also responsible for content pursuant to § 18 Abs. 2 MStV.
</p>
</div>
{/* Contact */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Contact
</h2>
<div className="p-4 bg-background border border-surface/80 rounded-xl font-mono text-xs">
<p className="text-muted">
<span className="text-white">Email:</span>{" "}
<span className="text-accent hover:underline cursor-pointer">
dev@andre-kempf.com
</span>
</p>
</div>
</div>
{/* Disclaimer */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Disclaimer & Legal Notes
</h2>
<div className="space-y-1">
<h3 className="font-semibold text-white text-sm">
Liability for Content
</h3>
<p className="text-muted">
As a service provider, I am responsible for my own content on
these pages according to general laws pursuant to § 7 Abs. 1 DDG.
However, according to §§ 8 to 10 DDG, I am not obligated to
monitor transmitted or stored third-party information or to
investigate circumstances that indicate illegal activity.
</p>
</div>
<div className="space-y-1 pt-3 border-t border-surface/60">
<h3 className="font-semibold text-white text-sm">
Liability for Links
</h3>
<p className="text-muted">
My website contains links to external third-party websites over
whose content I have no control. Therefore, I cannot accept any
liability for these external contents. The respective provider or
operator of the pages is always responsible for the content of the
linked pages.
</p>
</div>
<div className="space-y-1 pt-3 border-t border-surface/60">
<h3 className="font-semibold text-white text-sm">Copyright</h3>
<p className="text-muted">
The content and works created on these pages are subject to German
copyright law. Duplication, processing, distribution, or any form
of commercialization beyond the scope of copyright law require the
prior written consent of the author or creator.
</p>
</div>
</div>
{/* Dispute Resolution */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Dispute Resolution
</h2>
<p className="text-muted">
The European Commission provides a platform for online dispute
resolution (OS):{" "}
<a
href="https://ec.europa.eu/consumers/odr/"
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline break-all"
>
https://ec.europa.eu/consumers/odr/
</a>
.<br />I am neither willing nor obligated to participate in dispute
resolution proceedings before a consumer arbitration board.
</p>
</div>
</div>
<AppFooter />
</div>
);
}

View file

@ -0,0 +1,135 @@
/**
* @file app/(legal)/privacy/page.tsx
* @description Server component rendering the privacy policy page tailored to the app layout design.
*/
import AppFooter from "@/components/layout/AppFooter";
import { AppHeader } from "@/components/layout/AppHeader";
export default async function PrivacyPage() {
return (
<div className="flex flex-col h-full w-full bg-background p-4 min-h-0 overflow-hidden">
<AppHeader isLegalPrivacy />
<div className="flex-1 overflow-y-auto flex flex-col gap-4 py-4 min-h-0 pr-1">
{/* Overview */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Data Protection at a Glance
</h2>
<div className="space-y-2">
<h3 className="font-semibold text-white text-sm">
General Information
</h3>
<p className="text-muted">
The following notes provide a simple overview of what happens to
your personal data when you visit this website. Personal data is
any data that can be used to personally identify you.
</p>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-white text-sm">
Data Collection on This Website
</h3>
<p className="text-muted">
Data processing on this website is carried out by the website
operator. Your data is collected when you provide it to us, or
automatically by our IT systems when you visit the site.
</p>
</div>
</div>
{/* Responsible Party */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Responsible Party
(Controller)
</h2>
<p className="text-muted">
The controller responsible for data processing on this website is:
</p>
<div className="font-mono text-xs bg-background border border-surface/80 rounded-xl p-4 text-foreground space-y-1">
<p className="font-semibold text-white">André Kempf</p>
<p className="text-muted">Großschneidersweg 2a</p>
<p className="text-muted">76149 Karlsruhe, Germany</p>
<p className="pt-2 text-muted">
Email:{" "}
<span className="text-accent hover:underline cursor-pointer font-medium">
dev@andre-kempf.com
</span>
</p>
</div>
</div>
{/* Hosting & Server Log Files */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Hosting & Server
Infrastructure
</h2>
<p className="text-muted">
This website is hosted externally on web servers operated by Netcup
GmbH. Personal data processed on this website is stored on the
host&apos;s secure servers.
</p>
<p className="text-muted">
The hosting provider automatically processes technical access data
in server environment variables necessary to establish a stable
connection and deliver page assets securely.
</p>
</div>
{/* Server Analytics */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Server Analytics &
Privacy-First Tracking
</h2>
<p className="text-muted">
To evaluate website reach and optimize user experience, this website
processes minimal access metrics (e.g., total daily views, coarse
device category, and daily visitor counts).
</p>
<div className="p-4 rounded-xl bg-background/80 border border-surface/80 space-y-2 text-xs font-mono">
<p className="text-emerald-400 font-semibold">
// Key privacy guarantees:
</p>
<ul className="list-disc list-inside space-y-1 text-muted">
<li>
<strong className="text-white">No Cookies:</strong> We do not
store cookies or local storage identifiers on your device.
</li>
<li>
<strong className="text-white">Anonymized IPs:</strong> IP
addresses are instantly hashed with a daily salt and never
stored in plain text.
</li>
<li>
<strong className="text-white">Zero Third Parties:</strong>{" "}
Analytics data is processed locally on our own server and never
shared with external tracking services.
</li>
</ul>
</div>
<p className="text-xs text-muted">
The legal basis for this processing is our legitimate interest in
maintaining and optimizing our online portfolio (Art. 6(1)(f) GDPR).
</p>
</div>
{/* Your Rights */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Your Rights
</h2>
<p className="text-muted">
You have the right at any time to receive information free of charge
about the origin, recipient, and purpose of your stored personal
data. You also have a right to request the correction or deletion of
this data.
</p>
</div>
</div>
<AppFooter />
</div>
);
}

View file

@ -1,112 +0,0 @@
/**
* @file app/(legal)/imprint/page.tsx
* @description Server component rendering the imprint/legal notice page tailored to the app layout design.
*/
export default async function ImprintPage() {
return (
<section className="flex-1 overflow-y-auto w-full bg-background">
<div className="relative p-6 max-w-7xl mx-auto">
<h1 className="text-2xl font-bold text-white mb-1 tracking-tight">
Legal Notice<span className="text-accent">.</span>
</h1>
<div className="space-y-6 text-sm mt-4 text-foreground/80 leading-relaxed">
{/* Information pursuant to § 5 DDG */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Information Pursuant to §
5 DDG
</h2>
<div className="font-mono text-xs bg-background border border-surface/80 rounded-xl p-4 text-foreground space-y-1">
<p className="font-semibold text-white">André Kempf</p>
<p className="text-accent">Full-Stack Web Developer</p>
<p className="text-muted">Großschneidersweg 2a</p>
<p className="text-muted">76149 Karlsruhe, Germany</p>
</div>
<p className="text-xs text-muted">
Also responsible for content pursuant to § 18 Abs. 2 MStV.
</p>
</div>
{/* Contact */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Contact
</h2>
<div className="p-4 bg-background border border-surface/80 rounded-xl font-mono text-xs">
<p className="text-muted">
<span className="text-white">Email:</span>{" "}
<span className="text-accent hover:underline cursor-pointer">
dev@andre-kempf.com
</span>
</p>
</div>
</div>
{/* Disclaimer */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Disclaimer & Legal Notes
</h2>
<div className="space-y-1">
<h3 className="font-semibold text-white text-sm">
Liability for Content
</h3>
<p className="text-muted">
As a service provider, I am responsible for my own content on
these pages according to general laws pursuant to § 7 Abs. 1
DDG. However, according to §§ 8 to 10 DDG, I am not obligated to
monitor transmitted or stored third-party information or to
investigate circumstances that indicate illegal activity.
</p>
</div>
<div className="space-y-1 pt-3 border-t border-surface/60">
<h3 className="font-semibold text-white text-sm">
Liability for Links
</h3>
<p className="text-muted">
My website contains links to external third-party websites over
whose content I have no control. Therefore, I cannot accept any
liability for these external contents. The respective provider
or operator of the pages is always responsible for the content
of the linked pages.
</p>
</div>
<div className="space-y-1 pt-3 border-t border-surface/60">
<h3 className="font-semibold text-white text-sm">Copyright</h3>
<p className="text-muted">
The content and works created on these pages are subject to
German copyright law. Duplication, processing, distribution, or
any form of commercialization beyond the scope of copyright law
require the prior written consent of the author or creator.
</p>
</div>
</div>
{/* Dispute Resolution */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Dispute Resolution
</h2>
<p className="text-muted">
The European Commission provides a platform for online dispute
resolution (OS):{" "}
<a
href="https://ec.europa.eu/consumers/odr/"
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline break-all"
>
https://ec.europa.eu/consumers/odr/
</a>
.<br />I am neither willing nor obligated to participate in
dispute resolution proceedings before a consumer arbitration
board.
</p>
</div>
</div>
</div>
</section>
);
}

View file

@ -1,138 +0,0 @@
/**
* @file app/(legal)/privacy/page.tsx
* @description Server component rendering the privacy policy page tailored to the app layout design.
*/
export default async function PrivacyPage() {
return (
<section className="flex-1 overflow-y-auto w-full bg-background">
<div className="relative p-6 max-w-7xl mx-auto">
<h1 className="text-2xl font-bold text-white mb-1 tracking-tight">
Privacy Policy<span className="text-accent">.</span>
</h1>
<div className="space-y-6 text-sm mt-4 text-foreground/80 leading-relaxed">
{/* Overview */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Data Protection at a
Glance
</h2>
<div className="space-y-2">
<h3 className="font-semibold text-white text-sm">
General Information
</h3>
<p className="text-muted">
The following notes provide a simple overview of what happens to
your personal data when you visit this website. Personal data is
any data that can be used to personally identify you.
</p>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-white text-sm">
Data Collection on This Website
</h3>
<p className="text-muted">
Data processing on this website is carried out by the website
operator. Your data is collected when you provide it to us, or
automatically by our IT systems when you visit the site.
</p>
</div>
</div>
{/* Responsible Party */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Responsible Party
(Controller)
</h2>
<p className="text-muted">
The controller responsible for data processing on this website is:
</p>
<div className="font-mono text-xs bg-background border border-surface/80 rounded-xl p-4 text-foreground space-y-1">
<p className="font-semibold text-white">André Kempf</p>
<p className="text-muted">Großschneidersweg 2a</p>
<p className="text-muted">76149 Karlsruhe, Germany</p>
<p className="pt-2 text-muted">
Email:{" "}
<span className="text-accent hover:underline cursor-pointer font-medium">
dev@andre-kempf.com
</span>
</p>
</div>
</div>
{/* Hosting & Server Log Files */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Hosting & Server
Infrastructure
</h2>
<p className="text-muted">
This website is hosted externally on web servers operated by
Netcup GmbH. Personal data processed on this website is stored on
the host&apos;s secure servers.
</p>
<p className="text-muted">
The hosting provider automatically processes technical access data
in server environment variables necessary to establish a stable
connection and deliver page assets securely.
</p>
</div>
{/* Server Analytics */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Server Analytics &
Privacy-First Tracking
</h2>
<p className="text-muted">
To evaluate website reach and optimize user experience, this
website processes minimal access metrics (e.g., total daily views,
coarse device category, and daily visitor counts).
</p>
<div className="p-4 rounded-xl bg-background/80 border border-surface/80 space-y-2 text-xs font-mono">
<p className="text-emerald-400 font-semibold">
// Key privacy guarantees:
</p>
<ul className="list-disc list-inside space-y-1 text-muted">
<li>
<strong className="text-white">No Cookies:</strong> We do not
store cookies or local storage identifiers on your device.
</li>
<li>
<strong className="text-white">Anonymized IPs:</strong> IP
addresses are instantly hashed with a daily salt and never
stored in plain text.
</li>
<li>
<strong className="text-white">Zero Third Parties:</strong>{" "}
Analytics data is processed locally on our own server and
never shared with external tracking services.
</li>
</ul>
</div>
<p className="text-xs text-muted">
The legal basis for this processing is our legitimate interest in
maintaining and optimizing our online portfolio (Art. 6(1)(f)
GDPR).
</p>
</div>
{/* Your Rights */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 shadow-xl space-y-3">
<h2 className="text-base font-semibold text-white flex items-center gap-2 font-mono">
<span className="text-accent">//</span> Your Rights
</h2>
<p className="text-muted">
You have the right at any time to receive information free of
charge about the origin, recipient, and purpose of your stored
personal data. You also have a right to request the correction or
deletion of this data.
</p>
</div>
</div>
</div>
</section>
);
}

View file

@ -27,8 +27,6 @@ export default function AppFooter() {
<div key={link.href} className="flex items-center gap-2 sm:gap-4">
<Link
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="group inline-flex items-center gap-1.5 hover:text-foreground transition-colors cursor-pointer"
title={link.name}
>

View file

@ -1,6 +1,6 @@
/**
* @file components/layout/AppHeader.tsx
* @description Application top header component providing navigation controls, dynamic channel or friend tab titles, and optional server settings or member list toggles.
* @description Application top header component providing navigation controls, dynamic channel or friend tab titles, optional server settings, member list toggles, and legal views support.
*/
"use client";
@ -12,24 +12,13 @@ import {
Users,
Hash,
Settings,
FileText,
ShieldCheck,
} from "lucide-react";
import { ServerSettingsMenu } from "./ServerSettingsMenu";
import { FriendsHeader, TabType } from "@/components/friends/FriendsHeader";
/**
* Properties for the AppHeader component.
*
* @interface AppHeaderProps
* @property {string} [title] - The title of the current channel or view.
* @property {boolean} [showMembersButton=false] - Whether to show the button that toggles the members sidebar.
* @property {{ id: string; name: string }} [server] - Optional server configuration object containing its unique identifier and name.
* @property {boolean} [showFriendsTabs=false] - Whether to display the friends navigation tabs in the header.
* @property {TabType} [activeTab] - The currently active friends tab identifier.
* @property {(tab: TabType) => void} [setActiveTab] - Callback function to update the active friends tab.
* @property {number} [allCount=0] - The total number of friends.
* @property {number} [pendingCount=0] - The number of pending friend requests.
* @property {boolean} [isSettings=false] - Whether the header is rendered in the settings view.
*/
/** Properties for the AppHeader component. */
interface AppHeaderProps {
title?: string;
showMembersButton?: boolean;
@ -43,11 +32,11 @@ interface AppHeaderProps {
allCount?: number;
pendingCount?: number;
isSettings?: boolean;
isLegalImprint?: boolean;
isLegalPrivacy?: boolean;
}
/**
* Renders the application header with navigation controls, dynamic titles, tabs, and action buttons.
*/
/** Renders the application header with navigation controls, dynamic titles, tabs, and action buttons. */
export function AppHeader({
title,
showMembersButton = false,
@ -58,16 +47,21 @@ export function AppHeader({
allCount = 0,
pendingCount = 0,
isSettings = false,
isLegalImprint = false,
isLegalPrivacy = false,
}: AppHeaderProps) {
const { isNavOpen, toggleNav, toggleMembers } = useSidebarStore();
const isLegalView = isLegalImprint || isLegalPrivacy;
const hasContent =
!isNavOpen ||
!!title ||
showMembersButton ||
!!server ||
showFriendsTabs ||
isSettings;
isSettings ||
isLegalView;
return (
<div
@ -76,7 +70,7 @@ export function AppHeader({
}`}
>
<div className="flex items-center gap-2 min-w-0 overflow-hidden">
{/* Toggle button for navigation (auch in Settings sichtbar, wenn eingeklappt) */}
{/* Toggle button for navigation */}
<button
type="button"
onClick={toggleNav}
@ -102,8 +96,26 @@ export function AppHeader({
</div>
)}
{/* Legal Imprint Header Title */}
{isLegalImprint && (
<div className="flex items-center gap-1.5 ml-1 min-w-0">
<FileText className="w-4 h-4 text-muted shrink-0" />
<h1 className="font-bold text-white text-base truncate">Imprint</h1>
</div>
)}
{/* Legal Privacy Header Title */}
{isLegalPrivacy && (
<div className="flex items-center gap-1.5 ml-1 min-w-0">
<ShieldCheck className="w-4 h-4 text-muted shrink-0" />
<h1 className="font-bold text-white text-base truncate">
Privacy Policy
</h1>
</div>
)}
{/* Dynamic Channel Title */}
{!isSettings && title && (
{!isSettings && !isLegalView && title && (
<div className="flex items-center gap-1.5 ml-1 min-w-0">
<Hash className="w-4 h-4 text-muted shrink-0" />
<h1 className="font-bold text-white text-base truncate">{title}</h1>
@ -111,7 +123,11 @@ export function AppHeader({
)}
{/* Friends Header Component */}
{!isSettings && showFriendsTabs && setActiveTab && activeTab && (
{!isSettings &&
!isLegalView &&
showFriendsTabs &&
setActiveTab &&
activeTab && (
<FriendsHeader
activeTab={activeTab}
setActiveTab={setActiveTab}