Skip to content

Commit 200d084

Browse files
feat: add legal page and link in footer
- Implement LegalPage component with legal notice and privacy policy - Add link to legal page in footer - Update router to include legal route
1 parent 8b6b4f9 commit 200d084

4 files changed

Lines changed: 308 additions & 1 deletion

File tree

app/src/components/Footer.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Box from '@mui/material/Box';
22
import Link from '@mui/material/Link';
3+
import { Link as RouterLink } from 'react-router-dom';
34
import { GITHUB_URL } from '../constants';
45

56
interface FooterProps {
@@ -63,6 +64,18 @@ export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterPr
6364
>
6465
stats
6566
</Link>
67+
<span>·</span>
68+
<Link
69+
component={RouterLink}
70+
to="/legal"
71+
sx={{
72+
color: '#9ca3af',
73+
textDecoration: 'none',
74+
'&:hover': { color: '#6b7280' },
75+
}}
76+
>
77+
legal
78+
</Link>
6679
</Box>
6780
</Box>
6881
);

app/src/hooks/useUrlSync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { useEffect } from 'react';
88

9-
import type { FilterCategory, ActiveFilters } from '../types';
9+
import type { ActiveFilters } from '../types';
1010
import { FILTER_CATEGORIES } from '../types';
1111

1212
/**

app/src/pages/LegalPage.tsx

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import { useEffect } from 'react';
2+
import { Helmet } from 'react-helmet-async';
3+
import Box from '@mui/material/Box';
4+
import Typography from '@mui/material/Typography';
5+
import Link from '@mui/material/Link';
6+
import Paper from '@mui/material/Paper';
7+
import Table from '@mui/material/Table';
8+
import TableBody from '@mui/material/TableBody';
9+
import TableCell from '@mui/material/TableCell';
10+
import TableRow from '@mui/material/TableRow';
11+
12+
import { useAnalytics } from '../hooks';
13+
import { Breadcrumb, Footer } from '../components';
14+
import { GITHUB_URL } from '../constants';
15+
16+
export function LegalPage() {
17+
const { trackPageview, trackEvent } = useAnalytics();
18+
19+
useEffect(() => {
20+
trackPageview('/legal');
21+
}, [trackPageview]);
22+
23+
const headingStyle = {
24+
fontFamily: '"MonoLisa", monospace',
25+
fontWeight: 600,
26+
fontSize: '1rem',
27+
color: '#1f2937',
28+
mb: 2,
29+
};
30+
31+
const subheadingStyle = {
32+
fontFamily: '"MonoLisa", monospace',
33+
fontWeight: 600,
34+
fontSize: '1.1rem',
35+
color: '#374151',
36+
mt: 3,
37+
mb: 1,
38+
};
39+
40+
const textStyle = {
41+
fontFamily: '"MonoLisa", monospace',
42+
fontSize: '0.9rem',
43+
color: '#4b5563',
44+
lineHeight: 1.8,
45+
mb: 2,
46+
};
47+
48+
const tableStyle = {
49+
'& .MuiTableCell-root': {
50+
fontFamily: '"MonoLisa", monospace',
51+
fontSize: '0.85rem',
52+
color: '#4b5563',
53+
borderBottom: '1px solid #f3f4f6',
54+
py: 1.5,
55+
px: 2,
56+
},
57+
'& .MuiTableCell-root:first-of-type': {
58+
fontWeight: 500,
59+
color: '#374151',
60+
width: '40%',
61+
},
62+
};
63+
64+
return (
65+
<>
66+
<Helmet>
67+
<title>legal | pyplots.ai</title>
68+
<meta name="description" content="Legal notice, privacy policy, and transparency information for pyplots.ai" />
69+
<meta property="og:title" content="legal | pyplots.ai" />
70+
<meta property="og:description" content="Legal notice, privacy policy, and transparency information" />
71+
</Helmet>
72+
73+
<Breadcrumb items={[{ label: 'pyplots.ai', to: '/' }, { label: 'legal' }]} sx={{ mb: 4 }} />
74+
75+
<Box sx={{ pb: 4, maxWidth: 1100, mx: 'auto' }}>
76+
{/* Legal Notice */}
77+
<Paper component="section" id="legal-notice" sx={{ p: 3, mb: 2 }}>
78+
<Typography variant="h2" sx={headingStyle}>
79+
Legal Notice
80+
</Typography>
81+
82+
<Typography sx={textStyle}>
83+
<strong>Operator</strong>
84+
<br />
85+
Markus Neusinger
86+
<br />
87+
Visp, Switzerland
88+
</Typography>
89+
90+
<Typography sx={textStyle}>
91+
<strong>Contact</strong>
92+
<br />
93+
Email:{' '}
94+
<Link href="mailto:admin@pyplots.ai" sx={{ color: '#3776AB' }}>
95+
admin@pyplots.ai
96+
</Link>
97+
</Typography>
98+
99+
<Typography sx={textStyle}>
100+
<strong>Disclaimer</strong>
101+
<br />
102+
This is a personal portfolio project, not a commercial service. The content is provided &quot;as is&quot;
103+
without warranty of any kind. Code examples are for educational purposes and should be reviewed before use
104+
in production environments.
105+
</Typography>
106+
</Paper>
107+
108+
{/* Privacy Policy */}
109+
<Paper component="section" id="privacy" sx={{ p: 3, mb: 2 }}>
110+
<Typography variant="h2" sx={headingStyle}>
111+
Privacy Policy
112+
</Typography>
113+
114+
<Typography sx={subheadingStyle}>Data Controller</Typography>
115+
<Typography sx={textStyle}>Markus Neusinger (see Legal Notice above)</Typography>
116+
117+
<Typography sx={subheadingStyle}>What We Collect</Typography>
118+
<Typography sx={textStyle}>
119+
<strong>Anonymized Analytics</strong>: We use Plausible Analytics, a privacy-focused analytics tool. It
120+
collects no personal data, uses no cookies, and does not track you across websites. All data is aggregated
121+
and anonymous.
122+
</Typography>
123+
<Typography sx={textStyle}>
124+
<strong>Server Logs</strong>: Temporary technical logs (anonymized IP addresses) are retained for up to 30
125+
days for security and debugging purposes.
126+
</Typography>
127+
128+
<Typography sx={subheadingStyle}>What We Do NOT Collect</Typography>
129+
<Typography sx={textStyle}>
130+
• No user accounts or personal profiles
131+
<br />
132+
• No personal data (names, emails, etc.)
133+
<br />
134+
• No cookies (except technically necessary session cookies)
135+
<br /><strong>No AI training</strong>: Your interactions are not used to train AI models
136+
</Typography>
137+
138+
<Typography sx={textStyle}>
139+
<strong>Analytics</strong>: Filter selections and search terms are tracked anonymously via Plausible
140+
Analytics for usage statistics. This data is aggregated and cannot be linked to individual users.
141+
</Typography>
142+
143+
<Typography sx={textStyle}>
144+
<strong>Contributors</strong>: If you suggest a plot type via GitHub, your GitHub username may be credited
145+
in the specification metadata. This is public information from your GitHub profile.
146+
</Typography>
147+
148+
<Typography sx={subheadingStyle}>Hosting &amp; Third Parties</Typography>
149+
<Typography sx={textStyle}>All services are hosted in the EU (Netherlands, europe-west4):</Typography>
150+
<Table sx={tableStyle}>
151+
<TableBody>
152+
<TableRow>
153+
<TableCell>Hosting</TableCell>
154+
<TableCell>Google Cloud Run (Netherlands)</TableCell>
155+
</TableRow>
156+
<TableRow>
157+
<TableCell>Database</TableCell>
158+
<TableCell>Google Cloud SQL (Netherlands)</TableCell>
159+
</TableRow>
160+
<TableRow>
161+
<TableCell>Storage</TableCell>
162+
<TableCell>Google Cloud Storage (Netherlands)</TableCell>
163+
</TableRow>
164+
<TableRow>
165+
<TableCell>Analytics</TableCell>
166+
<TableCell>Plausible Analytics (EU, proxied)</TableCell>
167+
</TableRow>
168+
</TableBody>
169+
</Table>
170+
171+
<Typography sx={subheadingStyle}>Your Rights</Typography>
172+
<Typography sx={textStyle}>
173+
Under GDPR and Swiss DSG, you have the right to access, rectification, erasure, and data portability. Since
174+
we do not store personal data, there is typically nothing to delete or export. For questions, contact{' '}
175+
<Link href="mailto:admin@pyplots.ai" sx={{ color: '#3776AB' }}>
176+
admin@pyplots.ai
177+
</Link>
178+
.
179+
</Typography>
180+
</Paper>
181+
182+
{/* Transparency */}
183+
<Paper component="section" id="transparency" sx={{ p: 3, mb: 2 }}>
184+
<Typography variant="h2" sx={headingStyle}>
185+
Transparency
186+
</Typography>
187+
188+
<Typography sx={textStyle}>
189+
This project is open source and committed to full transparency about how it works and what it costs.
190+
</Typography>
191+
192+
<Typography sx={subheadingStyle}>Technology Stack</Typography>
193+
<Table sx={tableStyle}>
194+
<TableBody>
195+
<TableRow>
196+
<TableCell>Typography</TableCell>
197+
<TableCell>
198+
<Link href="https://www.monolisa.dev/" target="_blank" rel="noopener" sx={{ color: '#3776AB' }}>
199+
MonoLisa
200+
</Link>{' '}
201+
by Marcus Sterz
202+
</TableCell>
203+
</TableRow>
204+
<TableRow>
205+
<TableCell>Frontend</TableCell>
206+
<TableCell>React 19, Vite, MUI 7, TypeScript</TableCell>
207+
</TableRow>
208+
<TableRow>
209+
<TableCell>Backend</TableCell>
210+
<TableCell>Python 3.13, FastAPI, SQLAlchemy</TableCell>
211+
</TableRow>
212+
<TableRow>
213+
<TableCell>Database</TableCell>
214+
<TableCell>PostgreSQL</TableCell>
215+
</TableRow>
216+
<TableRow>
217+
<TableCell>Hosting</TableCell>
218+
<TableCell>Google Cloud Run (Netherlands)</TableCell>
219+
</TableRow>
220+
<TableRow>
221+
<TableCell>Storage</TableCell>
222+
<TableCell>Google Cloud Storage</TableCell>
223+
</TableRow>
224+
<TableRow>
225+
<TableCell>Analytics</TableCell>
226+
<TableCell>Plausible (privacy-friendly, no cookies)</TableCell>
227+
</TableRow>
228+
<TableRow>
229+
<TableCell>AI</TableCell>
230+
<TableCell>Anthropic Claude via Claude Max (code generation &amp; review)</TableCell>
231+
</TableRow>
232+
</TableBody>
233+
</Table>
234+
235+
<Typography sx={subheadingStyle}>Source Code</Typography>
236+
<Typography sx={textStyle}>
237+
The entire codebase is publicly available under the MIT License:
238+
<br />
239+
<Link href={GITHUB_URL} target="_blank" rel="noopener" sx={{ color: '#3776AB' }}>
240+
github.com/MarkusNeusinger/pyplots
241+
</Link>
242+
</Typography>
243+
244+
<Typography sx={subheadingStyle}>Monthly Costs (approximate)</Typography>
245+
<Table sx={tableStyle}>
246+
<TableBody>
247+
<TableRow>
248+
<TableCell>Cloud Run</TableCell>
249+
<TableCell>~$15</TableCell>
250+
</TableRow>
251+
<TableRow>
252+
<TableCell>Cloud SQL</TableCell>
253+
<TableCell>~$40</TableCell>
254+
</TableRow>
255+
<TableRow>
256+
<TableCell>Cloud Storage</TableCell>
257+
<TableCell>~$5</TableCell>
258+
</TableRow>
259+
<TableRow>
260+
<TableCell>Plausible Analytics</TableCell>
261+
<TableCell>~$9</TableCell>
262+
</TableRow>
263+
<TableRow>
264+
<TableCell>Domain (.ai)</TableCell>
265+
<TableCell>~$8</TableCell>
266+
</TableRow>
267+
<TableRow>
268+
<TableCell>Claude Max</TableCell>
269+
<TableCell>~$100 (shared)</TableCell>
270+
</TableRow>
271+
<TableRow>
272+
<TableCell>
273+
<strong>Total</strong>
274+
</TableCell>
275+
<TableCell>
276+
<strong>~$177/month</strong>
277+
</TableCell>
278+
</TableRow>
279+
</TableBody>
280+
</Table>
281+
<Typography sx={{ ...textStyle, fontSize: '0.8rem', color: '#9ca3af', mt: 1 }}>
282+
Claude Max subscription is shared across projects.
283+
<br />
284+
All costs are currently covered privately. Last updated: January 2026.
285+
</Typography>
286+
</Paper>
287+
</Box>
288+
289+
<Footer onTrackEvent={trackEvent} />
290+
</>
291+
);
292+
}

app/src/router.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SpecPage } from './pages/SpecPage';
77
import { CatalogPage } from './pages/CatalogPage';
88
import { InteractivePage } from './pages/InteractivePage';
99
import { DebugPage } from './pages/DebugPage';
10+
import { LegalPage } from './pages/LegalPage';
1011

1112
const router = createBrowserRouter([
1213
{
@@ -15,6 +16,7 @@ const router = createBrowserRouter([
1516
children: [
1617
{ index: true, element: <HomePage /> },
1718
{ path: 'catalog', element: <CatalogPage /> },
19+
{ path: 'legal', element: <LegalPage /> },
1820
{ path: ':specId', element: <SpecPage /> },
1921
{ path: ':specId/:library', element: <SpecPage /> },
2022
],

0 commit comments

Comments
 (0)