feat: Add GDPR compliance kit with cookie consent and privacy templates
- Privacy policy template with GDPR Art. 13/14 requirements - Cookie consent React component with granular controls - GDPR-compliant analytics wrapper for Vercel/GA - Netcup DPA Annex 3 completion guide - Records of Processing Activities (ROPA) template - High-priority backlog task for deployment to all sites 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
3e926fee0e
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Netcup DPA Annex 3 - Processing Specifications Guide
|
||||||
|
|
||||||
|
This guide helps you fill out Annex 3 of the Netcup Data Processing Agreement.
|
||||||
|
|
||||||
|
## Your Specific Situation
|
||||||
|
|
||||||
|
Based on your infrastructure:
|
||||||
|
- **Hosting Provider**: netcup GmbH (Germany)
|
||||||
|
- **CDN/Security**: Cloudflare
|
||||||
|
- **Newsletter**: Listmonk (self-hosted on Netcup)
|
||||||
|
- **Analytics**: Vercel Analytics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 1: Subject (Nature & Purpose) of the Processing
|
||||||
|
|
||||||
|
**Recommended text to enter:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Web hosting and delivery of websites and web applications. This includes:
|
||||||
|
- Serving static and dynamic web content to visitors
|
||||||
|
- Processing contact form submissions
|
||||||
|
- Managing newsletter subscriptions (via self-hosted Listmonk)
|
||||||
|
- Collecting anonymized website analytics
|
||||||
|
- Storing user-generated content where applicable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 2: Duration of the Processing
|
||||||
|
|
||||||
|
This is automatically determined by your contract term with Netcup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 3: Location of the Processing
|
||||||
|
|
||||||
|
The location is determined by your Netcup server location. For your RS 8000 G12 Pro:
|
||||||
|
- **Primary Location**: Nuremberg, Germany (EU)
|
||||||
|
- **Additional Processing**: Via Cloudflare's global network (with EU data residency options)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 4: Categories of Data Subjects
|
||||||
|
|
||||||
|
**Check the following boxes:**
|
||||||
|
|
||||||
|
- [x] **Customers** - if you have any e-commerce or client portals
|
||||||
|
- [x] **Interested parties** - potential customers visiting your sites
|
||||||
|
- [ ] **Suppliers** - only if you process supplier data
|
||||||
|
- [x] **Visitors to the website** - all website visitors
|
||||||
|
- [ ] **Employees of the Client** - only if you have employee data on the sites
|
||||||
|
- [ ] **External employees** - only if applicable
|
||||||
|
- [ ] **Data processors, other processors** - only if you subcontract
|
||||||
|
- [x] **Newsletter subscribers** - you use Listmonk
|
||||||
|
|
||||||
|
**Additional data subjects (if any):**
|
||||||
|
```
|
||||||
|
Event attendees (if you host events/conferences)
|
||||||
|
Community members (if you have user accounts)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 5: Categories of Personal Data
|
||||||
|
|
||||||
|
**Check the following boxes:**
|
||||||
|
|
||||||
|
- [x] **Name data** - contact forms, newsletter signups
|
||||||
|
- [ ] **Date of birth** - only if you collect this
|
||||||
|
- [ ] **Bank and payment data** - only if you handle payments directly
|
||||||
|
- [ ] **Location and geographic information data** - only if you track location
|
||||||
|
- [ ] **Education data** - only if relevant to your sites
|
||||||
|
- [ ] **Traffic data** - only if you log detailed traffic
|
||||||
|
- [ ] **Data relevant to criminal law** - NO
|
||||||
|
- [x] **Contact and address data** - contact forms
|
||||||
|
- [ ] **Customer contract data** - only if you have customer portals
|
||||||
|
- [ ] **Login and authentication** - only if you have user accounts
|
||||||
|
- [ ] **Preference and behavior data** - only if you track preferences
|
||||||
|
- [ ] **Motion profile data** - NO
|
||||||
|
- [ ] **Photo, video, or audio data** - only if you store media
|
||||||
|
|
||||||
|
**Additional data types:**
|
||||||
|
```
|
||||||
|
Email addresses
|
||||||
|
IP addresses (anonymized for analytics)
|
||||||
|
Browser/device information (anonymized)
|
||||||
|
Cookie consent preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Special Categories of Data (Art. 9 GDPR)
|
||||||
|
|
||||||
|
**IMPORTANT**: Select the first option unless you specifically process sensitive data.
|
||||||
|
|
||||||
|
- [x] **No special categories of personal data ("sensitive data") according to Art 9 GDPR are processed.**
|
||||||
|
|
||||||
|
If any of your sites deal with health, religion, political opinions, biometric data, etc., you would need to check the second option and specify which categories.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Form Example
|
||||||
|
|
||||||
|
Here's how your completed Annex 3 should look:
|
||||||
|
|
||||||
|
### 1. Subject Matter
|
||||||
|
```
|
||||||
|
Web hosting and content delivery for multiple websites and web applications including:
|
||||||
|
- Static and dynamic website hosting
|
||||||
|
- Newsletter subscription management (Listmonk)
|
||||||
|
- Contact form processing
|
||||||
|
- Anonymized web analytics collection
|
||||||
|
- Content management systems
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Data Subjects (check these):
|
||||||
|
- [x] Interested parties
|
||||||
|
- [x] Visitors to the website
|
||||||
|
- [x] Newsletter subscribers
|
||||||
|
- [x] Customers (if applicable)
|
||||||
|
|
||||||
|
### 5. Personal Data Categories (check these):
|
||||||
|
- [x] Name data
|
||||||
|
- [x] Contact and address data
|
||||||
|
|
||||||
|
**Additional data:**
|
||||||
|
```
|
||||||
|
Email addresses
|
||||||
|
IP addresses (anonymized)
|
||||||
|
Browser user agent information
|
||||||
|
Cookie consent preferences
|
||||||
|
Website usage data (anonymized)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special Categories:
|
||||||
|
- [x] No special categories of personal data are processed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Submitting
|
||||||
|
|
||||||
|
1. **Save a copy** of the completed agreement for your records
|
||||||
|
2. **Date it** when you submit
|
||||||
|
3. **Review annually** to ensure it still accurately reflects your processing activities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Be conservative** - only check categories you actually process
|
||||||
|
2. **When in doubt, exclude** - you can always add categories later
|
||||||
|
3. **Keep it updated** - if you add new features that collect data, update the DPA
|
||||||
|
4. **Document everything** - maintain your own Records of Processing Activities (ROPA)
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
# Privacy Policy
|
||||||
|
|
||||||
|
**Last Updated: [DATE]**
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
Welcome to [WEBSITE_NAME] ("we," "our," or "us"). We are committed to protecting your personal data and respecting your privacy in accordance with the General Data Protection Regulation (GDPR) and other applicable data protection laws.
|
||||||
|
|
||||||
|
This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website [WEBSITE_URL] (the "Site").
|
||||||
|
|
||||||
|
## 2. Data Controller
|
||||||
|
|
||||||
|
The data controller responsible for your personal data is:
|
||||||
|
|
||||||
|
**Jeff Emmett**
|
||||||
|
23 Birchpark Dr
|
||||||
|
L3M 4M9 Grimsby, Canada
|
||||||
|
|
||||||
|
Email: [CONTACT_EMAIL]
|
||||||
|
|
||||||
|
## 3. What Data We Collect
|
||||||
|
|
||||||
|
### 3.1 Data You Provide to Us
|
||||||
|
|
||||||
|
We may collect the following categories of personal data that you voluntarily provide:
|
||||||
|
|
||||||
|
- **Contact Information**: Name, email address when you contact us or subscribe to our newsletter
|
||||||
|
- **Communication Data**: Content of messages you send us through contact forms or email
|
||||||
|
|
||||||
|
### 3.2 Data Collected Automatically
|
||||||
|
|
||||||
|
When you visit our Site, we may automatically collect:
|
||||||
|
|
||||||
|
- **Technical Data**: IP address (anonymized), browser type, operating system, device type
|
||||||
|
- **Usage Data**: Pages visited, time spent on pages, referring website, click patterns
|
||||||
|
- **Cookie Data**: See our Cookie Policy section below
|
||||||
|
|
||||||
|
### 3.3 Data We Do NOT Collect
|
||||||
|
|
||||||
|
We do not collect:
|
||||||
|
- Special category data (health, religion, political opinions, etc.)
|
||||||
|
- Financial/payment data (unless you make a purchase, handled by third-party processors)
|
||||||
|
- Data from children under 16 years of age
|
||||||
|
|
||||||
|
## 4. How We Use Your Data
|
||||||
|
|
||||||
|
We process your personal data for the following purposes and legal bases:
|
||||||
|
|
||||||
|
| Purpose | Legal Basis (GDPR Art. 6) |
|
||||||
|
|---------|--------------------------|
|
||||||
|
| Responding to your inquiries | Legitimate interest / Contract performance |
|
||||||
|
| Sending newsletters (if subscribed) | Consent |
|
||||||
|
| Website analytics and improvement | Legitimate interest / Consent |
|
||||||
|
| Security and fraud prevention | Legitimate interest |
|
||||||
|
| Legal compliance | Legal obligation |
|
||||||
|
|
||||||
|
## 5. Newsletter & Email Communications
|
||||||
|
|
||||||
|
If you subscribe to our newsletter:
|
||||||
|
- We use **Listmonk** (self-hosted) to manage subscriptions
|
||||||
|
- You can unsubscribe at any time using the link in every email
|
||||||
|
- We will never share your email with third parties for marketing
|
||||||
|
- Legal basis: Your explicit consent (GDPR Art. 6(1)(a))
|
||||||
|
|
||||||
|
## 6. Cookies and Tracking
|
||||||
|
|
||||||
|
### 6.1 What Are Cookies?
|
||||||
|
|
||||||
|
Cookies are small text files stored on your device when you visit websites. We use cookies to:
|
||||||
|
- Remember your preferences (e.g., cookie consent choice)
|
||||||
|
- Understand how you use our website (analytics)
|
||||||
|
|
||||||
|
### 6.2 Types of Cookies We Use
|
||||||
|
|
||||||
|
| Cookie Type | Purpose | Duration | Consent Required? |
|
||||||
|
|-------------|---------|----------|-------------------|
|
||||||
|
| **Strictly Necessary** | Essential for site functionality | Session | No |
|
||||||
|
| **Analytics** | Understand site usage patterns | 1 year | Yes |
|
||||||
|
| **Preferences** | Remember your settings | 1 year | Yes |
|
||||||
|
|
||||||
|
### 6.3 Analytics
|
||||||
|
|
||||||
|
We use [Vercel Analytics / Plausible / other] to understand how visitors interact with our Site. This service:
|
||||||
|
- [Collects anonymized usage data / Collects IP addresses]
|
||||||
|
- [Does not use cookies / Uses first-party cookies]
|
||||||
|
- Data is processed in [location]
|
||||||
|
|
||||||
|
### 6.4 Managing Cookies
|
||||||
|
|
||||||
|
You can manage cookies through:
|
||||||
|
- Our cookie consent banner (appears on first visit)
|
||||||
|
- Your browser settings
|
||||||
|
- Links at the bottom of our pages
|
||||||
|
|
||||||
|
To opt-out of analytics, you can:
|
||||||
|
- Click "Reject" on our cookie consent banner
|
||||||
|
- Use browser extensions like uBlock Origin or Privacy Badger
|
||||||
|
|
||||||
|
## 7. Data Sharing and Third Parties
|
||||||
|
|
||||||
|
We may share your data with:
|
||||||
|
|
||||||
|
### 7.1 Infrastructure Providers (Data Processors)
|
||||||
|
|
||||||
|
| Provider | Service | Location | DPA |
|
||||||
|
|----------|---------|----------|-----|
|
||||||
|
| **netcup GmbH** | Web hosting infrastructure | Germany (EU) | Yes |
|
||||||
|
| **Cloudflare, Inc.** | CDN, security, DNS | Global (US company, EU processing) | Yes |
|
||||||
|
| **Vercel Inc.** | Analytics | US | Yes |
|
||||||
|
|
||||||
|
### 7.2 We Never:
|
||||||
|
- Sell your personal data
|
||||||
|
- Share data with advertisers
|
||||||
|
- Transfer data without appropriate safeguards
|
||||||
|
|
||||||
|
### 7.3 International Transfers
|
||||||
|
|
||||||
|
Some of our service providers are based outside the EU/EEA. When we transfer data internationally, we ensure appropriate safeguards such as:
|
||||||
|
- EU Standard Contractual Clauses (SCCs)
|
||||||
|
- Data Processing Agreements
|
||||||
|
- Adequacy decisions where applicable
|
||||||
|
|
||||||
|
## 8. Data Retention
|
||||||
|
|
||||||
|
We retain your personal data only for as long as necessary:
|
||||||
|
|
||||||
|
| Data Type | Retention Period |
|
||||||
|
|-----------|-----------------|
|
||||||
|
| Contact form submissions | 2 years |
|
||||||
|
| Newsletter subscriptions | Until you unsubscribe + 30 days |
|
||||||
|
| Analytics data | 14 months |
|
||||||
|
| Server logs | 14 days |
|
||||||
|
|
||||||
|
## 9. Your Rights Under GDPR
|
||||||
|
|
||||||
|
You have the following rights regarding your personal data:
|
||||||
|
|
||||||
|
### 9.1 Right of Access (Art. 15)
|
||||||
|
Request a copy of your personal data we hold.
|
||||||
|
|
||||||
|
### 9.2 Right to Rectification (Art. 16)
|
||||||
|
Request correction of inaccurate or incomplete data.
|
||||||
|
|
||||||
|
### 9.3 Right to Erasure (Art. 17)
|
||||||
|
Request deletion of your data ("right to be forgotten").
|
||||||
|
|
||||||
|
### 9.4 Right to Restrict Processing (Art. 18)
|
||||||
|
Request limitation of how we process your data.
|
||||||
|
|
||||||
|
### 9.5 Right to Data Portability (Art. 20)
|
||||||
|
Receive your data in a structured, commonly used format.
|
||||||
|
|
||||||
|
### 9.6 Right to Object (Art. 21)
|
||||||
|
Object to processing based on legitimate interests, including profiling.
|
||||||
|
|
||||||
|
### 9.7 Right to Withdraw Consent (Art. 7)
|
||||||
|
Withdraw consent at any time (does not affect prior lawful processing).
|
||||||
|
|
||||||
|
### 9.8 How to Exercise Your Rights
|
||||||
|
|
||||||
|
To exercise any of these rights, contact us at:
|
||||||
|
- Email: [CONTACT_EMAIL]
|
||||||
|
- Subject line: "GDPR Data Request - [Your Right]"
|
||||||
|
|
||||||
|
We will respond within **30 days** of receiving your request. We may ask for identification to verify your identity.
|
||||||
|
|
||||||
|
### 9.9 Right to Lodge a Complaint
|
||||||
|
|
||||||
|
If you believe we have violated your data protection rights, you have the right to lodge a complaint with a supervisory authority. Since our hosting is in Germany, you may contact:
|
||||||
|
|
||||||
|
**Landesbeauftragte für den Datenschutz und die Informationsfreiheit Baden-Württemberg**
|
||||||
|
Website: https://www.baden-wuerttemberg.datenschutz.de/
|
||||||
|
|
||||||
|
Or your local data protection authority.
|
||||||
|
|
||||||
|
## 10. Security Measures
|
||||||
|
|
||||||
|
We implement appropriate technical and organizational measures to protect your data:
|
||||||
|
|
||||||
|
- **Encryption**: All data transmitted via HTTPS/TLS
|
||||||
|
- **Access Controls**: Limited access to personal data
|
||||||
|
- **Infrastructure Security**: ISO 27001 certified data centers (netcup/Anexia)
|
||||||
|
- **Regular Updates**: Security patches and updates applied promptly
|
||||||
|
|
||||||
|
## 11. Children's Privacy
|
||||||
|
|
||||||
|
Our Site is not intended for children under 16 years of age. We do not knowingly collect personal data from children. If you believe we have collected data from a child, please contact us immediately.
|
||||||
|
|
||||||
|
## 12. Changes to This Privacy Policy
|
||||||
|
|
||||||
|
We may update this Privacy Policy from time to time. We will notify you of any material changes by:
|
||||||
|
- Posting the new policy on this page
|
||||||
|
- Updating the "Last Updated" date
|
||||||
|
- [Sending an email notification for significant changes]
|
||||||
|
|
||||||
|
## 13. Contact Us
|
||||||
|
|
||||||
|
If you have any questions about this Privacy Policy or our data practices, please contact us:
|
||||||
|
|
||||||
|
**Jeff Emmett**
|
||||||
|
Email: [CONTACT_EMAIL]
|
||||||
|
Website: [WEBSITE_URL]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A: Specific Processing Activities for [WEBSITE_NAME]
|
||||||
|
|
||||||
|
### Data Processing Summary
|
||||||
|
|
||||||
|
**Categories of Data Subjects:**
|
||||||
|
- [ ] Website visitors
|
||||||
|
- [ ] Newsletter subscribers
|
||||||
|
- [ ] Contact form users
|
||||||
|
- [ ] Customers/clients
|
||||||
|
- [ ] Other: _______________
|
||||||
|
|
||||||
|
**Categories of Personal Data:**
|
||||||
|
- [ ] Name
|
||||||
|
- [ ] Email address
|
||||||
|
- [ ] IP address (anonymized)
|
||||||
|
- [ ] Usage/analytics data
|
||||||
|
- [ ] Other: _______________
|
||||||
|
|
||||||
|
**Special Categories of Data (Art. 9):**
|
||||||
|
- [x] No special categories processed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This privacy policy template is provided for informational purposes. Consider consulting with a legal professional to ensure full compliance with applicable laws.*
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
# GDPR Compliance Kit
|
||||||
|
|
||||||
|
A comprehensive toolkit for making your websites GDPR compliant.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
```
|
||||||
|
gdpr-compliance-kit/
|
||||||
|
├── README.md # This file
|
||||||
|
├── PRIVACY_POLICY_TEMPLATE.md # Customizable privacy policy
|
||||||
|
├── NETCUP_DPA_ANNEX3_GUIDE.md # Guide for Netcup DPA form
|
||||||
|
├── RECORDS_OF_PROCESSING_ACTIVITIES.md # ROPA template (Art. 30)
|
||||||
|
└── components/
|
||||||
|
├── CookieConsent.tsx # React cookie banner component
|
||||||
|
└── GDPRAnalytics.tsx # Consent-aware analytics wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Add Cookie Consent to Your Site
|
||||||
|
|
||||||
|
Copy the components to your Next.js project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp components/CookieConsent.tsx your-project/components/
|
||||||
|
cp components/GDPRAnalytics.tsx your-project/components/
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to your `app/layout.tsx`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { CookieConsent } from "@/components/CookieConsent";
|
||||||
|
import { GDPRAnalytics } from "@/components/GDPRAnalytics";
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<CookieConsent />
|
||||||
|
<GDPRAnalytics />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remove** the standard Analytics import from your layout:
|
||||||
|
```diff
|
||||||
|
- import { Analytics } from "@vercel/analytics/next";
|
||||||
|
...
|
||||||
|
- <Analytics />
|
||||||
|
+ <GDPRAnalytics />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Your Privacy Policy
|
||||||
|
|
||||||
|
1. Copy `PRIVACY_POLICY_TEMPLATE.md` to your project
|
||||||
|
2. Replace all `[PLACEHOLDERS]` with your actual info:
|
||||||
|
- `[DATE]` - Current date
|
||||||
|
- `[WEBSITE_NAME]` - Your site name
|
||||||
|
- `[WEBSITE_URL]` - Your domain
|
||||||
|
- `[CONTACT_EMAIL]` - Your contact email
|
||||||
|
3. Review and customize based on your specific data processing
|
||||||
|
4. Add as a page at `/privacy` on your site
|
||||||
|
|
||||||
|
### 3. Complete Netcup DPA
|
||||||
|
|
||||||
|
1. Log into Netcup CCP
|
||||||
|
2. Go to Master Data → Agreement on contract data processing
|
||||||
|
3. Follow `NETCUP_DPA_ANNEX3_GUIDE.md` to fill out the form
|
||||||
|
4. Submit the agreement
|
||||||
|
5. Save a copy for your records
|
||||||
|
|
||||||
|
### 4. Maintain Records of Processing
|
||||||
|
|
||||||
|
1. Customize `RECORDS_OF_PROCESSING_ACTIVITIES.md` with your specific activities
|
||||||
|
2. Store it securely (not publicly accessible)
|
||||||
|
3. Review and update annually or when processing changes
|
||||||
|
|
||||||
|
## Component Details
|
||||||
|
|
||||||
|
### CookieConsent.tsx
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Three-tier consent (Necessary, Analytics, Preferences)
|
||||||
|
- Customizable appearance via Tailwind classes
|
||||||
|
- Stores consent in localStorage
|
||||||
|
- Provides hooks for checking consent status
|
||||||
|
|
||||||
|
**Custom styling:**
|
||||||
|
The component uses Tailwind CSS classes. Customize by editing the className props.
|
||||||
|
|
||||||
|
**Integration with other consent managers:**
|
||||||
|
The component stores consent in localStorage under `gdpr_consent`. You can read this from other parts of your app:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const consent = JSON.parse(localStorage.getItem('gdpr_consent') || '{}');
|
||||||
|
if (consent.analytics) {
|
||||||
|
// Load analytics
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GDPRAnalytics.tsx
|
||||||
|
|
||||||
|
Wraps Vercel Analytics and only loads it after consent:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Instead of:
|
||||||
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
<Analytics />
|
||||||
|
|
||||||
|
// Use:
|
||||||
|
import { GDPRAnalytics } from "@/components/GDPRAnalytics";
|
||||||
|
<GDPRAnalytics />
|
||||||
|
```
|
||||||
|
|
||||||
|
For Google Analytics, use `GDPRGoogleAnalytics`:
|
||||||
|
```tsx
|
||||||
|
<GDPRGoogleAnalytics measurementId="G-XXXXXXXXXX" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### ConsentGate Component
|
||||||
|
|
||||||
|
Conditionally render content based on consent:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ConsentGate } from "@/components/CookieConsent";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
return (
|
||||||
|
<ConsentGate type="analytics" fallback={<p>Analytics disabled</p>}>
|
||||||
|
<AnalyticsWidget />
|
||||||
|
</ConsentGate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
### Technical Implementation
|
||||||
|
- [ ] Cookie consent banner added to all sites
|
||||||
|
- [ ] Analytics wrapped with consent check
|
||||||
|
- [ ] Privacy policy page created at `/privacy`
|
||||||
|
- [ ] Cookie settings accessible from footer
|
||||||
|
- [ ] Contact forms have privacy notice
|
||||||
|
|
||||||
|
### Legal/Administrative
|
||||||
|
- [ ] Netcup DPA Annex 3 completed and submitted
|
||||||
|
- [ ] Records of Processing Activities created
|
||||||
|
- [ ] Privacy policy reviewed for accuracy
|
||||||
|
- [ ] Data retention periods documented
|
||||||
|
- [ ] Data breach response plan documented
|
||||||
|
|
||||||
|
### Ongoing Maintenance
|
||||||
|
- [ ] Annual privacy policy review scheduled
|
||||||
|
- [ ] ROPA review scheduled
|
||||||
|
- [ ] Processor agreements reviewed
|
||||||
|
- [ ] Staff awareness training (if applicable)
|
||||||
|
|
||||||
|
## Deployment Script
|
||||||
|
|
||||||
|
To add GDPR compliance to all your sites at once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# deploy-gdpr.sh
|
||||||
|
|
||||||
|
SITES=(
|
||||||
|
"mycofi-earth-website"
|
||||||
|
"canvas-website"
|
||||||
|
# Add more sites
|
||||||
|
)
|
||||||
|
|
||||||
|
for site in "${SITES[@]}"; do
|
||||||
|
echo "Adding GDPR components to $site..."
|
||||||
|
cp components/CookieConsent.tsx /opt/websites/$site/components/
|
||||||
|
cp components/GDPRAnalytics.tsx /opt/websites/$site/components/
|
||||||
|
echo "Done with $site"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
**Q: Do I need a cookie banner if I don't use cookies?**
|
||||||
|
A: If you use ANY analytics (even Vercel Analytics), you should have consent. Many analytics tools set cookies or use similar tracking technologies.
|
||||||
|
|
||||||
|
**Q: What about Cloudflare's cookies?**
|
||||||
|
A: Cloudflare sets some strictly necessary cookies for security (like `__cf_bm`). These don't require consent but should be mentioned in your privacy policy.
|
||||||
|
|
||||||
|
**Q: Do I need a DPO?**
|
||||||
|
A: Likely not. A Data Protection Officer is required only if:
|
||||||
|
- You're a public authority
|
||||||
|
- Your core activities involve large-scale monitoring
|
||||||
|
- You process special categories of data at large scale
|
||||||
|
|
||||||
|
**Q: What if someone requests data deletion?**
|
||||||
|
A: You have 30 days to respond. Delete from:
|
||||||
|
1. Your databases
|
||||||
|
2. Backups (when they rotate)
|
||||||
|
3. Notify any processors (they should delete too)
|
||||||
|
|
||||||
|
**Q: Is this enough for full GDPR compliance?**
|
||||||
|
A: This covers the major technical requirements. Full compliance also requires:
|
||||||
|
- Organizational measures (policies, training)
|
||||||
|
- Proper contracts with processors
|
||||||
|
- Breach response procedures
|
||||||
|
- Ongoing compliance monitoring
|
||||||
|
|
||||||
|
Consider consulting a legal professional for your specific situation.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [GDPR Full Text](https://gdpr-info.eu/)
|
||||||
|
- [ICO Guide to GDPR](https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/)
|
||||||
|
- [Netcup DPA Information](https://www.netcup.de/dsgvo-gdpr-processing)
|
||||||
|
- [Cloudflare GDPR](https://www.cloudflare.com/gdpr/introduction/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This kit is provided as-is for informational purposes. Not legal advice. Consult with a legal professional for your specific compliance needs.
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
# Records of Processing Activities (ROPA)
|
||||||
|
|
||||||
|
**Data Controller:** Jeff Emmett
|
||||||
|
**Last Updated:** [DATE]
|
||||||
|
**Version:** 1.0
|
||||||
|
|
||||||
|
This document fulfills the requirement under GDPR Article 30 to maintain records of processing activities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Item | Details |
|
||||||
|
|------|---------|
|
||||||
|
| **Controller Name** | Jeff Emmett |
|
||||||
|
| **Controller Address** | 23 Birchpark Dr, L3M 4M9 Grimsby, Canada |
|
||||||
|
| **Contact Email** | [YOUR_EMAIL] |
|
||||||
|
| **Data Protection Officer** | Not required (< 250 employees, no large-scale processing) |
|
||||||
|
| **EU Representative** | Not required (processing not regular/large-scale) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Processing Activity 1: Website Hosting & Analytics
|
||||||
|
|
||||||
|
| Field | Details |
|
||||||
|
|-------|---------|
|
||||||
|
| **Activity Name** | Website Hosting and Analytics |
|
||||||
|
| **Purpose** | Hosting websites, collecting anonymized usage analytics to improve user experience |
|
||||||
|
| **Legal Basis** | Legitimate Interest (Art. 6(1)(f)) for basic hosting; Consent (Art. 6(1)(a)) for analytics |
|
||||||
|
| **Data Subjects** | Website visitors |
|
||||||
|
| **Personal Data Categories** | IP address (anonymized), browser type, pages visited, referrer URL, device type |
|
||||||
|
| **Special Categories** | None |
|
||||||
|
| **Data Sources** | Direct collection via website |
|
||||||
|
| **Recipients** | netcup GmbH (hosting), Cloudflare Inc (CDN), Vercel Inc (analytics) |
|
||||||
|
| **Third Country Transfers** | USA (Cloudflare, Vercel) - protected by SCCs/DPA |
|
||||||
|
| **Retention Period** | Server logs: 14 days; Analytics: 14 months |
|
||||||
|
| **Security Measures** | TLS encryption, access controls, ISO 27001 certified infrastructure |
|
||||||
|
|
||||||
|
### Websites Covered:
|
||||||
|
- jeffemmett.com
|
||||||
|
- mycofi.earth
|
||||||
|
- bondingcurve.tech
|
||||||
|
- convictionvoting.xyz
|
||||||
|
- decolonizeti.me
|
||||||
|
- [Add all your domains]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Processing Activity 2: Newsletter Subscriptions
|
||||||
|
|
||||||
|
| Field | Details |
|
||||||
|
|-------|---------|
|
||||||
|
| **Activity Name** | Newsletter Management |
|
||||||
|
| **Purpose** | Sending newsletters and updates to subscribers |
|
||||||
|
| **Legal Basis** | Consent (Art. 6(1)(a)) - explicit opt-in |
|
||||||
|
| **Data Subjects** | Newsletter subscribers |
|
||||||
|
| **Personal Data Categories** | Email address, name (optional), subscription date, open/click tracking |
|
||||||
|
| **Special Categories** | None |
|
||||||
|
| **Data Sources** | Direct collection via subscription forms |
|
||||||
|
| **Recipients** | Self-hosted (Listmonk on netcup infrastructure) |
|
||||||
|
| **Third Country Transfers** | None (self-hosted in Germany) |
|
||||||
|
| **Retention Period** | Until unsubscribe + 30 days |
|
||||||
|
| **Security Measures** | TLS encryption, authentication required, database encryption |
|
||||||
|
|
||||||
|
### Consent Mechanism:
|
||||||
|
- Double opt-in required
|
||||||
|
- Clear unsubscribe link in every email
|
||||||
|
- Consent records stored with timestamp
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Processing Activity 3: Contact Form Submissions
|
||||||
|
|
||||||
|
| Field | Details |
|
||||||
|
|-------|---------|
|
||||||
|
| **Activity Name** | Contact Form Processing |
|
||||||
|
| **Purpose** | Responding to inquiries from website visitors |
|
||||||
|
| **Legal Basis** | Legitimate Interest (Art. 6(1)(f)) / Pre-contractual measures (Art. 6(1)(b)) |
|
||||||
|
| **Data Subjects** | People who submit contact forms |
|
||||||
|
| **Personal Data Categories** | Name, email address, message content |
|
||||||
|
| **Special Categories** | None |
|
||||||
|
| **Data Sources** | Direct submission via website forms |
|
||||||
|
| **Recipients** | Self-hosted email (or specify email provider) |
|
||||||
|
| **Third Country Transfers** | Depends on email provider |
|
||||||
|
| **Retention Period** | 2 years after last communication |
|
||||||
|
| **Security Measures** | TLS encryption, spam filtering |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Processing Activity 4: User Accounts (if applicable)
|
||||||
|
|
||||||
|
| Field | Details |
|
||||||
|
|-------|---------|
|
||||||
|
| **Activity Name** | User Account Management |
|
||||||
|
| **Purpose** | Providing authenticated access to services |
|
||||||
|
| **Legal Basis** | Contract performance (Art. 6(1)(b)) |
|
||||||
|
| **Data Subjects** | Registered users |
|
||||||
|
| **Personal Data Categories** | Email, username, hashed password, account settings |
|
||||||
|
| **Special Categories** | None |
|
||||||
|
| **Data Sources** | User registration |
|
||||||
|
| **Recipients** | Self-hosted only |
|
||||||
|
| **Third Country Transfers** | None |
|
||||||
|
| **Retention Period** | Account lifetime + 30 days after deletion request |
|
||||||
|
| **Security Measures** | Password hashing (bcrypt), session management, 2FA optional |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Processors (Sub-processors)
|
||||||
|
|
||||||
|
| Processor | Service | Location | DPA Signed | Contact |
|
||||||
|
|-----------|---------|----------|------------|---------|
|
||||||
|
| netcup GmbH | Web hosting infrastructure | Germany | Yes (online) | support@netcup.de |
|
||||||
|
| Cloudflare, Inc. | CDN, DNS, DDoS protection | USA (with EU options) | Yes (standard) | privacy@cloudflare.com |
|
||||||
|
| Vercel Inc. | Web analytics | USA | Yes (ToS) | privacy@vercel.com |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical and Organizational Measures (TOMs)
|
||||||
|
|
||||||
|
### Confidentiality
|
||||||
|
- [x] TLS/SSL encryption for all websites
|
||||||
|
- [x] Access controls for server infrastructure
|
||||||
|
- [x] SSH key authentication (no password auth)
|
||||||
|
- [x] Firewall and network segmentation
|
||||||
|
|
||||||
|
### Integrity
|
||||||
|
- [x] Regular backups
|
||||||
|
- [x] Version control for code
|
||||||
|
- [x] Audit logging
|
||||||
|
|
||||||
|
### Availability
|
||||||
|
- [x] Redundant infrastructure
|
||||||
|
- [x] DDoS protection (Cloudflare)
|
||||||
|
- [x] Monitoring and alerting
|
||||||
|
|
||||||
|
### Resilience
|
||||||
|
- [x] Disaster recovery procedures
|
||||||
|
- [x] Regular backup testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Subject Rights Procedures
|
||||||
|
|
||||||
|
### Access Requests (Art. 15)
|
||||||
|
1. Receive request via email
|
||||||
|
2. Verify identity
|
||||||
|
3. Compile data within 30 days
|
||||||
|
4. Provide data in machine-readable format
|
||||||
|
|
||||||
|
### Erasure Requests (Art. 17)
|
||||||
|
1. Receive request via email
|
||||||
|
2. Verify identity
|
||||||
|
3. Delete from: databases, backups (when rotated), analytics
|
||||||
|
4. Confirm deletion within 30 days
|
||||||
|
|
||||||
|
### Portability Requests (Art. 20)
|
||||||
|
1. Receive request via email
|
||||||
|
2. Verify identity
|
||||||
|
3. Export data as JSON/CSV
|
||||||
|
4. Provide within 30 days
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Breach Response Plan
|
||||||
|
|
||||||
|
### Detection
|
||||||
|
- Monitoring systems in place
|
||||||
|
- Log analysis for anomalies
|
||||||
|
|
||||||
|
### Assessment (within 24 hours)
|
||||||
|
1. Identify scope of breach
|
||||||
|
2. Assess risk to data subjects
|
||||||
|
3. Document findings
|
||||||
|
|
||||||
|
### Notification (within 72 hours if required)
|
||||||
|
1. Notify supervisory authority if risk to rights/freedoms
|
||||||
|
2. Notify affected individuals if high risk
|
||||||
|
3. Document all actions
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
1. Contain breach
|
||||||
|
2. Remediate vulnerabilities
|
||||||
|
3. Review and update security measures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Review Schedule
|
||||||
|
|
||||||
|
| Review Type | Frequency | Last Review | Next Review |
|
||||||
|
|-------------|-----------|-------------|-------------|
|
||||||
|
| ROPA Update | Annually | [DATE] | [DATE + 1 year] |
|
||||||
|
| Security Audit | Annually | [DATE] | [DATE + 1 year] |
|
||||||
|
| Processor Review | Annually | [DATE] | [DATE + 1 year] |
|
||||||
|
| Privacy Policy Review | Annually | [DATE] | [DATE + 1 year] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Change Log
|
||||||
|
|
||||||
|
| Date | Version | Changes | Author |
|
||||||
|
|------|---------|---------|--------|
|
||||||
|
| [DATE] | 1.0 | Initial creation | Jeff Emmett |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This document should be kept up to date and reviewed at least annually or whenever there are significant changes to processing activities.*
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
project_name: "GDPR Compliance Kit"
|
||||||
|
default_status: "To Do"
|
||||||
|
statuses: ["To Do", "In Progress", "Done"]
|
||||||
|
labels: []
|
||||||
|
milestones: []
|
||||||
|
date_format: yyyy-mm-dd
|
||||||
|
max_column_width: 20
|
||||||
|
default_editor: "nvim"
|
||||||
|
auto_open_browser: true
|
||||||
|
default_port: 6420
|
||||||
|
remote_operations: true
|
||||||
|
auto_commit: false
|
||||||
|
bypass_git_hooks: false
|
||||||
|
check_active_branches: true
|
||||||
|
active_branch_days: 30
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: task-high.1
|
||||||
|
title: Deploy GDPR compliance to all websites
|
||||||
|
status: To Do
|
||||||
|
assignee: []
|
||||||
|
created_date: '2025-12-11 18:04'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
parent_task_id: task-high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Deploy cookie consent banners, privacy policies, and GDPR-compliant analytics to all hosted websites. Complete Netcup DPA Annex 3 form.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 Cookie consent component deployed to all sites
|
||||||
|
- [ ] #2 Privacy policy pages created for each domain
|
||||||
|
- [ ] #3 Netcup DPA Annex 3 form completed and submitted
|
||||||
|
- [ ] #4 Analytics wrapped with consent checks
|
||||||
|
- [ ] #5 ROPA document stored securely
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type ConsentState = {
|
||||||
|
necessary: boolean;
|
||||||
|
analytics: boolean;
|
||||||
|
preferences: boolean;
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONSENT_COOKIE_NAME = "gdpr_consent";
|
||||||
|
const CONSENT_VERSION = "1.0";
|
||||||
|
|
||||||
|
export function CookieConsent() {
|
||||||
|
const [showBanner, setShowBanner] = useState(false);
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const [consent, setConsent] = useState<ConsentState>({
|
||||||
|
necessary: true, // Always required
|
||||||
|
analytics: false,
|
||||||
|
preferences: false,
|
||||||
|
timestamp: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if consent has already been given
|
||||||
|
const savedConsent = localStorage.getItem(CONSENT_COOKIE_NAME);
|
||||||
|
if (savedConsent) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedConsent);
|
||||||
|
setConsent(parsed);
|
||||||
|
// Apply saved consent settings
|
||||||
|
applyConsent(parsed);
|
||||||
|
} catch {
|
||||||
|
// Invalid consent data, show banner
|
||||||
|
setShowBanner(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No consent yet, show banner
|
||||||
|
setShowBanner(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyConsent = (consentState: ConsentState) => {
|
||||||
|
// Enable/disable analytics based on consent
|
||||||
|
if (consentState.analytics) {
|
||||||
|
// Enable analytics (e.g., Vercel Analytics, Plausible, etc.)
|
||||||
|
window.localStorage.setItem("va_disabled", "false");
|
||||||
|
// If using Google Analytics, you would enable it here
|
||||||
|
// window.gtag?.('consent', 'update', { analytics_storage: 'granted' });
|
||||||
|
} else {
|
||||||
|
// Disable analytics
|
||||||
|
window.localStorage.setItem("va_disabled", "true");
|
||||||
|
// window.gtag?.('consent', 'update', { analytics_storage: 'denied' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConsent = (consentState: ConsentState) => {
|
||||||
|
const consentWithTimestamp = {
|
||||||
|
...consentState,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
version: CONSENT_VERSION,
|
||||||
|
};
|
||||||
|
localStorage.setItem(CONSENT_COOKIE_NAME, JSON.stringify(consentWithTimestamp));
|
||||||
|
setConsent(consentWithTimestamp);
|
||||||
|
applyConsent(consentWithTimestamp);
|
||||||
|
setShowBanner(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptAll = () => {
|
||||||
|
saveConsent({
|
||||||
|
necessary: true,
|
||||||
|
analytics: true,
|
||||||
|
preferences: true,
|
||||||
|
timestamp: "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectAll = () => {
|
||||||
|
saveConsent({
|
||||||
|
necessary: true,
|
||||||
|
analytics: false,
|
||||||
|
preferences: false,
|
||||||
|
timestamp: "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCustom = () => {
|
||||||
|
saveConsent(consent);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!showBanner) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-background/95 backdrop-blur-sm border-t border-border shadow-lg"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Cookie consent"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
{!showDetails ? (
|
||||||
|
// Simple banner view
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-lg font-semibold mb-1">We value your privacy</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
We use cookies to improve your experience and analyze site usage.
|
||||||
|
You can choose which cookies to accept.{" "}
|
||||||
|
<Link href="/privacy" className="underline hover:text-foreground">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDetails(true)}
|
||||||
|
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
Customize
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={rejectAll}
|
||||||
|
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
Reject All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={acceptAll}
|
||||||
|
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
Accept All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Detailed settings view
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">Cookie Settings</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDetails(false)}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
aria-label="Close details"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
{/* Necessary Cookies */}
|
||||||
|
<div className="flex items-start justify-between p-3 bg-accent/50 rounded-lg">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-medium">Strictly Necessary</h3>
|
||||||
|
<span className="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded">Always Active</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Essential for the website to function properly. These cannot be disabled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={true}
|
||||||
|
disabled
|
||||||
|
className="mt-1 h-4 w-4 rounded border-border"
|
||||||
|
aria-label="Necessary cookies (always enabled)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Analytics Cookies */}
|
||||||
|
<div className="flex items-start justify-between p-3 border border-border rounded-lg">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-medium">Analytics</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Help us understand how visitors interact with our website by collecting anonymous usage data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={consent.analytics}
|
||||||
|
onChange={(e) => setConsent({ ...consent, analytics: e.target.checked })}
|
||||||
|
className="mt-1 h-4 w-4 rounded border-border cursor-pointer"
|
||||||
|
aria-label="Enable analytics cookies"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preference Cookies */}
|
||||||
|
<div className="flex items-start justify-between p-3 border border-border rounded-lg">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-medium">Preferences</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Remember your settings and preferences like theme choice and language.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={consent.preferences}
|
||||||
|
onChange={(e) => setConsent({ ...consent, preferences: e.target.checked })}
|
||||||
|
className="mt-1 h-4 w-4 rounded border-border cursor-pointer"
|
||||||
|
aria-label="Enable preference cookies"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
onClick={rejectAll}
|
||||||
|
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
Reject All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveCustom}
|
||||||
|
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
Save Preferences
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to check consent status from other components
|
||||||
|
export function useGDPRConsent() {
|
||||||
|
const [consent, setConsent] = useState<ConsentState | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConsent = localStorage.getItem(CONSENT_COOKIE_NAME);
|
||||||
|
if (savedConsent) {
|
||||||
|
try {
|
||||||
|
setConsent(JSON.parse(savedConsent));
|
||||||
|
} catch {
|
||||||
|
setConsent(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasConsented: consent !== null,
|
||||||
|
analyticsEnabled: consent?.analytics ?? false,
|
||||||
|
preferencesEnabled: consent?.preferences ?? false,
|
||||||
|
resetConsent: () => {
|
||||||
|
localStorage.removeItem(CONSENT_COOKIE_NAME);
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component to conditionally render based on consent
|
||||||
|
export function ConsentGate({
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
fallback,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
type: "analytics" | "preferences";
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { analyticsEnabled, preferencesEnabled } = useGDPRConsent();
|
||||||
|
|
||||||
|
const hasConsent = type === "analytics" ? analyticsEnabled : preferencesEnabled;
|
||||||
|
|
||||||
|
if (!hasConsent) {
|
||||||
|
return fallback || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
|
const CONSENT_COOKIE_NAME = "gdpr_consent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GDPR-Compliant Analytics Wrapper
|
||||||
|
*
|
||||||
|
* This component wraps Vercel Analytics (or any analytics provider)
|
||||||
|
* and only loads it if the user has consented to analytics cookies.
|
||||||
|
*
|
||||||
|
* Usage in your layout.tsx:
|
||||||
|
*
|
||||||
|
* import { GDPRAnalytics } from "@/components/GDPRAnalytics";
|
||||||
|
*
|
||||||
|
* export default function RootLayout({ children }) {
|
||||||
|
* return (
|
||||||
|
* <html>
|
||||||
|
* <body>
|
||||||
|
* {children}
|
||||||
|
* <GDPRAnalytics />
|
||||||
|
* </body>
|
||||||
|
* </html>
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function GDPRAnalytics() {
|
||||||
|
const [analyticsEnabled, setAnalyticsEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkConsent = () => {
|
||||||
|
const savedConsent = localStorage.getItem(CONSENT_COOKIE_NAME);
|
||||||
|
if (savedConsent) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedConsent);
|
||||||
|
setAnalyticsEnabled(parsed.analytics === true);
|
||||||
|
} catch {
|
||||||
|
setAnalyticsEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check on mount
|
||||||
|
checkConsent();
|
||||||
|
|
||||||
|
// Listen for consent changes
|
||||||
|
const handleStorageChange = (e: StorageEvent) => {
|
||||||
|
if (e.key === CONSENT_COOKIE_NAME) {
|
||||||
|
checkConsent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("storage", handleStorageChange);
|
||||||
|
return () => window.removeEventListener("storage", handleStorageChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Only render Analytics if user has consented
|
||||||
|
if (!analyticsEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Analytics />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative: Google Analytics 4 GDPR wrapper
|
||||||
|
*
|
||||||
|
* If you use Google Analytics instead of Vercel Analytics,
|
||||||
|
* use this component.
|
||||||
|
*/
|
||||||
|
export function GDPRGoogleAnalytics({ measurementId }: { measurementId: string }) {
|
||||||
|
const [analyticsEnabled, setAnalyticsEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConsent = localStorage.getItem(CONSENT_COOKIE_NAME);
|
||||||
|
if (savedConsent) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedConsent);
|
||||||
|
setAnalyticsEnabled(parsed.analytics === true);
|
||||||
|
} catch {
|
||||||
|
setAnalyticsEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!analyticsEnabled) return;
|
||||||
|
|
||||||
|
// Load Google Analytics script dynamically
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
|
||||||
|
script.async = true;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
// Initialize gtag
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(...args: unknown[]) {
|
||||||
|
window.dataLayer.push(args);
|
||||||
|
}
|
||||||
|
gtag("js", new Date());
|
||||||
|
gtag("config", measurementId, {
|
||||||
|
anonymize_ip: true, // GDPR requirement
|
||||||
|
cookie_flags: "SameSite=None;Secure",
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.head.removeChild(script);
|
||||||
|
};
|
||||||
|
}, [analyticsEnabled, measurementId]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type declaration for Google Analytics
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
dataLayer: unknown[];
|
||||||
|
gtag?: (...args: unknown[]) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue