feat(legal): add legal notice and privacy pages with footer links
This commit is contained in:
parent
ac964ea98b
commit
2f4d012d59
7 changed files with 329 additions and 7 deletions
|
|
@ -14,6 +14,7 @@ import { ChatMessages } from "@/components/chat/ChatMessages";
|
||||||
import type { MessageWithMember } from "@/components/chat/ChatItem";
|
import type { MessageWithMember } from "@/components/chat/ChatItem";
|
||||||
import { isValidUuid } from "@/lib/utils";
|
import { isValidUuid } from "@/lib/utils";
|
||||||
import { getUserFriendships } from "@/lib/services/friends.service";
|
import { getUserFriendships } from "@/lib/services/friends.service";
|
||||||
|
import AppFooter from "@/components/layout/AppFooter";
|
||||||
|
|
||||||
/** Renders the direct message conversation page with header, message history, and input field. */
|
/** Renders the direct message conversation page with header, message history, and input field. */
|
||||||
export default async function DirectMessagePage({
|
export default async function DirectMessagePage({
|
||||||
|
|
@ -98,6 +99,8 @@ export default async function DirectMessagePage({
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
placeholderName={partner.username}
|
placeholderName={partner.username}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AppFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,9 @@ import { friendships } from "@/db/schema";
|
||||||
import { or, eq } from "drizzle-orm";
|
import { or, eq } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { FriendsView, type Friendship } from "@/components/friends/FriendsView";
|
import { FriendsView, type Friendship } from "@/components/friends/FriendsView";
|
||||||
|
import AppFooter from "@/components/layout/AppFooter";
|
||||||
|
|
||||||
/**
|
/** Asynchronously renders the main application page for authenticated users. */
|
||||||
* Asynchronously renders the main application page for authenticated users.
|
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @function AppPage
|
|
||||||
* @returns {Promise<JSX.Element>} The rendered application page container with the FriendsView component.
|
|
||||||
*/
|
|
||||||
export default async function AppPage() {
|
export default async function AppPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const currentUserId = session?.user?.id;
|
const currentUserId = session?.user?.id;
|
||||||
|
|
@ -58,6 +53,7 @@ export default async function AppPage() {
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
initialFriendships={rawFriendships as Friendship[]}
|
initialFriendships={rawFriendships as Friendship[]}
|
||||||
/>
|
/>
|
||||||
|
<AppFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { getChannelMessages } from "@/lib/services/message.service";
|
||||||
import { getServerById } from "@/lib/services/server.service";
|
import { getServerById } from "@/lib/services/server.service";
|
||||||
import { isValidUuid } from "@/lib/utils";
|
import { isValidUuid } from "@/lib/utils";
|
||||||
import { getUserFriendships } from "@/lib/services/friends.service";
|
import { getUserFriendships } from "@/lib/services/friends.service";
|
||||||
|
import AppFooter from "@/components/layout/AppFooter";
|
||||||
|
|
||||||
/** Renders the channel chat view by validating parameters, checking user session, fetching channel data, friendships, and displaying headers, messages, and input controls. */
|
/** Renders the channel chat view by validating parameters, checking user session, fetching channel data, friendships, and displaying headers, messages, and input controls. */
|
||||||
export default async function ChannelPage({
|
export default async function ChannelPage({
|
||||||
|
|
@ -66,6 +67,8 @@ export default async function ChannelPage({
|
||||||
channelId={channel.id}
|
channelId={channel.id}
|
||||||
placeholderName={channel.name}
|
placeholderName={channel.name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AppFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
112
app/(legal)/imprint/page.tsx
Normal file
112
app/(legal)/imprint/page.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
/**
|
||||||
|
* @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="rounded-2xl border border-surface/80 bg-surface/30 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/80 p-4 rounded-xl border border-surface/80 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="rounded-2xl border border-surface/80 bg-surface/30 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 rounded-xl bg-background/80 border border-surface/80 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="rounded-2xl border border-surface/80 bg-surface/30 p-6 shadow-xl space-y-4">
|
||||||
|
<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="rounded-2xl border border-surface/80 bg-surface/30 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
138
app/(legal)/privacy/page.tsx
Normal file
138
app/(legal)/privacy/page.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
/**
|
||||||
|
* @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="rounded-2xl border border-surface/80 bg-surface/30 p-6 shadow-xl space-y-4">
|
||||||
|
<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="rounded-2xl border border-surface/80 bg-surface/30 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/80 p-4 rounded-xl border border-surface/80 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="rounded-2xl border border-surface/80 bg-surface/30 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'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="rounded-2xl border border-surface/80 bg-surface/30 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="rounded-2xl border border-surface/80 bg-surface/30 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
components/layout/AppFooter.tsx
Normal file
48
components/layout/AppFooter.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* @file app/components/layout/AppFooter.tsx
|
||||||
|
* @description Site footer component rendering legal compliance links and dynamic copyright notice.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { LEGAL_LINKS, COPYRIGHT_TEXT, ICON_MAP } from "@/lib/legal";
|
||||||
|
|
||||||
|
/** Renders the site footer containing the dynamic copyright notice and legal compliance links with icons. */
|
||||||
|
export default function AppFooter() {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="w-full shrink-0 bg-background text-xs mt-4">
|
||||||
|
<div className="w-full flex flex-row flex-wrap justify-between items-center gap-2">
|
||||||
|
{/* Left: Copyright Notice */}
|
||||||
|
<p className="text-muted truncate">{COPYRIGHT_TEXT(currentYear)}</p>
|
||||||
|
|
||||||
|
{/* Right: Legal & Compliance Links */}
|
||||||
|
<div className="flex items-center gap-2 sm:gap-4 text-muted">
|
||||||
|
{LEGAL_LINKS.map((link, index) => {
|
||||||
|
const IconComponent = ICON_MAP[link.iconName];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
<IconComponent className="w-3.5 h-3.5 shrink-0 text-foreground-muted group-hover:text-foreground transition-colors" />
|
||||||
|
<span className="hidden sm:inline">{link.name}</span>
|
||||||
|
</Link>
|
||||||
|
{index < LEGAL_LINKS.length - 1 && (
|
||||||
|
<span className="text-foreground-muted">|</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
lib/legal.ts
Normal file
22
lib/legal.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* @file app/lib/legal.ts
|
||||||
|
* @description Defines legal navigation configurations, icon mappings, and dynamic copyright text generators.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FileText, ShieldCheck } from "lucide-react";
|
||||||
|
|
||||||
|
/** List of legal navigation links with their corresponding route paths and icon identifiers. */
|
||||||
|
export const LEGAL_LINKS = [
|
||||||
|
{ name: "Imprint", href: "/imprint", iconName: "FileText" },
|
||||||
|
{ name: "Privacy Policy", href: "/privacy", iconName: "ShieldCheck" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Mapping object linking string identifiers to their respective Lucide icon components. */
|
||||||
|
export const ICON_MAP = {
|
||||||
|
FileText: FileText,
|
||||||
|
ShieldCheck: ShieldCheck,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Generates the standardized copyright notice string with the given year. */
|
||||||
|
export const COPYRIGHT_TEXT = (year: number) =>
|
||||||
|
`© ${year} André Kempf. All rights reserved.`;
|
||||||
Loading…
Reference in a new issue