Skip to content

Commit 6228ddd

Browse files
committed
feat(frontend): add advanced scan options
1 parent 3aab86f commit 6228ddd

2 files changed

Lines changed: 175 additions & 4 deletions

File tree

apps/frontend/src/app/globals.css

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,39 @@ body {
331331
background: rgba(92, 225, 230, 0.18);
332332
}
333333

334+
.advanced-panel {
335+
display: grid;
336+
gap: 12px;
337+
padding: 12px;
338+
border-radius: 16px;
339+
border: 1px solid rgba(140, 82, 255, 0.15);
340+
background: rgba(140, 82, 255, 0.06);
341+
}
342+
343+
.advanced-row {
344+
display: grid;
345+
gap: 6px;
346+
}
347+
348+
.advanced-row label {
349+
font-size: 12px;
350+
font-weight: 600;
351+
}
352+
353+
.advanced-row select,
354+
.advanced-row input[type="file"] {
355+
font-size: 13px;
356+
padding: 8px 10px;
357+
border-radius: 10px;
358+
border: 1px solid var(--ios-outline);
359+
background: #fff;
360+
}
361+
362+
.advanced-hint {
363+
font-size: 11px;
364+
color: rgba(11, 11, 22, 0.55);
365+
}
366+
334367
.result-card {
335368
border-radius: 18px;
336369
border: 1px solid var(--ios-outline);
@@ -399,6 +432,47 @@ body {
399432
gap: 10px;
400433
}
401434

435+
.bar-list {
436+
display: grid;
437+
gap: 8px;
438+
}
439+
440+
.bar-row {
441+
display: grid;
442+
grid-template-columns: 120px 1fr 40px;
443+
gap: 10px;
444+
align-items: center;
445+
font-size: 12px;
446+
}
447+
448+
.bar {
449+
height: 8px;
450+
border-radius: 999px;
451+
background: rgba(140, 82, 255, 0.12);
452+
overflow: hidden;
453+
}
454+
455+
.bar-fill {
456+
height: 100%;
457+
background: linear-gradient(90deg, var(--ios-purple), var(--ios-teal));
458+
}
459+
460+
.pill-row {
461+
display: flex;
462+
flex-wrap: wrap;
463+
gap: 8px;
464+
}
465+
466+
.pill-chip {
467+
display: inline-flex;
468+
align-items: center;
469+
gap: 8px;
470+
padding: 6px 10px;
471+
border-radius: 999px;
472+
background: rgba(92, 225, 230, 0.2);
473+
font-size: 12px;
474+
}
475+
402476
.result-card pre {
403477
margin: 0;
404478
max-height: 220px;

apps/frontend/src/app/page.tsx

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import { useMemo, useRef, useState } from "react";
44

55
export default function Home() {
66
const fileRef = useRef<HTMLInputElement>(null);
7+
const projectRef = useRef<HTMLInputElement>(null);
78
const [selectedFile, setSelectedFile] = useState<File | null>(null);
9+
const [projectFile, setProjectFile] = useState<File | null>(null);
810
const [status, setStatus] = useState<string | null>(null);
911
const [result, setResult] = useState<Record<string, unknown> | null>(null);
1012
const [rawResult, setRawResult] = useState<string | null>(null);
1113
const [isUploading, setIsUploading] = useState(false);
14+
const [profile, setProfile] = useState<"basic" | "full">("full");
15+
const [showAdvanced, setShowAdvanced] = useState(false);
1216

1317
const examplePayload = {
1418
report: {
@@ -67,6 +71,12 @@ export default function Home() {
6771
setRawResult(null);
6872
};
6973

74+
const handleProjectChange = (event: React.ChangeEvent<HTMLInputElement>) => {
75+
const file = event.target.files?.[0] ?? null;
76+
setProjectFile(file);
77+
setStatus(file ? `Attached ${file.name}` : "Project removed");
78+
};
79+
7080
const handleUpload = async () => {
7181
if (!selectedFile || isUploading) {
7282
return;
@@ -79,7 +89,10 @@ export default function Home() {
7989
try {
8090
const form = new FormData();
8191
form.append("bundle", selectedFile);
82-
form.append("profile", "full");
92+
form.append("profile", profile);
93+
if (projectFile) {
94+
form.append("project", projectFile);
95+
}
8396

8497
const response = await fetch("http://127.0.0.1:7070/api/v1/scan", {
8598
method: "POST",
@@ -133,7 +146,10 @@ export default function Home() {
133146

134147
const summary = useMemo(() => {
135148
const report = result?.report as
136-
| { results?: Array<Record<string, unknown>>; total_duration_ms?: number }
149+
| {
150+
results?: Array<Record<string, unknown>>;
151+
total_duration_ms?: number;
152+
}
137153
| undefined;
138154
const results = report?.results ?? [];
139155
const failures = results.filter((item) => {
@@ -147,12 +163,26 @@ export default function Home() {
147163
? `${report.total_duration_ms}ms`
148164
: null;
149165

166+
const byCategory = results.reduce<Record<string, number>>((acc, item) => {
167+
const category = String(item.category ?? "Other");
168+
acc[category] = (acc[category] ?? 0) + 1;
169+
return acc;
170+
}, {});
171+
172+
const bySeverity = results.reduce<Record<string, number>>((acc, item) => {
173+
const severity = String(item.severity ?? "Unknown");
174+
acc[severity] = (acc[severity] ?? 0) + 1;
175+
return acc;
176+
}, {});
177+
150178
return {
151179
results,
152180
failures,
153181
errorCount,
154182
warningCount,
155183
duration,
184+
byCategory,
185+
bySeverity,
156186
};
157187
}, [result]);
158188

@@ -291,6 +321,39 @@ export default function Home() {
291321
{selectedFile ? selectedFile.name : "No file selected"}
292322
</div>
293323
</div>
324+
{showAdvanced ? (
325+
<div className="advanced-panel">
326+
<div className="advanced-row">
327+
<label htmlFor="profile-select">Profile</label>
328+
<select
329+
id="profile-select"
330+
value={profile}
331+
onChange={(event) =>
332+
setProfile(event.target.value as "basic" | "full")
333+
}
334+
>
335+
<option value="basic">Basic (core rules)</option>
336+
<option value="full">Full (all rules)</option>
337+
</select>
338+
</div>
339+
<div className="advanced-row">
340+
<label htmlFor="project-file">Project zip (optional)</label>
341+
<input
342+
ref={projectRef}
343+
id="project-file"
344+
type="file"
345+
accept=".zip"
346+
onChange={handleProjectChange}
347+
/>
348+
<span className="advanced-hint">
349+
Zip your .xcodeproj or .xcworkspace to include project context.
350+
</span>
351+
</div>
352+
{projectFile ? (
353+
<div className="upload-status">Attached {projectFile.name}</div>
354+
) : null}
355+
</div>
356+
) : null}
294357
{status ? <div className="status-pill">{status}</div> : null}
295358
{result ? (
296359
<div className="report-stack">
@@ -328,6 +391,36 @@ export default function Home() {
328391
</ul>
329392
</div>
330393

394+
<div className="result-card">
395+
<div className="result-header">Findings by category</div>
396+
<div className="bar-list">
397+
{Object.entries(summary.byCategory).map(([name, count]) => (
398+
<div key={name} className="bar-row">
399+
<span>{name}</span>
400+
<div className="bar">
401+
<div
402+
className="bar-fill"
403+
style={{ width: `${Math.min(count * 12, 100)}%` }}
404+
/>
405+
</div>
406+
<strong>{count}</strong>
407+
</div>
408+
))}
409+
</div>
410+
</div>
411+
412+
<div className="result-card">
413+
<div className="result-header">Findings by severity</div>
414+
<div className="pill-row">
415+
{Object.entries(summary.bySeverity).map(([name, count]) => (
416+
<div key={name} className="pill-chip">
417+
<span>{name}</span>
418+
<strong>{count}</strong>
419+
</div>
420+
))}
421+
</div>
422+
</div>
423+
331424
<div className="result-card">
332425
<div className="result-header">Report actions</div>
333426
<div className="report-actions">
@@ -373,8 +466,12 @@ export default function Home() {
373466
<div>
374467
<strong>Next:</strong> privacy manifest, entitlements, ATS rules
375468
</div>
376-
<button className="ghost-button" type="button">
377-
Advanced options
469+
<button
470+
className="ghost-button"
471+
type="button"
472+
onClick={() => setShowAdvanced((prev) => !prev)}
473+
>
474+
{showAdvanced ? "Hide advanced options" : "Advanced options"}
378475
</button>
379476
</div>
380477
</div>

0 commit comments

Comments
 (0)