Spaces:
Running
Running
UI overhaul: Lucide icons (no emojis), expandable clause cards, severity filters, progress bars, copy/PDF actions, shared nav, interactive hover states
Browse files- extension/popup.html +74 -148
- extension/sidepanel.html +54 -119
- extension/sidepanel.js +72 -66
- web/app/dashboard-pages/analyze/page.tsx +203 -70
- web/app/layout.tsx +9 -11
- web/app/page.tsx +128 -75
- web/components/nav.tsx +74 -0
extension/popup.html
CHANGED
|
@@ -6,183 +6,109 @@
|
|
| 6 |
<title>ClauseGuard</title>
|
| 7 |
<style>
|
| 8 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
-
body {
|
| 10 |
-
width: 360px;
|
| 11 |
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 12 |
-
background: #fafafa;
|
| 13 |
-
color: #1f2937;
|
| 14 |
-
}
|
| 15 |
-
.header {
|
| 16 |
-
background: linear-gradient(135deg, #1e1b4b, #312e81);
|
| 17 |
-
color: white;
|
| 18 |
-
padding: 20px;
|
| 19 |
-
text-align: center;
|
| 20 |
-
}
|
| 21 |
-
.header h1 { font-size: 20px; font-weight: 700; }
|
| 22 |
-
.header p { font-size: 12px; opacity: 0.8; margin-top: 4px; }
|
| 23 |
|
| 24 |
-
.
|
| 25 |
-
.
|
| 26 |
-
|
| 27 |
-
border: 1px solid #e5e7eb;
|
| 28 |
-
border-radius: 12px;
|
| 29 |
-
padding: 16px;
|
| 30 |
-
text-align: center;
|
| 31 |
-
}
|
| 32 |
-
.risk-score {
|
| 33 |
-
font-size: 48px;
|
| 34 |
-
font-weight: 800;
|
| 35 |
-
line-height: 1;
|
| 36 |
-
}
|
| 37 |
-
.risk-label { font-size: 12px; color: #6b7280; margin-top: 4px; }
|
| 38 |
-
.grade {
|
| 39 |
-
display: inline-flex;
|
| 40 |
-
align-items: center;
|
| 41 |
-
gap: 6px;
|
| 42 |
-
margin-top: 10px;
|
| 43 |
-
padding: 6px 14px;
|
| 44 |
-
border-radius: 999px;
|
| 45 |
-
font-weight: 600;
|
| 46 |
-
font-size: 14px;
|
| 47 |
-
}
|
| 48 |
-
.grade-f { background: #fef2f2; color: #dc2626; }
|
| 49 |
-
.grade-d { background: #fff7ed; color: #ea580c; }
|
| 50 |
-
.grade-c { background: #fefce8; color: #ca8a04; }
|
| 51 |
-
.grade-b { background: #f0fdf4; color: #16a34a; }
|
| 52 |
-
.grade-a { background: #f0fdf4; color: #16a34a; }
|
| 53 |
|
| 54 |
-
.
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
}
|
| 60 |
-
.
|
| 61 |
-
|
| 62 |
-
border-radius: 8px;
|
| 63 |
-
text-align: center;
|
| 64 |
-
}
|
| 65 |
-
.count-box .num { font-size: 20px; font-weight: 700; }
|
| 66 |
-
.count-box .label { font-size: 10px; font-weight: 500; }
|
| 67 |
-
.count-high { background: #fef2f2; color: #dc2626; }
|
| 68 |
-
.count-med { background: #fff7ed; color: #ea580c; }
|
| 69 |
-
.count-low { background: #fefce8; color: #ca8a04; }
|
| 70 |
|
| 71 |
-
.
|
| 72 |
-
.
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
border: none;
|
| 77 |
-
border-radius: 10px;
|
| 78 |
-
font-size: 14px;
|
| 79 |
-
font-weight: 600;
|
| 80 |
-
cursor: pointer;
|
| 81 |
-
margin-bottom: 8px;
|
| 82 |
-
transition: opacity 0.15s;
|
| 83 |
-
}
|
| 84 |
-
.btn:hover { opacity: 0.9; }
|
| 85 |
-
.btn-primary { background: #4f46e5; color: white; }
|
| 86 |
-
.btn-secondary { background: #f3f4f6; color: #374151; }
|
| 87 |
-
.btn-danger { background: #fef2f2; color: #dc2626; }
|
| 88 |
|
| 89 |
-
.
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
}
|
| 98 |
-
.usage-bar {
|
| 99 |
-
width: 100px;
|
| 100 |
-
height: 4px;
|
| 101 |
-
background: #e5e7eb;
|
| 102 |
-
border-radius: 99px;
|
| 103 |
-
overflow: hidden;
|
| 104 |
-
}
|
| 105 |
-
.usage-bar-fill {
|
| 106 |
-
height: 100%;
|
| 107 |
-
background: #4f46e5;
|
| 108 |
-
border-radius: 99px;
|
| 109 |
-
transition: width 0.3s;
|
| 110 |
-
}
|
| 111 |
|
| 112 |
-
.
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
}
|
| 117 |
-
.
|
| 118 |
-
.
|
| 119 |
|
| 120 |
-
.
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
.
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
}
|
| 131 |
-
.footer a:hover {
|
| 132 |
</style>
|
| 133 |
</head>
|
| 134 |
<body>
|
| 135 |
<div class="header">
|
| 136 |
-
<
|
| 137 |
-
<
|
| 138 |
</div>
|
| 139 |
|
| 140 |
<div id="results-view" style="display:none;">
|
| 141 |
-
<div class="
|
| 142 |
-
<div class="
|
| 143 |
-
<div class="
|
| 144 |
-
<
|
| 145 |
-
<div class="grade" id="grade-badge"></div>
|
| 146 |
-
<div class="counts">
|
| 147 |
-
<div class="count-box count-high">
|
| 148 |
-
<div class="num" id="count-high">0</div>
|
| 149 |
-
<div class="label">🔴 HIGH</div>
|
| 150 |
-
</div>
|
| 151 |
-
<div class="count-box count-med">
|
| 152 |
-
<div class="num" id="count-med">0</div>
|
| 153 |
-
<div class="label">🟠 MEDIUM</div>
|
| 154 |
-
</div>
|
| 155 |
-
<div class="count-box count-low">
|
| 156 |
-
<div class="num" id="count-low">0</div>
|
| 157 |
-
<div class="label">🟡 LOW</div>
|
| 158 |
-
</div>
|
| 159 |
-
</div>
|
| 160 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
</div>
|
| 162 |
<div class="actions">
|
| 163 |
-
<button class="btn btn-primary" id="btn-details">
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
</div>
|
| 166 |
</div>
|
| 167 |
|
| 168 |
<div id="empty-view">
|
| 169 |
-
<div class="empty
|
| 170 |
-
<
|
| 171 |
-
<p>No scan results
|
| 172 |
</div>
|
| 173 |
<div class="actions">
|
| 174 |
-
<button class="btn btn-primary" id="btn-scan">
|
|
|
|
|
|
|
|
|
|
| 175 |
</div>
|
| 176 |
</div>
|
| 177 |
|
| 178 |
<div class="usage">
|
| 179 |
-
<span id="usage-text">Free: 0/10 scans</span>
|
| 180 |
-
<div class="usage-bar"><div class="usage-
|
| 181 |
</div>
|
| 182 |
|
| 183 |
<div class="footer">
|
| 184 |
-
<a href="https://clauseguard.com" target="_blank">clauseguard.com</a>
|
| 185 |
-
<a href="https://app.clauseguard.com/pricing" target="_blank">Upgrade to Pro</a>
|
| 186 |
</div>
|
| 187 |
|
| 188 |
<script src="popup.js"></script>
|
|
|
|
| 6 |
<title>ClauseGuard</title>
|
| 7 |
<style>
|
| 8 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
body { width: 340px; font-family: system-ui, -apple-system, sans-serif; background: #fff; color: #18181b; font-size: 13px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
.header { padding: 16px 16px 12px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 8px; }
|
| 12 |
+
.header svg { width: 18px; height: 18px; color: #18181b; }
|
| 13 |
+
.header h1 { font-size: 15px; font-weight: 600; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
.score-card { padding: 16px; }
|
| 16 |
+
.score-row { display: flex; align-items: baseline; justify-content: space-between; }
|
| 17 |
+
.score-num { font-size: 36px; font-weight: 600; letter-spacing: -1px; }
|
| 18 |
+
.score-label { font-size: 12px; color: #a1a1aa; margin-left: 4px; }
|
| 19 |
+
.grade { font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: 6px; border: 1px solid; }
|
| 20 |
+
.grade-f, .grade-d { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
|
| 21 |
+
.grade-c { background: #fffbeb; color: #a16207; border-color: #fde68a; }
|
| 22 |
+
.grade-b, .grade-a { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
.bar-wrap { margin-top: 10px; height: 4px; background: #f4f4f5; border-radius: 99px; overflow: hidden; }
|
| 25 |
+
.bar-fill { height: 100%; border-radius: 99px; transition: width 0.6s ease; }
|
| 26 |
+
.bar-red { background: #ef4444; }
|
| 27 |
+
.bar-amber { background: #f59e0b; }
|
| 28 |
+
.bar-green { background: #22c55e; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
.counts { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; padding: 0 16px 12px; }
|
| 31 |
+
.count-box { text-align: center; padding: 8px 4px; border-radius: 8px; border: 1px solid #f4f4f5; }
|
| 32 |
+
.count-num { font-size: 18px; font-weight: 600; }
|
| 33 |
+
.count-label { font-size: 10px; margin-top: 2px; display: flex; align-items: center; justify-content: center; gap: 4px; }
|
| 34 |
+
.dot { width: 6px; height: 6px; border-radius: 50%; }
|
| 35 |
+
.dot-red { background: #ef4444; }
|
| 36 |
+
.dot-amber { background: #f59e0b; }
|
| 37 |
+
.dot-blue { background: #3b82f6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
.actions { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 6px; }
|
| 40 |
+
.btn { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
|
| 41 |
+
.btn svg { width: 15px; height: 15px; }
|
| 42 |
+
.btn-primary { background: #18181b; color: #fff; }
|
| 43 |
+
.btn-primary:hover { background: #27272a; }
|
| 44 |
+
.btn-secondary { background: #f4f4f5; color: #3f3f46; }
|
| 45 |
+
.btn-secondary:hover { background: #e4e4e7; }
|
| 46 |
|
| 47 |
+
.usage { padding: 10px 16px; border-top: 1px solid #f4f4f5; display: flex; align-items: center; justify-content: space-between; }
|
| 48 |
+
.usage-text { font-size: 11px; color: #a1a1aa; }
|
| 49 |
+
.usage-bar { width: 80px; height: 3px; background: #f4f4f5; border-radius: 99px; overflow: hidden; }
|
| 50 |
+
.usage-fill { height: 100%; background: #18181b; border-radius: 99px; transition: width 0.3s; }
|
| 51 |
+
|
| 52 |
+
.empty { padding: 40px 16px; text-align: center; }
|
| 53 |
+
.empty svg { width: 32px; height: 32px; color: #d4d4d8; margin: 0 auto 8px; }
|
| 54 |
+
.empty p { color: #a1a1aa; font-size: 13px; }
|
| 55 |
+
|
| 56 |
+
.footer { padding: 8px 16px; border-top: 1px solid #f4f4f5; text-align: center; }
|
| 57 |
+
.footer a { color: #a1a1aa; text-decoration: none; font-size: 11px; }
|
| 58 |
+
.footer a:hover { color: #52525b; }
|
| 59 |
</style>
|
| 60 |
</head>
|
| 61 |
<body>
|
| 62 |
<div class="header">
|
| 63 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
|
| 64 |
+
<h1>ClauseGuard</h1>
|
| 65 |
</div>
|
| 66 |
|
| 67 |
<div id="results-view" style="display:none;">
|
| 68 |
+
<div class="score-card">
|
| 69 |
+
<div class="score-row">
|
| 70 |
+
<div><span class="score-num" id="risk-score">—</span><span class="score-label">/100 risk</span></div>
|
| 71 |
+
<span class="grade" id="grade-badge"></span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
</div>
|
| 73 |
+
<div class="bar-wrap"><div class="bar-fill" id="bar-fill" style="width:0%"></div></div>
|
| 74 |
+
</div>
|
| 75 |
+
<div class="counts">
|
| 76 |
+
<div class="count-box"><div class="count-num" id="c-high">0</div><div class="count-label"><span class="dot dot-red"></span>High</div></div>
|
| 77 |
+
<div class="count-box"><div class="count-num" id="c-med">0</div><div class="count-label"><span class="dot dot-amber"></span>Medium</div></div>
|
| 78 |
+
<div class="count-box"><div class="count-num" id="c-low">0</div><div class="count-label"><span class="dot dot-blue"></span>Low</div></div>
|
| 79 |
</div>
|
| 80 |
<div class="actions">
|
| 81 |
+
<button class="btn btn-primary" id="btn-details">
|
| 82 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
| 83 |
+
View full details
|
| 84 |
+
</button>
|
| 85 |
+
<button class="btn btn-secondary" id="btn-rescan">
|
| 86 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
| 87 |
+
Re-scan page
|
| 88 |
+
</button>
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
|
| 92 |
<div id="empty-view">
|
| 93 |
+
<div class="empty">
|
| 94 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
|
| 95 |
+
<p>No scan results yet.</p>
|
| 96 |
</div>
|
| 97 |
<div class="actions">
|
| 98 |
+
<button class="btn btn-primary" id="btn-scan">
|
| 99 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
|
| 100 |
+
Scan this page
|
| 101 |
+
</button>
|
| 102 |
</div>
|
| 103 |
</div>
|
| 104 |
|
| 105 |
<div class="usage">
|
| 106 |
+
<span class="usage-text" id="usage-text">Free: 0/10 scans</span>
|
| 107 |
+
<div class="usage-bar"><div class="usage-fill" id="usage-fill" style="width:0%"></div></div>
|
| 108 |
</div>
|
| 109 |
|
| 110 |
<div class="footer">
|
| 111 |
+
<a href="https://clauseguard.com" target="_blank">clauseguard.com</a>
|
|
|
|
| 112 |
</div>
|
| 113 |
|
| 114 |
<script src="popup.js"></script>
|
extension/sidepanel.html
CHANGED
|
@@ -3,149 +3,84 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>ClauseGuard
|
| 7 |
<style>
|
| 8 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
-
body {
|
| 10 |
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 11 |
-
background: #f8fafc;
|
| 12 |
-
color: #1e293b;
|
| 13 |
-
font-size: 14px;
|
| 14 |
-
line-height: 1.5;
|
| 15 |
-
}
|
| 16 |
|
| 17 |
-
.header {
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
position: sticky;
|
| 22 |
-
top: 0;
|
| 23 |
-
z-index: 10;
|
| 24 |
-
}
|
| 25 |
-
.header h1 { font-size: 18px; font-weight: 700; }
|
| 26 |
-
.header-meta { font-size: 12px; opacity: 0.7; margin-top: 4px; }
|
| 27 |
|
| 28 |
-
.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
}
|
| 34 |
-
.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
text-align: center;
|
| 38 |
-
}
|
| 39 |
-
.summary-box .num { font-size: 24px; font-weight: 700; }
|
| 40 |
-
.summary-box .label { font-size: 11px; font-weight: 500; margin-top: 2px; }
|
| 41 |
-
.sum-high { background: #fef2f2; color: #dc2626; }
|
| 42 |
-
.sum-med { background: #fff7ed; color: #ea580c; }
|
| 43 |
-
.sum-low { background: #fefce8; color: #ca8a04; }
|
| 44 |
|
| 45 |
-
.
|
| 46 |
-
.
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
}
|
| 54 |
-
.clause-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
| 55 |
-
.clause-card.severity-high { border-left: 4px solid #ef4444; }
|
| 56 |
-
.clause-card.severity-medium { border-left: 4px solid #f59e0b; }
|
| 57 |
-
.clause-card.severity-low { border-left: 4px solid #3b82f6; }
|
| 58 |
|
| 59 |
-
.clause-
|
| 60 |
-
.clause-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
line-height: 1.6;
|
| 66 |
-
}
|
| 67 |
-
.clause-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
| 68 |
-
.tag {
|
| 69 |
-
font-size: 11px;
|
| 70 |
-
font-weight: 600;
|
| 71 |
-
padding: 3px 10px;
|
| 72 |
-
border-radius: 999px;
|
| 73 |
-
}
|
| 74 |
-
.tag-high { background: #fecaca; color: #991b1b; }
|
| 75 |
-
.tag-medium { background: #fed7aa; color: #9a3412; }
|
| 76 |
-
.tag-low { background: #dbeafe; color: #1e40af; }
|
| 77 |
|
| 78 |
-
.clause-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
border-radius: 8px;
|
| 85 |
-
border-left: 3px solid #cbd5e1;
|
| 86 |
-
}
|
| 87 |
|
| 88 |
-
.
|
| 89 |
-
|
| 90 |
-
padding: 60px 20px;
|
| 91 |
-
color: #94a3b8;
|
| 92 |
-
}
|
| 93 |
-
.empty .icon { font-size: 48px; margin-bottom: 12px; }
|
| 94 |
|
| 95 |
-
.
|
| 96 |
-
|
| 97 |
-
gap: 6px;
|
| 98 |
-
padding: 12px 16px;
|
| 99 |
-
overflow-x: auto;
|
| 100 |
-
}
|
| 101 |
-
.filter-btn {
|
| 102 |
-
padding: 6px 12px;
|
| 103 |
-
border: 1px solid #e2e8f0;
|
| 104 |
-
border-radius: 999px;
|
| 105 |
-
background: white;
|
| 106 |
-
font-size: 12px;
|
| 107 |
-
cursor: pointer;
|
| 108 |
-
white-space: nowrap;
|
| 109 |
-
font-weight: 500;
|
| 110 |
-
transition: all 0.15s;
|
| 111 |
-
}
|
| 112 |
-
.filter-btn:hover { background: #f1f5f9; }
|
| 113 |
-
.filter-btn.active { background: #4f46e5; color: white; border-color: #4f46e5; }
|
| 114 |
</style>
|
| 115 |
</head>
|
| 116 |
<body>
|
| 117 |
<div class="header">
|
| 118 |
-
<
|
| 119 |
-
<
|
|
|
|
| 120 |
</div>
|
| 121 |
|
| 122 |
-
<div class="
|
| 123 |
-
<div class="
|
| 124 |
-
|
| 125 |
-
<div class="
|
| 126 |
-
|
| 127 |
-
<div class="summary-box sum-med">
|
| 128 |
-
<div class="num" id="s-med">0</div>
|
| 129 |
-
<div class="label">🟠 Medium</div>
|
| 130 |
-
</div>
|
| 131 |
-
<div class="summary-box sum-low">
|
| 132 |
-
<div class="num" id="s-low">0</div>
|
| 133 |
-
<div class="label">🟡 Low</div>
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
|
| 137 |
-
<div class="
|
| 138 |
<button class="filter-btn active" data-filter="all">All</button>
|
| 139 |
-
<button class="filter-btn" data-filter="HIGH">
|
| 140 |
-
<button class="filter-btn" data-filter="MEDIUM">
|
| 141 |
-
<button class="filter-btn" data-filter="LOW">
|
| 142 |
</div>
|
| 143 |
|
| 144 |
<div class="clause-list" id="clause-list"></div>
|
| 145 |
|
| 146 |
<div class="empty" id="empty-state">
|
| 147 |
-
<
|
| 148 |
-
<p>Scan a page to see
|
| 149 |
</div>
|
| 150 |
|
| 151 |
<script src="sidepanel.js"></script>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>ClauseGuard</title>
|
| 7 |
<style>
|
| 8 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
body { font-family: system-ui, -apple-system, sans-serif; background: #fff; color: #18181b; font-size: 13px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
.header { padding: 14px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 8px; position: sticky; top: 0; background: #fff; z-index: 10; }
|
| 12 |
+
.header svg { width: 16px; height: 16px; }
|
| 13 |
+
.header-title { font-size: 14px; font-weight: 600; }
|
| 14 |
+
.header-meta { font-size: 11px; color: #a1a1aa; margin-left: auto; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
.score-bar { padding: 12px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 12px; }
|
| 17 |
+
.grade-box { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 700; border: 1px solid; }
|
| 18 |
+
.grade-f, .grade-d { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
|
| 19 |
+
.grade-c { background: #fffbeb; color: #a16207; border-color: #fde68a; }
|
| 20 |
+
.grade-b, .grade-a { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
|
| 21 |
+
.score-info { flex: 1; }
|
| 22 |
+
.score-top { display: flex; justify-content: space-between; font-size: 11px; color: #a1a1aa; margin-bottom: 4px; }
|
| 23 |
+
.progress { height: 5px; background: #f4f4f5; border-radius: 99px; overflow: hidden; }
|
| 24 |
+
.progress-fill { height: 100%; border-radius: 99px; transition: width 0.5s ease; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
.filters { display: flex; gap: 4px; padding: 10px 16px; border-bottom: 1px solid #f4f4f5; overflow-x: auto; }
|
| 27 |
+
.filter-btn { padding: 5px 10px; border: 1px solid #e4e4e7; border-radius: 6px; background: #fff; font-size: 12px; cursor: pointer; white-space: nowrap; font-weight: 500; color: #71717a; transition: all 0.15s; display: flex; align-items: center; gap: 5px; }
|
| 28 |
+
.filter-btn:hover { background: #f4f4f5; }
|
| 29 |
+
.filter-btn.active { background: #18181b; color: #fff; border-color: #18181b; }
|
| 30 |
+
.filter-count { font-size: 10px; opacity: 0.6; }
|
| 31 |
+
.dot { width: 6px; height: 6px; border-radius: 50%; }
|
| 32 |
+
.dot-red { background: #ef4444; }
|
| 33 |
+
.dot-amber { background: #f59e0b; }
|
| 34 |
+
.dot-blue { background: #3b82f6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
.clause-list { padding: 8px; }
|
| 37 |
+
.clause-card { border: 1px solid #e4e4e7; border-radius: 10px; padding: 12px; margin-bottom: 6px; transition: all 0.15s; cursor: default; }
|
| 38 |
+
.clause-card:hover { border-color: #d4d4d8; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
| 39 |
+
.clause-card.sev-high { border-left: 3px solid #ef4444; }
|
| 40 |
+
.clause-card.sev-medium { border-left: 3px solid #f59e0b; }
|
| 41 |
+
.clause-card.sev-low { border-left: 3px solid #3b82f6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
.clause-tags { display: flex; flex-wrap: wrap; gap: 4px; }
|
| 44 |
+
.tag { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 4px; border: 1px solid; display: inline-flex; align-items: center; gap: 3px; }
|
| 45 |
+
.tag svg { width: 10px; height: 10px; }
|
| 46 |
+
.tag-high { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
|
| 47 |
+
.tag-medium { background: #fffbeb; color: #a16207; border-color: #fde68a; }
|
| 48 |
+
.tag-low { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
.clause-text { font-size: 12px; color: #3f3f46; margin-top: 8px; line-height: 1.6; }
|
| 51 |
+
.clause-desc { font-size: 11px; color: #71717a; margin-top: 6px; padding: 6px 8px; background: #fafafa; border-radius: 6px; line-height: 1.5; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
.empty { text-align: center; padding: 48px 16px; color: #a1a1aa; }
|
| 54 |
+
.empty svg { width: 36px; height: 36px; color: #d4d4d8; margin: 0 auto 10px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
</style>
|
| 56 |
</head>
|
| 57 |
<body>
|
| 58 |
<div class="header">
|
| 59 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
|
| 60 |
+
<span class="header-title">ClauseGuard</span>
|
| 61 |
+
<span class="header-meta" id="meta"></span>
|
| 62 |
</div>
|
| 63 |
|
| 64 |
+
<div class="score-bar" id="score-bar" style="display:none;">
|
| 65 |
+
<div class="grade-box" id="grade-box">—</div>
|
| 66 |
+
<div class="score-info">
|
| 67 |
+
<div class="score-top"><span>Risk Score</span><span id="score-num">0 / 100</span></div>
|
| 68 |
+
<div class="progress"><div class="progress-fill" id="progress-fill" style="width:0%"></div></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
</div>
|
| 70 |
</div>
|
| 71 |
|
| 72 |
+
<div class="filters" id="filters" style="display:none;">
|
| 73 |
<button class="filter-btn active" data-filter="all">All</button>
|
| 74 |
+
<button class="filter-btn" data-filter="HIGH"><span class="dot dot-red"></span>High <span class="filter-count" id="fc-high">0</span></button>
|
| 75 |
+
<button class="filter-btn" data-filter="MEDIUM"><span class="dot dot-amber"></span>Medium <span class="filter-count" id="fc-med">0</span></button>
|
| 76 |
+
<button class="filter-btn" data-filter="LOW"><span class="dot dot-blue"></span>Low <span class="filter-count" id="fc-low">0</span></button>
|
| 77 |
</div>
|
| 78 |
|
| 79 |
<div class="clause-list" id="clause-list"></div>
|
| 80 |
|
| 81 |
<div class="empty" id="empty-state">
|
| 82 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
|
| 83 |
+
<p>Scan a page to see the analysis.</p>
|
| 84 |
</div>
|
| 85 |
|
| 86 |
<script src="sidepanel.js"></script>
|
extension/sidepanel.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
| 1 |
/**
|
| 2 |
-
* ClauseGuard — Side Panel
|
| 3 |
-
* Displays detailed clause analysis with filtering
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
const
|
| 7 |
-
"Limitation of liability": "Company limits or excludes liability for losses
|
| 8 |
-
"Unilateral termination": "
|
| 9 |
-
"Unilateral change": "
|
| 10 |
-
"Content removal": "
|
| 11 |
-
"Contract by using": "You
|
| 12 |
-
"Choice of law": "
|
| 13 |
-
"Jurisdiction": "Disputes
|
| 14 |
-
"Arbitration": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
};
|
| 16 |
|
| 17 |
let allClauses = [];
|
|
@@ -28,87 +34,87 @@ async function loadResults() {
|
|
| 28 |
}
|
| 29 |
|
| 30 |
document.getElementById("empty-state").style.display = "none";
|
| 31 |
-
document.getElementById("
|
| 32 |
document.getElementById("filters").style.display = "flex";
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
//
|
| 38 |
const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
| 39 |
const flagged = results.results.filter(r => r.categories?.length > 0);
|
| 40 |
-
flagged.forEach(r => r.categories.forEach(c => counts[c.severity]++));
|
| 41 |
-
|
| 42 |
-
document.getElementById("
|
| 43 |
-
document.getElementById("
|
| 44 |
-
document.getElementById("s-low").textContent = counts.LOW;
|
| 45 |
|
| 46 |
allClauses = flagged;
|
| 47 |
-
renderClauses(
|
| 48 |
}
|
| 49 |
|
| 50 |
-
function renderClauses(
|
| 51 |
const list = document.getElementById("clause-list");
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
const filtered = currentFilter === "all"
|
| 55 |
-
? clauses
|
| 56 |
-
: clauses.filter(c => c.categories.some(cat => cat.severity === currentFilter));
|
| 57 |
|
| 58 |
if (filtered.length === 0) {
|
| 59 |
-
list.innerHTML = '<div style="text-align:center;
|
| 60 |
return;
|
| 61 |
}
|
| 62 |
|
| 63 |
-
filtered.
|
| 64 |
-
const maxSev =
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
<div class="clause-num">Clause #${i + 1}</div>
|
| 69 |
-
<div class="clause-text">"${truncate(clause.text, 250)}"</div>
|
| 70 |
-
<div class="clause-tags">
|
| 71 |
-
${clause.categories.map(c => `
|
| 72 |
-
<span class="tag tag-${c.severity.toLowerCase()}">${c.name}</span>
|
| 73 |
-
`).join("")}
|
| 74 |
-
</div>
|
| 75 |
-
${clause.categories.map(c => `
|
| 76 |
-
<div class="clause-desc">
|
| 77 |
-
<strong>${c.name}:</strong> ${CATEGORY_DESCRIPTIONS[c.name] || c.name}
|
| 78 |
-
</div>
|
| 79 |
-
`).join("")}
|
| 80 |
-
`;
|
| 81 |
-
list.appendChild(card);
|
| 82 |
-
});
|
| 83 |
-
}
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
-
//
|
| 95 |
document.getElementById("filters").addEventListener("click", (e) => {
|
| 96 |
-
|
|
|
|
| 97 |
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
|
| 98 |
-
|
| 99 |
-
currentFilter =
|
| 100 |
-
renderClauses(
|
| 101 |
});
|
| 102 |
|
| 103 |
-
//
|
| 104 |
chrome.storage.onChanged.addListener((changes) => {
|
| 105 |
for (const key of Object.keys(changes)) {
|
| 106 |
-
if (key.startsWith("results_")) {
|
| 107 |
-
loadResults();
|
| 108 |
-
break;
|
| 109 |
-
}
|
| 110 |
}
|
| 111 |
});
|
| 112 |
|
| 113 |
-
// ─── Init ───
|
| 114 |
loadResults();
|
|
|
|
| 1 |
/**
|
| 2 |
+
* ClauseGuard — Side Panel (redesigned)
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
+
const DESCS = {
|
| 6 |
+
"Limitation of liability": "Company limits or excludes liability for losses or damages.",
|
| 7 |
+
"Unilateral termination": "They can close your account without reason.",
|
| 8 |
+
"Unilateral change": "Terms can change without your consent.",
|
| 9 |
+
"Content removal": "Your content can be deleted without notice.",
|
| 10 |
+
"Contract by using": "You agree just by visiting or using the site.",
|
| 11 |
+
"Choice of law": "Foreign law applies instead of your local protections.",
|
| 12 |
+
"Jurisdiction": "Disputes handled in their preferred court.",
|
| 13 |
+
"Arbitration": "You waive your right to sue in court.",
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// SVG icons for severity
|
| 17 |
+
const SEV_ICONS = {
|
| 18 |
+
HIGH: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
|
| 19 |
+
MEDIUM: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>',
|
| 20 |
+
LOW: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
|
| 21 |
};
|
| 22 |
|
| 23 |
let allClauses = [];
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
document.getElementById("empty-state").style.display = "none";
|
| 37 |
+
document.getElementById("score-bar").style.display = "flex";
|
| 38 |
document.getElementById("filters").style.display = "flex";
|
| 39 |
|
| 40 |
+
// Meta
|
| 41 |
+
document.getElementById("meta").textContent = `${results.total_clauses} clauses · ${results.flagged_count} flagged`;
|
| 42 |
+
|
| 43 |
+
// Grade
|
| 44 |
+
const gb = document.getElementById("grade-box");
|
| 45 |
+
gb.textContent = results.grade;
|
| 46 |
+
gb.className = `grade-box grade-${results.grade.toLowerCase()}`;
|
| 47 |
+
|
| 48 |
+
// Score
|
| 49 |
+
document.getElementById("score-num").textContent = `${results.risk_score} / 100`;
|
| 50 |
+
const pf = document.getElementById("progress-fill");
|
| 51 |
+
pf.style.width = `${results.risk_score}%`;
|
| 52 |
+
pf.style.background = results.risk_score >= 60 ? "#ef4444" : results.risk_score >= 30 ? "#f59e0b" : "#22c55e";
|
| 53 |
|
| 54 |
+
// Counts
|
| 55 |
const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
| 56 |
const flagged = results.results.filter(r => r.categories?.length > 0);
|
| 57 |
+
flagged.forEach(r => r.categories.forEach(c => { if (counts[c.severity] !== undefined) counts[c.severity]++; }));
|
| 58 |
+
document.getElementById("fc-high").textContent = counts.HIGH;
|
| 59 |
+
document.getElementById("fc-med").textContent = counts.MEDIUM;
|
| 60 |
+
document.getElementById("fc-low").textContent = counts.LOW;
|
|
|
|
| 61 |
|
| 62 |
allClauses = flagged;
|
| 63 |
+
renderClauses();
|
| 64 |
}
|
| 65 |
|
| 66 |
+
function renderClauses() {
|
| 67 |
const list = document.getElementById("clause-list");
|
| 68 |
+
const filtered = currentFilter === "all" ? allClauses : allClauses.filter(c => c.categories.some(cat => cat.severity === currentFilter));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
if (filtered.length === 0) {
|
| 71 |
+
list.innerHTML = '<div style="text-align:center;padding:24px;color:#a1a1aa;font-size:12px;">No clauses match this filter.</div>';
|
| 72 |
return;
|
| 73 |
}
|
| 74 |
|
| 75 |
+
list.innerHTML = filtered.map((clause, i) => {
|
| 76 |
+
const maxSev = clause.categories.reduce((m, c) => {
|
| 77 |
+
const o = { HIGH: 3, MEDIUM: 2, LOW: 1 };
|
| 78 |
+
return (o[c.severity] || 0) > (o[m] || 0) ? c.severity : m;
|
| 79 |
+
}, "LOW");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
const tagMap = { HIGH: "tag-high", MEDIUM: "tag-medium", LOW: "tag-low" };
|
| 82 |
+
|
| 83 |
+
const tags = clause.categories.map(c =>
|
| 84 |
+
`<span class="tag ${tagMap[c.severity] || "tag-medium"}">${SEV_ICONS[c.severity] || ""} ${c.name}</span>`
|
| 85 |
+
).join("");
|
| 86 |
+
|
| 87 |
+
const descs = clause.categories.map(c =>
|
| 88 |
+
`<div class="clause-desc">${DESCS[c.name] || c.name}</div>`
|
| 89 |
+
).join("");
|
| 90 |
|
| 91 |
+
const text = clause.text.length > 200 ? clause.text.slice(0, 200) + "…" : clause.text;
|
| 92 |
+
|
| 93 |
+
return `
|
| 94 |
+
<div class="clause-card sev-${maxSev.toLowerCase()}">
|
| 95 |
+
<div class="clause-tags">${tags}</div>
|
| 96 |
+
<div class="clause-text">${text}</div>
|
| 97 |
+
${descs}
|
| 98 |
+
</div>
|
| 99 |
+
`;
|
| 100 |
+
}).join("");
|
| 101 |
}
|
| 102 |
|
| 103 |
+
// Filters
|
| 104 |
document.getElementById("filters").addEventListener("click", (e) => {
|
| 105 |
+
const btn = e.target.closest(".filter-btn");
|
| 106 |
+
if (!btn) return;
|
| 107 |
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
|
| 108 |
+
btn.classList.add("active");
|
| 109 |
+
currentFilter = btn.dataset.filter;
|
| 110 |
+
renderClauses();
|
| 111 |
});
|
| 112 |
|
| 113 |
+
// Listen for updates
|
| 114 |
chrome.storage.onChanged.addListener((changes) => {
|
| 115 |
for (const key of Object.keys(changes)) {
|
| 116 |
+
if (key.startsWith("results_")) { loadResults(); break; }
|
|
|
|
|
|
|
|
|
|
| 117 |
}
|
| 118 |
});
|
| 119 |
|
|
|
|
| 120 |
loadResults();
|
web/app/dashboard-pages/analyze/page.tsx
CHANGED
|
@@ -1,13 +1,35 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import { useState } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
interface
|
| 6 |
-
interface
|
| 7 |
-
interface
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
|
| 13 |
|
|
@@ -17,7 +39,7 @@ In no event will Spotify be liable for any indirect, incidental, special, conseq
|
|
| 17 |
|
| 18 |
Spotify reserves the right to remove or disable access to any User Content for any reason, without prior notice.
|
| 19 |
|
| 20 |
-
Spotify may terminate your account or suspend your access
|
| 21 |
|
| 22 |
These Terms will be governed by and construed in accordance with the laws of the State of New York.
|
| 23 |
|
|
@@ -25,105 +47,212 @@ Any dispute shall be finally settled by arbitration in New York County.`;
|
|
| 25 |
|
| 26 |
export default function AnalyzePage() {
|
| 27 |
const [text, setText] = useState("");
|
| 28 |
-
const [results, setResults] = useState<
|
| 29 |
const [loading, setLoading] = useState(false);
|
| 30 |
const [error, setError] = useState("");
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
async function handleAnalyze() {
|
| 33 |
if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
|
| 34 |
-
setLoading(true); setError(""); setResults(null);
|
| 35 |
try {
|
| 36 |
-
const res = await fetch("/api/analyze", {
|
| 37 |
-
|
| 38 |
-
body: JSON.stringify({ text }),
|
| 39 |
-
});
|
| 40 |
-
if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed"); }
|
| 41 |
setResults(await res.json());
|
| 42 |
-
} catch (
|
| 43 |
finally { setLoading(false); }
|
| 44 |
}
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
return (
|
| 53 |
<div className="min-h-screen bg-white">
|
| 54 |
-
<div className="max-w-
|
| 55 |
<div className="mb-8">
|
| 56 |
-
<h1 className="text-2xl font-semibold tracking-tight
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
</div>
|
| 59 |
|
| 60 |
-
<div className="grid lg:grid-cols-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
placeholder="Paste your text here..."
|
| 65 |
-
className="w-full h-
|
| 66 |
-
/>
|
| 67 |
<div className="mt-3 flex gap-2">
|
| 68 |
<button onClick={handleAnalyze} disabled={loading}
|
| 69 |
-
className="flex-1 bg-zinc-900 text-white py-2.5 rounded-
|
| 70 |
-
{loading ? "Scanning...
|
| 71 |
</button>
|
| 72 |
<button onClick={() => setText(EXAMPLE)}
|
| 73 |
-
className="px-4 border border-zinc-200 rounded-
|
| 74 |
Example
|
| 75 |
</button>
|
| 76 |
</div>
|
| 77 |
-
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
| 78 |
</div>
|
| 79 |
|
| 80 |
-
|
|
|
|
| 81 |
{results ? (
|
| 82 |
-
<div>
|
| 83 |
-
{/* Score */}
|
| 84 |
-
<div className="border border-zinc-200 rounded-
|
| 85 |
-
<div className="flex items-
|
| 86 |
<div>
|
| 87 |
-
<
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
</div>
|
| 90 |
-
<
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
</span>
|
| 97 |
</div>
|
| 98 |
-
<p className="mt-2 text-xs text-zinc-400">
|
| 99 |
-
{results.total_clauses} clauses scanned · {results.flagged_count} flagged · {results.latency_ms}ms
|
| 100 |
-
</p>
|
| 101 |
</div>
|
| 102 |
|
| 103 |
-
{/*
|
| 104 |
-
<div className="
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</div>
|
| 121 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
) : (
|
| 125 |
-
<div className="border border-dashed border-zinc-200 rounded-
|
| 126 |
-
<
|
|
|
|
| 127 |
</div>
|
| 128 |
)}
|
| 129 |
</div>
|
|
@@ -132,3 +261,7 @@ export default function AnalyzePage() {
|
|
| 132 |
</div>
|
| 133 |
);
|
| 134 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import { useState } from "react";
|
| 4 |
+
import {
|
| 5 |
+
ScanText, ScanLine, TriangleAlert, CircleAlert, CircleCheck, Info,
|
| 6 |
+
Download, FileDown, ChevronDown, ChevronUp, Highlighter, Copy, Check,
|
| 7 |
+
ShieldCheck, ShieldAlert, Scale, Gavel, Ban, Globe, Eye, Stamp, FileX
|
| 8 |
+
} from "lucide-react";
|
| 9 |
|
| 10 |
+
interface Cat { name: string; severity: string; description?: string; confidence?: number; }
|
| 11 |
+
interface Clause { text: string; categories: Cat[]; }
|
| 12 |
+
interface Result { risk_score: number; grade: string; total_clauses: number; flagged_count: number; results: Clause[]; model: string; latency_ms: number; }
|
| 13 |
+
|
| 14 |
+
const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string; dot: string }> = {
|
| 15 |
+
HIGH: { icon: TriangleAlert, label: "High", text: "text-red-600", bg: "bg-red-50", border: "border-red-200", dot: "bg-red-500" },
|
| 16 |
+
MEDIUM: { icon: CircleAlert, label: "Medium", text: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200", dot: "bg-amber-500" },
|
| 17 |
+
LOW: { icon: Info, label: "Low", text: "text-blue-600", bg: "bg-blue-50", border: "border-blue-200", dot: "bg-blue-500" },
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
const GRADE_STYLE: Record<string, string> = {
|
| 21 |
+
A: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
| 22 |
+
B: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
| 23 |
+
C: "bg-amber-50 text-amber-700 border-amber-200",
|
| 24 |
+
D: "bg-red-50 text-red-700 border-red-200",
|
| 25 |
+
F: "bg-red-50 text-red-700 border-red-200",
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
const CATEGORY_ICONS: Record<string, any> = {
|
| 29 |
+
"Arbitration": Scale, "Limitation of liability": ShieldAlert, "Unilateral termination": Ban,
|
| 30 |
+
"Unilateral change": FileX, "Content removal": Eye, "Jurisdiction": Globe,
|
| 31 |
+
"Choice of law": Gavel, "Contract by using": Stamp,
|
| 32 |
+
};
|
| 33 |
|
| 34 |
const EXAMPLE = `By using the Spotify Service, you agree to be bound by these Terms of Use.
|
| 35 |
|
|
|
|
| 39 |
|
| 40 |
Spotify reserves the right to remove or disable access to any User Content for any reason, without prior notice.
|
| 41 |
|
| 42 |
+
Spotify may terminate your account or suspend your access at any time, with or without cause, with or without notice, effective immediately.
|
| 43 |
|
| 44 |
These Terms will be governed by and construed in accordance with the laws of the State of New York.
|
| 45 |
|
|
|
|
| 47 |
|
| 48 |
export default function AnalyzePage() {
|
| 49 |
const [text, setText] = useState("");
|
| 50 |
+
const [results, setResults] = useState<Result | null>(null);
|
| 51 |
const [loading, setLoading] = useState(false);
|
| 52 |
const [error, setError] = useState("");
|
| 53 |
+
const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
|
| 54 |
+
const [filter, setFilter] = useState<string>("all");
|
| 55 |
+
const [copied, setCopied] = useState(false);
|
| 56 |
|
| 57 |
async function handleAnalyze() {
|
| 58 |
if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
|
| 59 |
+
setLoading(true); setError(""); setResults(null); setExpandedIdx(null);
|
| 60 |
try {
|
| 61 |
+
const res = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
|
| 62 |
+
if (!res.ok) throw new Error((await res.json()).error || "Failed");
|
|
|
|
|
|
|
|
|
|
| 63 |
setResults(await res.json());
|
| 64 |
+
} catch (e: any) { setError(e.message); }
|
| 65 |
finally { setLoading(false); }
|
| 66 |
}
|
| 67 |
|
| 68 |
+
async function handleDownloadPDF() {
|
| 69 |
+
if (!results) return;
|
| 70 |
+
try {
|
| 71 |
+
const res = await fetch("/api/pdf/report", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(results) });
|
| 72 |
+
const blob = await res.blob();
|
| 73 |
+
const url = URL.createObjectURL(blob);
|
| 74 |
+
const a = document.createElement("a"); a.href = url; a.download = "clauseguard-report.pdf"; a.click();
|
| 75 |
+
URL.revokeObjectURL(url);
|
| 76 |
+
} catch {}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function handleCopy() {
|
| 80 |
+
if (!results) return;
|
| 81 |
+
const summary = `ClauseGuard Report\nRisk: ${results.risk_score}/100 (Grade ${results.grade})\n${results.flagged_count} of ${results.total_clauses} clauses flagged\n\n` +
|
| 82 |
+
results.results.filter(r => r.categories.length > 0).map((r, i) =>
|
| 83 |
+
`${i+1}. [${r.categories.map(c => c.name).join(", ")}] ${r.text.slice(0, 100)}...`
|
| 84 |
+
).join("\n");
|
| 85 |
+
navigator.clipboard.writeText(summary);
|
| 86 |
+
setCopied(true); setTimeout(() => setCopied(false), 2000);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const flagged = results?.results.filter(r => r.categories.length > 0) || [];
|
| 90 |
+
const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
|
| 91 |
+
|
| 92 |
+
const sevCounts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
| 93 |
+
flagged.forEach(r => r.categories.forEach(c => { if (sevCounts[c.severity as keyof typeof sevCounts] !== undefined) sevCounts[c.severity as keyof typeof sevCounts]++; }));
|
| 94 |
|
| 95 |
return (
|
| 96 |
<div className="min-h-screen bg-white">
|
| 97 |
+
<div className="max-w-6xl mx-auto px-5 py-10">
|
| 98 |
<div className="mb-8">
|
| 99 |
+
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
| 100 |
+
<ScanText className="w-6 h-6 text-zinc-400" />
|
| 101 |
+
Scan a document
|
| 102 |
+
</h1>
|
| 103 |
+
<p className="mt-1 text-sm text-zinc-500">Paste Terms of Service, a contract, or a lease agreement.</p>
|
| 104 |
</div>
|
| 105 |
|
| 106 |
+
<div className="grid lg:grid-cols-5 gap-6">
|
| 107 |
+
{/* Input — 2 cols */}
|
| 108 |
+
<div className="lg:col-span-2">
|
| 109 |
+
<textarea value={text} onChange={(e) => setText(e.target.value)}
|
| 110 |
+
placeholder="Paste your document text here..."
|
| 111 |
+
className="w-full h-[420px] p-4 border border-zinc-200 rounded-xl text-sm leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-zinc-900/10 focus:border-zinc-300 placeholder:text-zinc-300 font-mono" />
|
|
|
|
| 112 |
<div className="mt-3 flex gap-2">
|
| 113 |
<button onClick={handleAnalyze} disabled={loading}
|
| 114 |
+
className="flex-1 inline-flex items-center justify-center gap-2 bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
|
| 115 |
+
{loading ? <><ScanLine className="w-4 h-4 animate-pulse" /> Scanning...</> : <><ScanText className="w-4 h-4" /> Scan</>}
|
| 116 |
</button>
|
| 117 |
<button onClick={() => setText(EXAMPLE)}
|
| 118 |
+
className="px-4 border border-zinc-200 rounded-lg text-sm text-zinc-500 hover:bg-zinc-50 transition-colors">
|
| 119 |
Example
|
| 120 |
</button>
|
| 121 |
</div>
|
| 122 |
+
{error && <p className="mt-2 text-sm text-red-600 flex items-center gap-1.5"><TriangleAlert className="w-3.5 h-3.5" />{error}</p>}
|
| 123 |
</div>
|
| 124 |
|
| 125 |
+
{/* Results — 3 cols */}
|
| 126 |
+
<div className="lg:col-span-3">
|
| 127 |
{results ? (
|
| 128 |
+
<div className="space-y-4">
|
| 129 |
+
{/* Score card */}
|
| 130 |
+
<div className="border border-zinc-200 rounded-xl p-5">
|
| 131 |
+
<div className="flex items-start justify-between">
|
| 132 |
<div>
|
| 133 |
+
<div className="flex items-baseline gap-2">
|
| 134 |
+
<span className="text-4xl font-semibold tracking-tight">{results.risk_score}</span>
|
| 135 |
+
<span className="text-sm text-zinc-400">/100 risk</span>
|
| 136 |
+
</div>
|
| 137 |
+
<div className="mt-2 h-1.5 w-48 bg-zinc-100 rounded-full overflow-hidden">
|
| 138 |
+
<div className={`h-full rounded-full transition-all duration-700 ${
|
| 139 |
+
results.risk_score >= 60 ? "bg-red-500" : results.risk_score >= 30 ? "bg-amber-400" : "bg-emerald-500"
|
| 140 |
+
}`} style={{ width: `${results.risk_score}%` }} />
|
| 141 |
+
</div>
|
| 142 |
</div>
|
| 143 |
+
<div className="flex items-center gap-2">
|
| 144 |
+
<span className={`text-sm font-semibold px-3 py-1 rounded-lg border ${GRADE_STYLE[results.grade] || GRADE_STYLE.C}`}>
|
| 145 |
+
Grade {results.grade}
|
| 146 |
+
</span>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
<div className="mt-4 flex items-center gap-4 text-xs text-zinc-400">
|
| 150 |
+
<span>{results.total_clauses} clauses</span>
|
| 151 |
+
<span className="w-px h-3 bg-zinc-200" />
|
| 152 |
+
<span>{results.flagged_count} flagged</span>
|
| 153 |
+
<span className="w-px h-3 bg-zinc-200" />
|
| 154 |
+
<span>{results.latency_ms}ms</span>
|
| 155 |
+
<span className="w-px h-3 bg-zinc-200" />
|
| 156 |
+
<span className="flex items-center gap-1">
|
| 157 |
+
{results.model === "ml" ? <Sparkles className="w-3 h-3" /> : null}
|
| 158 |
+
{results.model === "ml" ? "Legal-BERT" : "Pattern matching"}
|
| 159 |
</span>
|
| 160 |
</div>
|
|
|
|
|
|
|
|
|
|
| 161 |
</div>
|
| 162 |
|
| 163 |
+
{/* Actions bar */}
|
| 164 |
+
<div className="flex items-center justify-between">
|
| 165 |
+
<div className="flex gap-1">
|
| 166 |
+
{[
|
| 167 |
+
{ key: "all", label: "All", count: flagged.length },
|
| 168 |
+
{ key: "HIGH", label: "High", count: sevCounts.HIGH },
|
| 169 |
+
{ key: "MEDIUM", label: "Medium", count: sevCounts.MEDIUM },
|
| 170 |
+
{ key: "LOW", label: "Low", count: sevCounts.LOW },
|
| 171 |
+
].map((f) => (
|
| 172 |
+
<button key={f.key} onClick={() => setFilter(f.key)}
|
| 173 |
+
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
| 174 |
+
filter === f.key ? "bg-zinc-900 text-white" : "text-zinc-500 hover:bg-zinc-100"
|
| 175 |
+
}`}>
|
| 176 |
+
{f.label} {f.count > 0 && <span className="ml-1 opacity-60">{f.count}</span>}
|
| 177 |
+
</button>
|
| 178 |
+
))}
|
| 179 |
+
</div>
|
| 180 |
+
<div className="flex gap-1.5">
|
| 181 |
+
<button onClick={handleCopy} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Copy summary">
|
| 182 |
+
{copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
|
| 183 |
+
</button>
|
| 184 |
+
<button onClick={handleDownloadPDF} className="p-2 rounded-md hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors" title="Download PDF">
|
| 185 |
+
<FileDown className="w-4 h-4" />
|
| 186 |
+
</button>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
{/* Clause list */}
|
| 191 |
+
<div className="space-y-2 max-h-[400px] overflow-y-auto pr-1">
|
| 192 |
+
{filtered.length === 0 ? (
|
| 193 |
+
<div className="border border-dashed border-zinc-200 rounded-xl p-10 text-center">
|
| 194 |
+
<CircleCheck className="w-8 h-8 text-emerald-400 mx-auto mb-2" />
|
| 195 |
+
<p className="text-sm text-zinc-500">{filter === "all" ? "No unfair clauses found." : "No clauses at this severity level."}</p>
|
| 196 |
</div>
|
| 197 |
+
) : filtered.map((clause, i) => {
|
| 198 |
+
const maxSev = clause.categories.reduce((m, c) => {
|
| 199 |
+
const order: Record<string, number> = { HIGH: 3, MEDIUM: 2, LOW: 1 };
|
| 200 |
+
return (order[c.severity] || 0) > (order[m] || 0) ? c.severity : m;
|
| 201 |
+
}, "LOW");
|
| 202 |
+
const conf = SEV_CONFIG[maxSev] || SEV_CONFIG.MEDIUM;
|
| 203 |
+
const isExpanded = expandedIdx === i;
|
| 204 |
+
const CatIcon = CATEGORY_ICONS[clause.categories[0]?.name] || TriangleAlert;
|
| 205 |
+
|
| 206 |
+
return (
|
| 207 |
+
<div key={i}
|
| 208 |
+
className={`border rounded-xl overflow-hidden transition-all ${conf.border} ${isExpanded ? "shadow-sm" : ""}`}>
|
| 209 |
+
<button onClick={() => setExpandedIdx(isExpanded ? null : i)}
|
| 210 |
+
className="w-full text-left p-4 flex items-start gap-3 hover:bg-zinc-50/50 transition-colors">
|
| 211 |
+
<div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${conf.bg}`}>
|
| 212 |
+
<CatIcon className={`w-4 h-4 ${conf.text}`} />
|
| 213 |
+
</div>
|
| 214 |
+
<div className="flex-1 min-w-0">
|
| 215 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 216 |
+
{clause.categories.map((cat, j) => {
|
| 217 |
+
const s = SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM;
|
| 218 |
+
return (
|
| 219 |
+
<span key={j} className={`text-[11px] font-medium px-2 py-0.5 rounded border ${s.bg} ${s.text} ${s.border}`}>
|
| 220 |
+
{cat.name}
|
| 221 |
+
{cat.confidence ? ` ${Math.round(cat.confidence * 100)}%` : ""}
|
| 222 |
+
</span>
|
| 223 |
+
);
|
| 224 |
+
})}
|
| 225 |
+
</div>
|
| 226 |
+
<p className="mt-1.5 text-sm text-zinc-600 leading-relaxed line-clamp-2">{clause.text}</p>
|
| 227 |
+
</div>
|
| 228 |
+
<div className="shrink-0 mt-1">
|
| 229 |
+
{isExpanded ? <ChevronUp className="w-4 h-4 text-zinc-400" /> : <ChevronDown className="w-4 h-4 text-zinc-400" />}
|
| 230 |
+
</div>
|
| 231 |
+
</button>
|
| 232 |
+
{isExpanded && (
|
| 233 |
+
<div className="px-4 pb-4 pt-0 border-t border-zinc-100">
|
| 234 |
+
<p className="text-sm text-zinc-700 leading-relaxed mt-3 font-mono bg-zinc-50 rounded-lg p-3">
|
| 235 |
+
{clause.text}
|
| 236 |
+
</p>
|
| 237 |
+
{clause.categories.map((cat, j) => (
|
| 238 |
+
<div key={j} className="mt-3 flex items-start gap-2">
|
| 239 |
+
<TriangleAlert className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${(SEV_CONFIG[cat.severity] || SEV_CONFIG.MEDIUM).text}`} />
|
| 240 |
+
<p className="text-[13px] text-zinc-500 leading-relaxed">
|
| 241 |
+
<span className="font-medium text-zinc-700">{cat.name}:</span> {cat.description || "This clause may be unfair under consumer protection law."}
|
| 242 |
+
</p>
|
| 243 |
+
</div>
|
| 244 |
+
))}
|
| 245 |
+
</div>
|
| 246 |
+
)}
|
| 247 |
+
</div>
|
| 248 |
+
);
|
| 249 |
+
})}
|
| 250 |
</div>
|
| 251 |
</div>
|
| 252 |
) : (
|
| 253 |
+
<div className="border border-dashed border-zinc-200 rounded-xl h-[420px] flex flex-col items-center justify-center">
|
| 254 |
+
<ScanText className="w-10 h-10 text-zinc-200 mb-3" />
|
| 255 |
+
<p className="text-sm text-zinc-300">Paste text and scan to see results</p>
|
| 256 |
</div>
|
| 257 |
)}
|
| 258 |
</div>
|
|
|
|
| 261 |
</div>
|
| 262 |
);
|
| 263 |
}
|
| 264 |
+
|
| 265 |
+
function Sparkles({ className }: { className?: string }) {
|
| 266 |
+
return <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/></svg>;
|
| 267 |
+
}
|
web/app/layout.tsx
CHANGED
|
@@ -1,29 +1,27 @@
|
|
| 1 |
import type { Metadata } from "next";
|
|
|
|
| 2 |
import "./globals.css";
|
| 3 |
|
| 4 |
export const metadata: Metadata = {
|
| 5 |
title: "ClauseGuard — AI Fine Print Scanner",
|
| 6 |
-
description:
|
| 7 |
-
|
| 8 |
-
keywords: ["terms of service", "contract scanner", "legal AI", "unfair clauses", "fine print"],
|
| 9 |
openGraph: {
|
| 10 |
-
title: "ClauseGuard
|
| 11 |
-
description: "
|
| 12 |
url: "https://clauseguard.com",
|
| 13 |
siteName: "ClauseGuard",
|
| 14 |
type: "website",
|
| 15 |
},
|
| 16 |
-
twitter: {
|
| 17 |
-
card: "summary_large_image",
|
| 18 |
-
title: "ClauseGuard — AI Fine Print Scanner",
|
| 19 |
-
description: "Stop signing away your rights.",
|
| 20 |
-
},
|
| 21 |
};
|
| 22 |
|
| 23 |
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
| 24 |
return (
|
| 25 |
<html lang="en">
|
| 26 |
-
<body className="antialiased">
|
|
|
|
|
|
|
|
|
|
| 27 |
</html>
|
| 28 |
);
|
| 29 |
}
|
|
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
+
import { Nav } from "@/components/nav";
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
title: "ClauseGuard — AI Fine Print Scanner",
|
| 7 |
+
description: "Scans Terms of Service, contracts, and leases for unfair clauses. Get a risk score before you click accept.",
|
| 8 |
+
keywords: ["terms of service scanner", "contract analyzer", "unfair clauses", "legal AI"],
|
|
|
|
| 9 |
openGraph: {
|
| 10 |
+
title: "ClauseGuard",
|
| 11 |
+
description: "Know what you are agreeing to.",
|
| 12 |
url: "https://clauseguard.com",
|
| 13 |
siteName: "ClauseGuard",
|
| 14 |
type: "website",
|
| 15 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
};
|
| 17 |
|
| 18 |
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
| 19 |
return (
|
| 20 |
<html lang="en">
|
| 21 |
+
<body className="antialiased text-zinc-900 bg-white">
|
| 22 |
+
<Nav />
|
| 23 |
+
{children}
|
| 24 |
+
</body>
|
| 25 |
</html>
|
| 26 |
);
|
| 27 |
}
|
web/app/page.tsx
CHANGED
|
@@ -1,84 +1,100 @@
|
|
| 1 |
import Link from "next/link";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
const
|
| 4 |
-
{ name: "Arbitration", desc: "Waives your right to sue in court" },
|
| 5 |
-
{ name: "Liability limits", desc: "Company avoids responsibility for damages" },
|
| 6 |
-
{ name: "Unilateral termination", desc: "They can close your account without reason" },
|
| 7 |
-
{ name: "Unilateral change", desc: "Terms can change without your consent" },
|
| 8 |
-
{ name: "Content removal", desc: "Your content deleted without notice" },
|
| 9 |
-
{ name: "Jurisdiction", desc: "Disputes handled in their preferred court" },
|
| 10 |
-
{ name: "Choice of law", desc: "Foreign law overrides your local protections" },
|
| 11 |
-
{ name: "Contract by using", desc: "You agree just by visiting the site" },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
];
|
| 13 |
|
| 14 |
const PRICING = [
|
| 15 |
{
|
| 16 |
-
name: "Free", price: "$0", period: "", highlight: false,
|
| 17 |
-
features: ["10 scans per month", "All 8 clause types", "Risk score and grade", "Chrome extension"],
|
| 18 |
-
cta: "Get started",
|
| 19 |
},
|
| 20 |
{
|
| 21 |
-
name: "Pro", price: "$12", period: "/mo", highlight: true,
|
| 22 |
-
features: ["Unlimited scans", "Upload contracts and leases", "
|
| 23 |
-
cta: "Start free trial",
|
| 24 |
},
|
| 25 |
{
|
| 26 |
-
name: "Team", price: "$49", period: "/mo", highlight: false,
|
| 27 |
-
features: ["Everything in Pro", "5 seats", "10,000 API calls", "Shared dashboard", "Slack support"],
|
| 28 |
-
cta: "Talk to us",
|
| 29 |
},
|
| 30 |
];
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
export default function Home() {
|
| 33 |
return (
|
| 34 |
<main className="min-h-screen bg-white text-zinc-900">
|
| 35 |
-
{/* Nav */}
|
| 36 |
-
<nav className="border-b border-zinc-100">
|
| 37 |
-
<div className="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
|
| 38 |
-
<span className="font-semibold tracking-tight">ClauseGuard</span>
|
| 39 |
-
<div className="hidden sm:flex items-center gap-6 text-sm text-zinc-500">
|
| 40 |
-
<a href="#features" className="hover:text-zinc-900">Features</a>
|
| 41 |
-
<a href="#pricing" className="hover:text-zinc-900">Pricing</a>
|
| 42 |
-
<Link href="/auth/login" className="hover:text-zinc-900">Log in</Link>
|
| 43 |
-
<Link href="/auth/signup" className="bg-zinc-900 text-white px-3.5 py-1.5 rounded-md text-sm hover:bg-zinc-800">Get started</Link>
|
| 44 |
-
</div>
|
| 45 |
-
</div>
|
| 46 |
-
</nav>
|
| 47 |
-
|
| 48 |
{/* Hero */}
|
| 49 |
-
<section className="max-w-
|
| 50 |
<div className="max-w-2xl">
|
| 51 |
-
<
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
| 54 |
</h1>
|
| 55 |
-
<p className="mt-5 text-
|
| 56 |
ClauseGuard scans Terms of Service, contracts, and leases for unfair clauses.
|
| 57 |
-
|
| 58 |
</p>
|
| 59 |
<div className="mt-8 flex flex-wrap gap-3">
|
| 60 |
-
<a href="#" className="bg-zinc-900 text-white px-5 py-2.5 rounded-
|
|
|
|
| 61 |
Add to Chrome
|
| 62 |
</a>
|
| 63 |
-
<Link href="/dashboard-pages/analyze"
|
| 64 |
-
|
|
|
|
|
|
|
| 65 |
</Link>
|
| 66 |
</div>
|
|
|
|
| 67 |
</div>
|
| 68 |
</section>
|
| 69 |
|
| 70 |
{/* What it detects */}
|
| 71 |
<section id="features" className="border-t border-zinc-100">
|
| 72 |
-
<div className="max-w-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
<h2 className="text-2xl font-semibold tracking-tight">Eight types of unfair clauses</h2>
|
| 74 |
-
<p className="mt-2 text-zinc-500 max-w-lg">
|
| 75 |
-
Based on the CLAUDETTE
|
| 76 |
</p>
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
</div>
|
| 83 |
))}
|
| 84 |
</div>
|
|
@@ -86,19 +102,25 @@ export default function Home() {
|
|
| 86 |
</section>
|
| 87 |
|
| 88 |
{/* How it works */}
|
| 89 |
-
<section className="border-t border-zinc-100">
|
| 90 |
-
<div className="max-w-
|
| 91 |
-
<
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
<
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
</div>
|
| 103 |
))}
|
| 104 |
</div>
|
|
@@ -107,25 +129,36 @@ export default function Home() {
|
|
| 107 |
|
| 108 |
{/* Pricing */}
|
| 109 |
<section id="pricing" className="border-t border-zinc-100">
|
| 110 |
-
<div className="max-w-
|
| 111 |
<h2 className="text-2xl font-semibold tracking-tight">Pricing</h2>
|
| 112 |
-
<p className="mt-2 text-zinc-500">Free forever. Upgrade
|
| 113 |
-
|
|
|
|
| 114 |
{PRICING.map((plan) => (
|
| 115 |
-
<div key={plan.name}
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
<span className="text-sm text-zinc-400">{plan.period}</span>
|
| 120 |
</p>
|
| 121 |
-
<ul className="mt-5 space-y-2">
|
| 122 |
{plan.features.map((f) => (
|
| 123 |
-
<li key={f} className="
|
| 124 |
-
<
|
|
|
|
| 125 |
</li>
|
| 126 |
))}
|
| 127 |
</ul>
|
| 128 |
-
<button className={`mt-6 w-full py-2 rounded-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
{plan.cta}
|
| 130 |
</button>
|
| 131 |
</div>
|
|
@@ -134,11 +167,31 @@ export default function Home() {
|
|
| 134 |
</div>
|
| 135 |
</section>
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
{/* Footer */}
|
| 138 |
<footer className="border-t border-zinc-100">
|
| 139 |
-
<div className="max-w-
|
| 140 |
-
<
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
| 142 |
<a href="/privacy" className="hover:text-zinc-600">Privacy</a>
|
| 143 |
<a href="/terms" className="hover:text-zinc-600">Terms</a>
|
| 144 |
<a href="mailto:hello@clauseguard.com" className="hover:text-zinc-600">Contact</a>
|
|
|
|
| 1 |
import Link from "next/link";
|
| 2 |
+
import {
|
| 3 |
+
ShieldCheck, ShieldAlert, Scale, Gavel, ScrollText, Handshake,
|
| 4 |
+
ScanText, FileCheck, TriangleAlert, ArrowRight, Zap, Eye, Download,
|
| 5 |
+
ChevronRight, Sparkles, Lock, Globe, Ban, FileX, Stamp
|
| 6 |
+
} from "lucide-react";
|
| 7 |
|
| 8 |
+
const CLAUSES = [
|
| 9 |
+
{ icon: Scale, name: "Arbitration", desc: "Waives your right to sue in court", severity: "high" },
|
| 10 |
+
{ icon: ShieldAlert, name: "Liability limits", desc: "Company avoids responsibility for damages", severity: "high" },
|
| 11 |
+
{ icon: Ban, name: "Unilateral termination", desc: "They can close your account without reason", severity: "high" },
|
| 12 |
+
{ icon: FileX, name: "Unilateral change", desc: "Terms can change without your consent", severity: "medium" },
|
| 13 |
+
{ icon: Eye, name: "Content removal", desc: "Your content deleted without notice", severity: "medium" },
|
| 14 |
+
{ icon: Globe, name: "Jurisdiction", desc: "Disputes handled in their preferred court", severity: "medium" },
|
| 15 |
+
{ icon: Gavel, name: "Choice of law", desc: "Foreign law overrides your local protections", severity: "medium" },
|
| 16 |
+
{ icon: Stamp, name: "Contract by using", desc: "You agree just by visiting the site", severity: "low" },
|
| 17 |
+
];
|
| 18 |
+
|
| 19 |
+
const STEPS = [
|
| 20 |
+
{ icon: Download, title: "Install", desc: "Add the Chrome extension. Two clicks, no signup required." },
|
| 21 |
+
{ icon: ScanText, title: "Browse normally", desc: "Visit any terms page. ClauseGuard scans it in the background." },
|
| 22 |
+
{ icon: TriangleAlert, title: "See the flags", desc: "Unfair clauses get highlighted. Open the sidebar for the full breakdown." },
|
| 23 |
];
|
| 24 |
|
| 25 |
const PRICING = [
|
| 26 |
{
|
| 27 |
+
name: "Free", price: "$0", period: "", highlight: false, cta: "Get started",
|
| 28 |
+
features: ["10 scans per month", "All 8 clause types", "Risk score and grade", "Chrome extension", "Local fallback"],
|
|
|
|
| 29 |
},
|
| 30 |
{
|
| 31 |
+
name: "Pro", price: "$12", period: "/mo", highlight: true, cta: "Start free trial",
|
| 32 |
+
features: ["Unlimited scans", "Upload contracts and leases", "AI clause explanations", "Scan history", "PDF report export", "Email scan reports", "Priority support"],
|
|
|
|
| 33 |
},
|
| 34 |
{
|
| 35 |
+
name: "Team", price: "$49", period: "/mo", highlight: false, cta: "Talk to us",
|
| 36 |
+
features: ["Everything in Pro", "5 team seats", "10,000 API calls", "Shared dashboard", "Slack support", "Custom clause rules"],
|
|
|
|
| 37 |
},
|
| 38 |
];
|
| 39 |
|
| 40 |
+
const sevColor: Record<string, string> = {
|
| 41 |
+
high: "text-red-500 bg-red-50",
|
| 42 |
+
medium: "text-amber-500 bg-amber-50",
|
| 43 |
+
low: "text-blue-500 bg-blue-50",
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
export default function Home() {
|
| 47 |
return (
|
| 48 |
<main className="min-h-screen bg-white text-zinc-900">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
{/* Hero */}
|
| 50 |
+
<section className="max-w-6xl mx-auto px-5 pt-24 pb-20">
|
| 51 |
<div className="max-w-2xl">
|
| 52 |
+
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-zinc-200 text-[13px] text-zinc-500 mb-6">
|
| 53 |
+
<Sparkles className="w-3.5 h-3.5 text-zinc-400" />
|
| 54 |
+
Trained on 9,414 legal clauses
|
| 55 |
+
</div>
|
| 56 |
+
<h1 className="text-[42px] sm:text-5xl font-semibold tracking-tight leading-[1.1]">
|
| 57 |
+
Know what you are<br />agreeing to
|
| 58 |
</h1>
|
| 59 |
+
<p className="mt-5 text-[17px] text-zinc-500 leading-relaxed max-w-lg">
|
| 60 |
ClauseGuard scans Terms of Service, contracts, and leases for unfair clauses.
|
| 61 |
+
Get a clear risk score before you click accept.
|
| 62 |
</p>
|
| 63 |
<div className="mt-8 flex flex-wrap gap-3">
|
| 64 |
+
<a href="#" className="inline-flex items-center gap-2 bg-zinc-900 text-white px-5 py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 transition-colors">
|
| 65 |
+
<Download className="w-4 h-4" />
|
| 66 |
Add to Chrome
|
| 67 |
</a>
|
| 68 |
+
<Link href="/dashboard-pages/analyze"
|
| 69 |
+
className="inline-flex items-center gap-2 border border-zinc-200 px-5 py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-50 transition-colors">
|
| 70 |
+
Try the scanner
|
| 71 |
+
<ArrowRight className="w-4 h-4" />
|
| 72 |
</Link>
|
| 73 |
</div>
|
| 74 |
+
<p className="mt-4 text-xs text-zinc-400">No account needed for free tier</p>
|
| 75 |
</div>
|
| 76 |
</section>
|
| 77 |
|
| 78 |
{/* What it detects */}
|
| 79 |
<section id="features" className="border-t border-zinc-100">
|
| 80 |
+
<div className="max-w-6xl mx-auto px-5 py-20">
|
| 81 |
+
<div className="flex items-center gap-2 mb-2">
|
| 82 |
+
<ShieldCheck className="w-4 h-4 text-zinc-400" />
|
| 83 |
+
<p className="text-[13px] font-medium text-zinc-400 uppercase tracking-wider">Detection</p>
|
| 84 |
+
</div>
|
| 85 |
<h2 className="text-2xl font-semibold tracking-tight">Eight types of unfair clauses</h2>
|
| 86 |
+
<p className="mt-2 text-zinc-500 text-[15px] max-w-lg">
|
| 87 |
+
Based on the CLAUDETTE taxonomy — the same framework used by EU consumer protection researchers.
|
| 88 |
</p>
|
| 89 |
+
|
| 90 |
+
<div className="mt-10 grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
| 91 |
+
{CLAUSES.map((c) => (
|
| 92 |
+
<div key={c.name} className="group border border-zinc-100 rounded-xl p-4 hover:border-zinc-200 hover:shadow-sm transition-all cursor-default">
|
| 93 |
+
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${sevColor[c.severity]}`}>
|
| 94 |
+
<c.icon className="w-4 h-4" />
|
| 95 |
+
</div>
|
| 96 |
+
<p className="mt-3 text-sm font-medium">{c.name}</p>
|
| 97 |
+
<p className="mt-1 text-[13px] text-zinc-500 leading-relaxed">{c.desc}</p>
|
| 98 |
</div>
|
| 99 |
))}
|
| 100 |
</div>
|
|
|
|
| 102 |
</section>
|
| 103 |
|
| 104 |
{/* How it works */}
|
| 105 |
+
<section className="border-t border-zinc-100 bg-zinc-50/50">
|
| 106 |
+
<div className="max-w-6xl mx-auto px-5 py-20">
|
| 107 |
+
<div className="flex items-center gap-2 mb-2">
|
| 108 |
+
<Zap className="w-4 h-4 text-zinc-400" />
|
| 109 |
+
<p className="text-[13px] font-medium text-zinc-400 uppercase tracking-wider">How it works</p>
|
| 110 |
+
</div>
|
| 111 |
+
<h2 className="text-2xl font-semibold tracking-tight">Three steps, under two seconds</h2>
|
| 112 |
+
|
| 113 |
+
<div className="mt-10 grid sm:grid-cols-3 gap-8">
|
| 114 |
+
{STEPS.map((s, i) => (
|
| 115 |
+
<div key={s.title} className="relative">
|
| 116 |
+
<div className="w-10 h-10 rounded-xl bg-white border border-zinc-200 flex items-center justify-center shadow-sm">
|
| 117 |
+
<s.icon className="w-5 h-5 text-zinc-600" />
|
| 118 |
+
</div>
|
| 119 |
+
<h3 className="mt-4 text-[15px] font-medium">{s.title}</h3>
|
| 120 |
+
<p className="mt-1.5 text-[13px] text-zinc-500 leading-relaxed">{s.desc}</p>
|
| 121 |
+
{i < 2 && (
|
| 122 |
+
<ChevronRight className="hidden sm:block absolute top-4 -right-5 w-4 h-4 text-zinc-300" />
|
| 123 |
+
)}
|
| 124 |
</div>
|
| 125 |
))}
|
| 126 |
</div>
|
|
|
|
| 129 |
|
| 130 |
{/* Pricing */}
|
| 131 |
<section id="pricing" className="border-t border-zinc-100">
|
| 132 |
+
<div className="max-w-6xl mx-auto px-5 py-20">
|
| 133 |
<h2 className="text-2xl font-semibold tracking-tight">Pricing</h2>
|
| 134 |
+
<p className="mt-2 text-zinc-500 text-[15px]">Free forever. Upgrade when you need more.</p>
|
| 135 |
+
|
| 136 |
+
<div className="mt-10 grid sm:grid-cols-3 gap-5 max-w-4xl">
|
| 137 |
{PRICING.map((plan) => (
|
| 138 |
+
<div key={plan.name}
|
| 139 |
+
className={`rounded-xl p-6 transition-shadow ${
|
| 140 |
+
plan.highlight
|
| 141 |
+
? "border-2 border-zinc-900 shadow-sm"
|
| 142 |
+
: "border border-zinc-200"
|
| 143 |
+
}`}>
|
| 144 |
+
<p className="text-[13px] font-medium text-zinc-400">{plan.name}</p>
|
| 145 |
+
<p className="mt-2 flex items-baseline gap-1">
|
| 146 |
+
<span className="text-3xl font-semibold tracking-tight">{plan.price}</span>
|
| 147 |
<span className="text-sm text-zinc-400">{plan.period}</span>
|
| 148 |
</p>
|
| 149 |
+
<ul className="mt-5 space-y-2.5">
|
| 150 |
{plan.features.map((f) => (
|
| 151 |
+
<li key={f} className="flex items-start gap-2.5 text-[13px] text-zinc-600">
|
| 152 |
+
<FileCheck className="w-3.5 h-3.5 text-zinc-300 mt-0.5 shrink-0" />
|
| 153 |
+
{f}
|
| 154 |
</li>
|
| 155 |
))}
|
| 156 |
</ul>
|
| 157 |
+
<button className={`mt-6 w-full py-2.5 rounded-lg text-[13px] font-medium transition-colors ${
|
| 158 |
+
plan.highlight
|
| 159 |
+
? "bg-zinc-900 text-white hover:bg-zinc-800"
|
| 160 |
+
: "border border-zinc-200 text-zinc-700 hover:bg-zinc-50"
|
| 161 |
+
}`}>
|
| 162 |
{plan.cta}
|
| 163 |
</button>
|
| 164 |
</div>
|
|
|
|
| 167 |
</div>
|
| 168 |
</section>
|
| 169 |
|
| 170 |
+
{/* CTA */}
|
| 171 |
+
<section className="border-t border-zinc-100 bg-zinc-50/50">
|
| 172 |
+
<div className="max-w-6xl mx-auto px-5 py-16 text-center">
|
| 173 |
+
<Lock className="w-6 h-6 text-zinc-300 mx-auto mb-4" />
|
| 174 |
+
<h2 className="text-2xl font-semibold tracking-tight">Read the fine print without reading it</h2>
|
| 175 |
+
<p className="mt-2 text-[15px] text-zinc-500 max-w-md mx-auto">
|
| 176 |
+
Join thousands protecting themselves before clicking accept.
|
| 177 |
+
</p>
|
| 178 |
+
<div className="mt-6">
|
| 179 |
+
<a href="#" className="inline-flex items-center gap-2 bg-zinc-900 text-white px-6 py-3 rounded-lg text-sm font-medium hover:bg-zinc-800 transition-colors">
|
| 180 |
+
<Download className="w-4 h-4" />
|
| 181 |
+
Add to Chrome — free
|
| 182 |
+
</a>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
</section>
|
| 186 |
+
|
| 187 |
{/* Footer */}
|
| 188 |
<footer className="border-t border-zinc-100">
|
| 189 |
+
<div className="max-w-6xl mx-auto px-5 py-8 flex flex-col sm:flex-row justify-between items-center gap-4">
|
| 190 |
+
<div className="flex items-center gap-2">
|
| 191 |
+
<ShieldCheck className="w-4 h-4 text-zinc-300" />
|
| 192 |
+
<span className="text-[13px] text-zinc-400">ClauseGuard — not legal advice</span>
|
| 193 |
+
</div>
|
| 194 |
+
<div className="flex gap-5 text-[13px] text-zinc-400">
|
| 195 |
<a href="/privacy" className="hover:text-zinc-600">Privacy</a>
|
| 196 |
<a href="/terms" className="hover:text-zinc-600">Terms</a>
|
| 197 |
<a href="mailto:hello@clauseguard.com" className="hover:text-zinc-600">Contact</a>
|
web/components/nav.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
import { usePathname } from "next/navigation";
|
| 5 |
+
import { ShieldCheck, Menu, X } from "lucide-react";
|
| 6 |
+
import { useState } from "react";
|
| 7 |
+
|
| 8 |
+
const links = [
|
| 9 |
+
{ href: "/#features", label: "Features" },
|
| 10 |
+
{ href: "/#pricing", label: "Pricing" },
|
| 11 |
+
{ href: "/dashboard-pages/analyze", label: "Scanner" },
|
| 12 |
+
];
|
| 13 |
+
|
| 14 |
+
export function Nav() {
|
| 15 |
+
const [open, setOpen] = useState(false);
|
| 16 |
+
const pathname = usePathname();
|
| 17 |
+
const isDashboard = pathname?.startsWith("/dashboard");
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-zinc-100">
|
| 21 |
+
<div className="max-w-6xl mx-auto px-5 h-14 flex items-center justify-between">
|
| 22 |
+
<Link href="/" className="flex items-center gap-2">
|
| 23 |
+
<ShieldCheck className="w-5 h-5 text-zinc-900" strokeWidth={2.2} />
|
| 24 |
+
<span className="font-semibold text-[15px] tracking-tight text-zinc-900">ClauseGuard</span>
|
| 25 |
+
</Link>
|
| 26 |
+
|
| 27 |
+
{/* Desktop */}
|
| 28 |
+
<div className="hidden md:flex items-center gap-1">
|
| 29 |
+
{links.map((l) => (
|
| 30 |
+
<a key={l.href} href={l.href}
|
| 31 |
+
className="px-3 py-1.5 text-[13px] text-zinc-500 hover:text-zinc-900 rounded-md hover:bg-zinc-50 transition-colors">
|
| 32 |
+
{l.label}
|
| 33 |
+
</a>
|
| 34 |
+
))}
|
| 35 |
+
<div className="w-px h-4 bg-zinc-200 mx-2" />
|
| 36 |
+
{isDashboard ? (
|
| 37 |
+
<Link href="/dashboard-pages/settings"
|
| 38 |
+
className="px-3 py-1.5 text-[13px] text-zinc-500 hover:text-zinc-900 rounded-md hover:bg-zinc-50">
|
| 39 |
+
Settings
|
| 40 |
+
</Link>
|
| 41 |
+
) : (
|
| 42 |
+
<Link href="/auth/login"
|
| 43 |
+
className="px-3 py-1.5 text-[13px] text-zinc-500 hover:text-zinc-900 rounded-md hover:bg-zinc-50">
|
| 44 |
+
Log in
|
| 45 |
+
</Link>
|
| 46 |
+
)}
|
| 47 |
+
<Link href={isDashboard ? "/dashboard-pages/analyze" : "/auth/signup"}
|
| 48 |
+
className="ml-1 px-3.5 py-1.5 text-[13px] font-medium text-white bg-zinc-900 rounded-md hover:bg-zinc-800 transition-colors">
|
| 49 |
+
{isDashboard ? "New scan" : "Get started"}
|
| 50 |
+
</Link>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
{/* Mobile toggle */}
|
| 54 |
+
<button className="md:hidden p-1.5 rounded-md hover:bg-zinc-100" onClick={() => setOpen(!open)}>
|
| 55 |
+
{open ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
| 56 |
+
</button>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
{/* Mobile menu */}
|
| 60 |
+
{open && (
|
| 61 |
+
<div className="md:hidden border-t border-zinc-100 bg-white px-5 py-3 space-y-1">
|
| 62 |
+
{links.map((l) => (
|
| 63 |
+
<a key={l.href} href={l.href} onClick={() => setOpen(false)}
|
| 64 |
+
className="block px-3 py-2 text-sm text-zinc-600 rounded-md hover:bg-zinc-50">
|
| 65 |
+
{l.label}
|
| 66 |
+
</a>
|
| 67 |
+
))}
|
| 68 |
+
<Link href="/auth/login" onClick={() => setOpen(false)}
|
| 69 |
+
className="block px-3 py-2 text-sm text-zinc-600 rounded-md hover:bg-zinc-50">Log in</Link>
|
| 70 |
+
</div>
|
| 71 |
+
)}
|
| 72 |
+
</nav>
|
| 73 |
+
);
|
| 74 |
+
}
|