Skip to content

Commit e5c15ad

Browse files
authored
feat: add legal pages, routing, and responsive navigation (#4)
Add TOS/Privacy pages, routing, and responsive nav/typography
1 parent 0a41e46 commit e5c15ad

11 files changed

Lines changed: 511 additions & 98 deletions

File tree

backend/src/services/privacyLabel.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,26 @@ function buildLabel(perms) {
2929
}
3030
});
3131

32-
return {
33-
data_categories: Array.from(used).map((key) => ({
32+
const orderedCategories = Array.from(used)
33+
.map((key) => ({
3434
key,
3535
...categories[key],
36-
})),
36+
}))
37+
.sort((a, b) => {
38+
if (a.key === "unknown") return 1;
39+
if (b.key === "unknown") return -1;
40+
41+
const order = {
42+
high: 0,
43+
moderate: 1,
44+
low: 2,
45+
};
46+
47+
return (order[a.sensitivity] ?? 99) - (order[b.sensitivity] ?? 99);
48+
});
49+
50+
return {
51+
data_categories: orderedCategories,
3752
overall_sensitivity: sensitivity,
3853
permissions: perms,
3954
notes: [

frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
},
1313
"dependencies": {
1414
"react": "^19.2.0",
15-
"react-dom": "^19.2.0"
15+
"react-dom": "^19.2.0",
16+
"react-router-dom": "^6.30.1"
1617
},
1718
"devDependencies": {
1819
"@eslint/js": "^9.39.1",

frontend/src/App.jsx

Lines changed: 17 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,23 @@
1-
import { useState } from "react";
2-
import { fetchLabel } from "./api/permlens";
3-
import AppHeader from "./components/AppHeader";
4-
import AppSearch from "./components/AppSearch";
5-
import LabelView from "./components/LabelView";
6-
import ErrorState from "./components/ErrorState";
1+
import { BrowserRouter, Routes, Route } from "react-router-dom";
2+
import TopNav from "./components/TopNav";
3+
import Home from "./pages/Home";
4+
import TermsOfService from "./pages/TermsOfService";
5+
import PrivacyPolicy from "./pages/PrivacyPolicy";
76

87
function App() {
9-
const [label, setLabel] = useState(null);
10-
const [meta, setMeta] = useState(null);
11-
const [error, setError] = useState(null);
12-
const [loading, setLoading] = useState(false);
13-
const [slug, setSlug] = useState(null);
14-
15-
async function handleSearch(slug) {
16-
const clean = slug.trim();
17-
setSlug(clean);
18-
19-
if (!clean) {
20-
setLabel(null);
21-
setMeta(null);
22-
setError("Enter a GitHub App slug to inspect.");
23-
return;
24-
}
25-
26-
setError(null);
27-
setLabel(null);
28-
setMeta(null);
29-
setLoading(true);
30-
31-
const start = Date.now();
32-
33-
try {
34-
const data = await fetchLabel(clean);
35-
36-
setLabel(data.label || null);
37-
setMeta({
38-
resolved: data.resolved,
39-
fallback: data.fallback,
40-
rateLimits: data.rate_limits,
41-
api: data.api,
42-
cache: data.cache,
43-
app: data.app,
44-
});
45-
} catch (err) {
46-
setError(err.message);
47-
} finally {
48-
const elapsed = Date.now() - start;
49-
if (elapsed < 800) {
50-
await new Promise((r) => setTimeout(r, 800 - elapsed));
51-
}
52-
setLoading(false);
53-
}
54-
}
55-
568
return (
57-
<main className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white">
58-
<div className="mx-auto max-w-3xl px-6 py-16">
59-
<AppHeader />
60-
61-
<AppSearch onSubmit={handleSearch} loading={loading} />
62-
63-
{loading && (
64-
<div className="mt-4 text-sm text-blue-300">
65-
Loading privacy label…
66-
</div>
67-
)}
68-
69-
{error && <ErrorState message={error} />}
70-
71-
{meta?.fallback && !meta?.rateLimits?.authenticated && (
72-
<div className="mt-4 rounded bg-yellow-500/10 px-4 py-2 text-xs text-yellow-300">
73-
App could not be resolved! Showing permissions for the PermLens GitHub App instead.
74-
</div>
75-
)}
76-
77-
{meta?.rateLimits?.unauthenticated &&
78-
meta?.rateLimits?.authenticated && (
79-
<div className="mt-4 rounded bg-red-500/10 px-4 py-2 text-xs text-red-300">
80-
GitHub API rate limit exceeded! Please try again later.
81-
</div>
82-
)}
83-
84-
{label && <LabelView label={label} meta={meta} slug={slug} />}
85-
</div>
86-
</main>
9+
<BrowserRouter>
10+
<main className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white">
11+
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-10">
12+
<TopNav />
13+
<Routes>
14+
<Route path="/" element={<Home />} />
15+
<Route path="/tos" element={<TermsOfService />} />
16+
<Route path="/privacy" element={<PrivacyPolicy />} />
17+
</Routes>
18+
</div>
19+
</main>
20+
</BrowserRouter>
8721
);
8822
}
8923

frontend/src/components/AppHeader.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
function AppHeader() {
22
return (
33
<header>
4-
<h1 className="text-3xl font-semibold">PermLens</h1>
5-
<p className="text-gray-300 mt-1">
4+
<h1 className="text-2xl font-semibold sm:text-3xl">PermLens</h1>
5+
<p className="mt-1 text-sm text-gray-300 sm:text-base">
66
Permission transparency for GitHub Apps.
77
</p>
88
</header>

frontend/src/components/AppSearch.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ function AppSearch({ onSubmit, loading }) {
99
e.preventDefault();
1010
onSubmit(value);
1111
}}
12-
className="mt-6 flex gap-2"
12+
className="mt-6 flex flex-col gap-2 sm:flex-row"
1313
>
1414
<input
1515
type="text"
1616
placeholder="GitHub App slug (e.g. gitguardian)"
1717
value={value}
1818
onChange={(e) => setValue(e.target.value)}
19-
className="flex-1 rounded bg-white/10 px-3 py-2 text-sm text-white placeholder:text-gray-400 focus:outline-none focus:ring"
19+
className="w-full flex-1 rounded bg-white/10 px-3 py-2 text-sm text-white placeholder:text-gray-400 focus:outline-none focus:ring"
2020
/>
2121
<button
2222
type="submit"
2323
disabled={loading}
24-
className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium hover:bg-indigo-500 disabled:opacity-50"
24+
className="w-full rounded bg-indigo-600 px-4 py-2 text-sm font-medium hover:bg-indigo-500 disabled:opacity-50 sm:w-auto"
2525
>
2626
{loading ? "Inspecting…" : "Inspect"}
2727
</button>

frontend/src/components/LabelView.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ function LabelView({ label, meta, slug }) {
1717
}
1818

1919
return (
20-
<section className="relative mt-6 rounded-xl bg-white/5 p-6">
20+
<section className="relative mt-6 rounded-xl bg-white/5 p-4 sm:p-6">
2121
{slug && (
22-
<div className="absolute right-4 top-6 flex flex-col items-end gap-3">
22+
<div className="mb-4 flex flex-col items-start gap-3 sm:absolute sm:right-4 sm:top-6 sm:mb-0 sm:items-end [@media(max-height:700px)]:absolute [@media(max-height:700px)]:right-4 [@media(max-height:700px)]:top-6 [@media(max-height:700px)]:mb-0 [@media(max-height:700px)]:items-end">
2323
<div className="flex items-start gap-2">
2424
{meta?.cache?.hit && (
2525
<div className="group relative">
@@ -54,7 +54,7 @@ function LabelView({ label, meta, slug }) {
5454
)}
5555

5656
<header className="mb-6">
57-
<h2 className="text-xl font-semibold">Privacy Label</h2>
57+
<h2 className="text-lg font-semibold sm:text-xl">Privacy Label</h2>
5858
<div
5959
className={`mt-2 inline-flex items-center rounded px-3 py-1 text-sm font-medium ${styles}`}
6060
>
@@ -66,7 +66,7 @@ function LabelView({ label, meta, slug }) {
6666
<h3 className="mb-2 text-sm font-semibold uppercase tracking-wide text-gray-400">
6767
Data categories
6868
</h3>
69-
<ul className="space-y-3">
69+
<ul className="grid grid-cols-1 gap-3 sm:grid-cols-2 [@media(max-height:700px)]:grid-cols-2">
7070
{label.data_categories.map((cat) => (
7171
<li key={cat.key} className="rounded bg-black/30 p-3">
7272
<div className="flex items-center justify-between">
@@ -87,7 +87,7 @@ function LabelView({ label, meta, slug }) {
8787
<h3 className="mb-2 text-sm font-semibold uppercase tracking-wide text-gray-400">
8888
Declared permissions
8989
</h3>
90-
<ul className="grid grid-cols-2 gap-2 text-xs">
90+
<ul className="grid grid-cols-1 gap-2 text-xs sm:grid-cols-2 [@media(max-height:700px)]:grid-cols-2">
9191
{label.permissions.map((p) => (
9292
<li
9393
key={p.name}

frontend/src/components/TopNav.jsx

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { useState } from "react";
2+
import { Link, NavLink } from "react-router-dom";
3+
4+
function TopNav() {
5+
const [menuOpen, setMenuOpen] = useState(false);
6+
7+
const closeMenu = () => setMenuOpen(false);
8+
9+
return (
10+
<nav className="relative">
11+
<div className="flex items-center justify-between">
12+
<div className="flex items-center gap-3">
13+
<button
14+
type="button"
15+
onClick={() => setMenuOpen((open) => !open)}
16+
className="rounded p-2 text-gray-200 hover:bg-white/10 md:hidden"
17+
aria-label="Open menu"
18+
aria-expanded={menuOpen}
19+
aria-controls="legal-menu"
20+
>
21+
<svg
22+
viewBox="0 0 24 24"
23+
fill="none"
24+
stroke="currentColor"
25+
strokeWidth="2"
26+
strokeLinecap="round"
27+
strokeLinejoin="round"
28+
className="h-5 w-5"
29+
>
30+
<line x1="3" y1="6" x2="21" y2="6" />
31+
<line x1="3" y1="12" x2="21" y2="12" />
32+
<line x1="3" y1="18" x2="21" y2="18" />
33+
</svg>
34+
</button>
35+
36+
<Link
37+
to="/"
38+
onClick={closeMenu}
39+
className="hidden text-sm font-semibold uppercase tracking-wide text-gray-200 hover:text-white md:inline"
40+
>
41+
Home
42+
</Link>
43+
</div>
44+
45+
<div className="flex items-center gap-3">
46+
<Link
47+
to="/"
48+
onClick={closeMenu}
49+
className="text-sm font-semibold uppercase tracking-wide text-gray-200 hover:text-white md:hidden"
50+
>
51+
Home
52+
</Link>
53+
<div className="hidden items-center gap-3 text-sm font-semibold uppercase tracking-wide text-gray-300 md:flex">
54+
<NavLink
55+
to="/tos"
56+
className={({ isActive }) =>
57+
`rounded px-2 py-1 hover:text-white ${
58+
isActive ? "bg-white/10 text-white" : ""
59+
}`
60+
}
61+
>
62+
Terms of Service
63+
</NavLink>
64+
<NavLink
65+
to="/privacy"
66+
className={({ isActive }) =>
67+
`rounded px-2 py-1 hover:text-white ${
68+
isActive ? "bg-white/10 text-white" : ""
69+
}`
70+
}
71+
>
72+
Privacy Policy
73+
</NavLink>
74+
</div>
75+
</div>
76+
</div>
77+
78+
<div
79+
className={`fixed inset-0 z-40 bg-black/60 transition-opacity duration-300 md:hidden ${
80+
menuOpen ? "opacity-100" : "pointer-events-none opacity-0"
81+
}`}
82+
onClick={closeMenu}
83+
aria-hidden={!menuOpen}
84+
/>
85+
<aside
86+
id="legal-menu"
87+
className={`fixed inset-y-0 left-0 z-50 w-64 bg-slate-900/95 p-5 text-sm text-gray-200 shadow-xl transition-transform duration-300 md:hidden ${
88+
menuOpen ? "translate-x-0" : "-translate-x-full"
89+
}`}
90+
aria-hidden={!menuOpen}
91+
>
92+
<div className="mb-6 flex items-center justify-between">
93+
<span className="text-xs font-semibold uppercase tracking-wide text-gray-400">
94+
Menu
95+
</span>
96+
<button
97+
type="button"
98+
onClick={closeMenu}
99+
className="rounded bg-white/10 px-2 py-1 text-xs text-gray-200 hover:bg-white/20"
100+
aria-label="Close menu"
101+
>
102+
Close
103+
</button>
104+
</div>
105+
<div className="flex flex-col gap-2">
106+
<NavLink
107+
to="/"
108+
onClick={closeMenu}
109+
className={({ isActive }) =>
110+
`rounded px-3 py-2 hover:text-white ${
111+
isActive ? "bg-white/10 text-white" : ""
112+
}`
113+
}
114+
>
115+
Home
116+
</NavLink>
117+
<NavLink
118+
to="/tos"
119+
onClick={closeMenu}
120+
className={({ isActive }) =>
121+
`rounded px-3 py-2 hover:text-white ${
122+
isActive ? "bg-white/10 text-white" : ""
123+
}`
124+
}
125+
>
126+
Terms of Service
127+
</NavLink>
128+
<NavLink
129+
to="/privacy"
130+
onClick={closeMenu}
131+
className={({ isActive }) =>
132+
`rounded px-3 py-2 hover:text-white ${
133+
isActive ? "bg-white/10 text-white" : ""
134+
}`
135+
}
136+
>
137+
Privacy Policy
138+
</NavLink>
139+
</div>
140+
</aside>
141+
</nav>
142+
);
143+
}
144+
145+
export default TopNav;

0 commit comments

Comments
 (0)