Skip to content

Commit 6d263d5

Browse files
committed
fix(web,api): confirm host mount, reconnect flow, sheet preview, better errors
- mount ConfirmHost in dashboard so delete modals actually render - GoogleReconnectRequiredError (401 + code) distinguishes expired grant; api-client auto-redirects to /signin?reconnect=1 with tailored copy - ApiError surfaces body.error.message instead of bare "API 500" - GET /projects/:id/sheets/:id/preview + UI table showing live rows - remove broken hero video + dead newsletter form
1 parent 83b78cd commit 6d263d5

8 files changed

Lines changed: 293 additions & 62 deletions

File tree

apps/web/src/app/app/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import Link from "next/link";
44
import { useEffect, useState } from "react";
5-
import { ToastHost, pushToast } from "@/components/ui";
5+
import { ConfirmHost, ToastHost, pushToast } from "@/components/ui";
66
import { ApiError, getMe, logout } from "@/lib/api-client";
77

88
interface User {
@@ -151,6 +151,7 @@ export default function DashboardLayout({
151151
<div className="px-[80px] py-[48px]">{children}</div>
152152
</div>
153153
<ToastHost />
154+
<ConfirmHost />
154155
</main>
155156
);
156157
}

apps/web/src/app/app/projects/[projectId]/sheets/[sheetId]/page.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
} from "@/components/ui";
1212
import {
1313
type LedgerStats,
14+
type PreviewResult,
1415
type SchemaSnapshot,
1516
type TestWriteResult,
1617
disconnectSheet,
1718
getLedgerStats,
1819
getSchema,
20+
previewSheet,
1921
refreshSchema,
2022
sdkUrl,
2123
testWrite,
@@ -46,6 +48,9 @@ export default function SheetDetailPage() {
4648

4749
const [schema, setSchema] = useState<SchemaSnapshot | null>(null);
4850
const [ledger, setLedger] = useState<LedgerStats | null>(null);
51+
const [preview, setPreview] = useState<PreviewResult | null>(null);
52+
const [previewError, setPreviewError] = useState<string | null>(null);
53+
const [previewLoading, setPreviewLoading] = useState(false);
4954
const [error, setError] = useState<string | null>(null);
5055
const [refreshing, setRefreshing] = useState(false);
5156
const [submitting, setSubmitting] = useState(false);
@@ -109,6 +114,27 @@ export default function SheetDetailPage() {
109114
}
110115
}
111116

117+
async function loadPreview() {
118+
setPreviewLoading(true);
119+
setPreviewError(null);
120+
try {
121+
const result = await previewSheet(projectId, sheetId, 10);
122+
setPreview(result);
123+
} catch (err) {
124+
setPreviewError(err instanceof Error ? err.message : "unknown error");
125+
} finally {
126+
setPreviewLoading(false);
127+
}
128+
}
129+
130+
// Auto-load preview once the schema is available so the row grid shows up
131+
// without an extra click.
132+
useEffect(() => {
133+
if (schema === null) return;
134+
loadPreview();
135+
// eslint-disable-next-line react-hooks/exhaustive-deps
136+
}, [schema === null ? null : schema.id]);
137+
112138
const downloadUrl = sdkUrl(projectId, sheetId);
113139

114140
return (
@@ -347,6 +373,93 @@ export default function SheetDetailPage() {
347373
)}
348374
</section>
349375

376+
{/* ── Row preview ────────────────────────────────────── */}
377+
<section>
378+
<div className="flex items-center justify-between mb-4">
379+
<h2 className="text-[16px] font-bold">Row preview</h2>
380+
<button
381+
type="button"
382+
onClick={loadPreview}
383+
disabled={previewLoading || schema === null}
384+
className="rounded px-4 py-2 font-medium border transition-colors disabled:opacity-50"
385+
style={{ borderColor: "#3d3838", color: "#b8b2b2" }}
386+
>
387+
{previewLoading ? "Loading…" : "refresh preview"}
388+
</button>
389+
</div>
390+
<p style={{ color: "#b8b2b2" }} className="text-sm mb-4">
391+
First 10 rows read through the API — the same path an API-key client
392+
hits. Verifies the whole read loop end-to-end without curl.
393+
</p>
394+
395+
{previewError && (
396+
<p style={{ color: "#b8b2b2" }} className="text-sm mb-4">
397+
[!] {previewError}
398+
</p>
399+
)}
400+
401+
{preview === null && !previewError && (
402+
<p style={{ color: "#7f7a7a" }} className="text-sm">
403+
[*] loading preview…
404+
</p>
405+
)}
406+
407+
{preview !== null && preview.rows.length === 0 && (
408+
<p style={{ color: "#7f7a7a" }} className="text-sm">
409+
sheet has no data rows yet — use the Test write button above
410+
</p>
411+
)}
412+
413+
{preview !== null && preview.rows.length > 0 && (
414+
<div
415+
className="border rounded overflow-x-auto"
416+
style={{ borderColor: "#3d3838", backgroundColor: "#1b1818" }}
417+
>
418+
<table className="w-full text-xs">
419+
<thead>
420+
<tr
421+
className="border-b"
422+
style={{ borderColor: "#3d3838" }}
423+
>
424+
{preview.columns.map((col) => (
425+
<th
426+
key={col.name}
427+
className="text-left px-4 py-2 font-medium"
428+
style={{ color: "#7f7a7a" }}
429+
>
430+
{col.name}{" "}
431+
<span style={{ color: "#4a4545" }}>: {col.type}</span>
432+
</th>
433+
))}
434+
</tr>
435+
</thead>
436+
<tbody>
437+
{preview.rows.map((row, i) => (
438+
<tr
439+
key={i}
440+
className="border-b last:border-b-0"
441+
style={{ borderColor: "#3d3838" }}
442+
>
443+
{preview.columns.map((col) => {
444+
const v = row[col.name];
445+
return (
446+
<td
447+
key={col.name}
448+
className="px-4 py-2"
449+
style={{ color: v == null ? "#4a4545" : "#b8b2b2" }}
450+
>
451+
{v == null ? "—" : String(v)}
452+
</td>
453+
);
454+
})}
455+
</tr>
456+
))}
457+
</tbody>
458+
</table>
459+
</div>
460+
)}
461+
</section>
462+
350463
{/* ── SDK ────────────────────────────────────────────── */}
351464
<section>
352465
<h2 className="text-[16px] font-bold mb-4">Typed TypeScript SDK</h2>

apps/web/src/app/page.tsx

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -258,24 +258,6 @@ function HeroSection({ authStatus }: { authStatus: AuthStatus }) {
258258
);
259259
}
260260

261-
function VideoSection() {
262-
return (
263-
<section className="border-b" style={{ borderColor: "#3d3838" }}>
264-
<video
265-
autoPlay
266-
loop
267-
muted
268-
playsInline
269-
poster="/images/sheetforge-poster.png"
270-
className="w-full"
271-
>
272-
<source src="/videos/sheetforge-demo.mp4" type="video/mp4" />
273-
Your browser does not support the video tag.
274-
</video>
275-
</section>
276-
);
277-
}
278-
279261
function FeaturesSection() {
280262
return (
281263
<section className="px-[80px] py-[64px] border-b" style={{ borderColor: "#3d3838" }}>
@@ -394,31 +376,6 @@ function ZenSection() {
394376
);
395377
}
396378

397-
function NewsletterSection() {
398-
return (
399-
<section className="px-[80px] py-[64px] border-b" style={{ borderColor: "#3d3838" }}>
400-
<h3 className="text-[16px] font-bold text-[#f2eded] mb-3">
401-
Join the waitlist for the HN launch
402-
</h3>
403-
<p className="text-[#b8b2b2] mb-6 leading-[24px]">Get notified when we go live — early access, no spam.</p>
404-
<div className="flex gap-3">
405-
<input
406-
type="email"
407-
placeholder="Email"
408-
className="border rounded px-4 py-2 bg-transparent text-[#f2eded] placeholder-[#7f7a7a] flex-1 max-w-sm focus:outline-none focus:border-[#7f7a7a] transition-colors"
409-
style={{ borderColor: "#3d3838" }}
410-
/>
411-
<button
412-
className="rounded px-6 py-2 font-medium transition-opacity hover:opacity-90"
413-
style={{ backgroundColor: "#f2eded", color: "#131010" }}
414-
>
415-
Subscribe
416-
</button>
417-
</div>
418-
</section>
419-
);
420-
}
421-
422379
function Footer() {
423380
return (
424381
<footer className="px-[80px] py-4 flex items-center justify-between text-sm text-[#4a4545]">
@@ -449,13 +406,11 @@ export default function Home() {
449406
>
450407
<Header authStatus={authStatus} />
451408
<HeroSection authStatus={authStatus} />
452-
<VideoSection />
453409
<FeaturesSection />
454410
<StatsSection />
455411
<PrivacySection />
456412
<FAQSection />
457413
<ZenSection />
458-
<NewsletterSection />
459414
</div>
460415
<div className="mx-auto" style={{ maxWidth: "1080px" }}>
461416
<Footer />

apps/web/src/app/signin/page.tsx

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,26 @@ import { ApiError, getMe, loginUrl } from "@/lib/api-client";
77

88
export default function SignInPage() {
99
const [checking, setChecking] = useState(true);
10+
const [reconnect, setReconnect] = useState(false);
1011

11-
// If you already have a live session, bounce straight to the dashboard.
1212
useEffect(() => {
13+
// Reading window.location on mount avoids the useSearchParams Suspense
14+
// requirement introduced in Next 15+.
15+
setReconnect(
16+
new URL(window.location.href).searchParams.get("reconnect") === "1",
17+
);
18+
}, []);
19+
20+
// If you already have a live session, bounce straight to the dashboard —
21+
// unless we're explicitly in reconnect mode (stale Google grant), in which
22+
// case the user needs to re-authorize even with a valid session cookie.
23+
useEffect(() => {
24+
const isReconnect =
25+
new URL(window.location.href).searchParams.get("reconnect") === "1";
26+
if (isReconnect) {
27+
setChecking(false);
28+
return;
29+
}
1330
getMe()
1431
.then(() => {
1532
window.location.href = "/app";
@@ -19,7 +36,6 @@ export default function SignInPage() {
1936
setChecking(false);
2037
return;
2138
}
22-
// API unreachable or unknown error — let them try the button anyway.
2339
setChecking(false);
2440
});
2541
}, []);
@@ -58,23 +74,40 @@ export default function SignInPage() {
5874
}}
5975
>
6076
<h1 className="text-[28px] font-bold leading-[36px] mb-2">
61-
Sign in to sheetforge
77+
{reconnect
78+
? "Re-authorize Google"
79+
: "Sign in to sheetforge"}
6280
</h1>
63-
<p
64-
style={{ color: "#b8b2b2" }}
65-
className="text-sm mb-8 leading-[20px]"
66-
>
67-
Connect your Google account to turn any Sheet into a
68-
race-condition-safe API.
69-
</p>
81+
{reconnect ? (
82+
<p
83+
style={{ color: "#b8b2b2" }}
84+
className="text-sm mb-8 leading-[20px]"
85+
>
86+
[!] Your Google connection expired or was revoked.
87+
Re-authorize below to get your projects back online — nothing
88+
in your sheetforge account is lost.
89+
</p>
90+
) : (
91+
<p
92+
style={{ color: "#b8b2b2" }}
93+
className="text-sm mb-8 leading-[20px]"
94+
>
95+
Connect your Google account to turn any Sheet into a
96+
race-condition-safe API.
97+
</p>
98+
)}
7099

71100
<a
72101
href={loginUrl()}
73102
className="flex items-center justify-center gap-3 w-full rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
74103
style={{ backgroundColor: "#f2eded", color: "#131010" }}
75104
>
76105
<GoogleGlyph />
77-
{checking ? "Checking session…" : "Sign in with Google"}
106+
{checking
107+
? "Checking session…"
108+
: reconnect
109+
? "Reconnect Google"
110+
: "Sign in with Google"}
78111
</a>
79112

80113
<div

0 commit comments

Comments
 (0)