initial project setup
Fastify + Prisma backend, React + Vite frontend, Docker deployment. Multi-board feedback platform with anonymous cookie auth, passkey upgrade path, ALTCHA spam protection, plugin system, and full privacy-first architecture.
This commit is contained in:
163
packages/web/src/pages/PrivacyPage.tsx
Normal file
163
packages/web/src/pages/PrivacyPage.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { api } from '../lib/api'
|
||||
|
||||
interface DataField {
|
||||
field: string
|
||||
purpose: string
|
||||
retention: string
|
||||
deletable: boolean
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
fields: DataField[]
|
||||
cookieInfo: string
|
||||
dataLocation: string
|
||||
thirdParties: string[]
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
const [manifest, setManifest] = useState<Manifest | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
api.get<Manifest>('/privacy/data-manifest')
|
||||
.then(setManifest)
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-4 py-8">
|
||||
<h1
|
||||
className="text-2xl font-bold mb-2"
|
||||
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
|
||||
>
|
||||
Privacy
|
||||
</h1>
|
||||
<p className="text-sm mb-8" style={{ color: 'var(--text-secondary)' }}>
|
||||
Here is exactly what data this Echoboard instance collects and why.
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div
|
||||
className="w-6 h-6 border-2 rounded-full"
|
||||
style={{ borderColor: 'var(--border)', borderTopColor: 'var(--accent)', animation: 'spin 0.6s linear infinite' }}
|
||||
/>
|
||||
</div>
|
||||
) : manifest ? (
|
||||
<>
|
||||
{/* Quick summary */}
|
||||
<div className="card p-5 mb-6">
|
||||
<h2
|
||||
className="text-base font-semibold mb-3"
|
||||
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
|
||||
>
|
||||
The short version
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
No tracking scripts, no analytics, no third-party cookies
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
All data stays on this server - {manifest.dataLocation}
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
No external fonts or resources loaded
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--success)" strokeWidth={2.5} className="shrink-0 mt-0.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
You can delete everything at any time
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Cookie info */}
|
||||
<div className="card p-5 mb-6">
|
||||
<h2
|
||||
className="text-base font-semibold mb-2"
|
||||
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
|
||||
>
|
||||
Cookies
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
{manifest.cookieInfo}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Data fields */}
|
||||
<div className="card p-5 mb-6">
|
||||
<h2
|
||||
className="text-base font-semibold mb-4"
|
||||
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
|
||||
>
|
||||
What we store
|
||||
</h2>
|
||||
<div className="flex flex-col gap-3">
|
||||
{manifest.fields.map((f) => (
|
||||
<div
|
||||
key={f.field}
|
||||
className="p-3 rounded-lg"
|
||||
style={{ background: 'var(--bg)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--text)' }}>
|
||||
{f.field}
|
||||
</span>
|
||||
{f.deletable && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(34, 197, 94, 0.15)', color: 'var(--success)' }}>
|
||||
deletable
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs mb-1" style={{ color: 'var(--text-secondary)' }}>
|
||||
{f.purpose}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>
|
||||
Retained: {f.retention}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Third parties */}
|
||||
{manifest.thirdParties.length > 0 && (
|
||||
<div className="card p-5 mb-6">
|
||||
<h2
|
||||
className="text-base font-semibold mb-2"
|
||||
style={{ fontFamily: 'var(--font-heading)', color: 'var(--text)' }}
|
||||
>
|
||||
Third parties
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-1">
|
||||
{manifest.thirdParties.map((tp) => (
|
||||
<li key={tp} className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
- {tp}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="card p-5">
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
This Echoboard instance is self-hosted and privacy-focused. No tracking, no external services, no data sharing. Your identity is cookie-based unless you register a passkey. You can delete all your data at any time from the settings page.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user