Skip to content

Commit a359aca

Browse files
committed
ADD - Airtable sync and status sync pg
1 parent 146780e commit a359aca

7 files changed

Lines changed: 572 additions & 0 deletions

File tree

client/src/components/TestPage.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,52 @@
2424
font-size: 1.1rem;
2525
}
2626

27+
.test-page__sync {
28+
display: flex;
29+
align-items: center;
30+
justify-content: space-between;
31+
gap: 16px;
32+
margin: 0 0 24px;
33+
padding: 16px;
34+
border: 3px solid #162456;
35+
border-radius: 16px;
36+
background: #ffffff;
37+
}
38+
39+
.test-page__sync div {
40+
display: grid;
41+
gap: 4px;
42+
}
43+
44+
.test-page__sync strong,
45+
.test-page__sync span {
46+
font-size: 1rem;
47+
}
48+
49+
.test-page__sync button {
50+
padding: 10px 18px;
51+
border: 3px solid #162456;
52+
border-radius: 999px;
53+
background: #ffd84d;
54+
color: #162456;
55+
font: inherit;
56+
cursor: pointer;
57+
box-shadow: 4px 4px 0 #162456;
58+
}
59+
60+
.test-page__sync button:disabled {
61+
cursor: not-allowed;
62+
opacity: 0.65;
63+
}
64+
2765
.test-page__error {
2866
color: #b3261e;
2967
}
3068

69+
.test-page__success {
70+
color: #146c2e;
71+
}
72+
3173
.test-page__rows {
3274
display: grid;
3375
gap: 16px;
@@ -43,3 +85,10 @@
4385
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
4486
font-size: 0.95rem;
4587
}
88+
89+
@media (max-width: 600px) {
90+
.test-page__sync {
91+
align-items: stretch;
92+
flex-direction: column;
93+
}
94+
}

client/src/components/TestPage.jsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export function TestPage() {
55
const [rows, setRows] = useState([]);
66
const [status, setStatus] = useState("Loading test rows...");
77
const [error, setError] = useState("");
8+
const [syncStatus, setSyncStatus] = useState(null);
9+
const [syncError, setSyncError] = useState("");
10+
const [syncMessage, setSyncMessage] = useState("");
11+
const [isSyncing, setIsSyncing] = useState(false);
812

913
useEffect(() => {
1014
let isMounted = true;
@@ -31,19 +35,113 @@ export function TestPage() {
3135
}
3236
}
3337

38+
async function loadSyncStatus() {
39+
try {
40+
const response = await fetch("/api/airtable/status");
41+
const isJson = response.headers.get("content-type")?.includes("application/json");
42+
const data = isJson ? await response.json() : {};
43+
44+
if (!response.ok) {
45+
throw new Error(data.error || "Failed to load sync status.");
46+
}
47+
48+
if (isMounted) {
49+
setSyncStatus(data);
50+
setSyncError("");
51+
}
52+
} catch (err) {
53+
if (isMounted) {
54+
setSyncError(err.message);
55+
}
56+
}
57+
}
58+
3459
loadRows();
60+
loadSyncStatus();
3561

3662
return () => {
3763
isMounted = false;
3864
};
3965
}, []);
4066

67+
async function loadRows() {
68+
const response = await fetch("/api/test");
69+
const isJson = response.headers.get("content-type")?.includes("application/json");
70+
const data = isJson ? await response.json() : {};
71+
72+
if (!response.ok) {
73+
throw new Error(data.error || "Failed to load test rows.");
74+
}
75+
76+
setRows(data.rows || []);
77+
}
78+
79+
async function loadSyncStatus() {
80+
const response = await fetch("/api/airtable/status");
81+
const isJson = response.headers.get("content-type")?.includes("application/json");
82+
const data = isJson ? await response.json() : {};
83+
84+
if (!response.ok) {
85+
throw new Error(data.error || "Failed to load sync status.");
86+
}
87+
88+
setSyncStatus(data);
89+
setSyncError("");
90+
return data;
91+
}
92+
93+
async function handleSyncNow() {
94+
setIsSyncing(true);
95+
setSyncError("");
96+
setSyncMessage("");
97+
98+
try {
99+
const response = await fetch("/api/airtable/sync", {
100+
method: "POST",
101+
});
102+
const isJson = response.headers.get("content-type")?.includes("application/json");
103+
const data = isJson ? await response.json() : {};
104+
105+
if (!response.ok) {
106+
throw new Error(data.error || data.message || "Failed to sync Airtable.");
107+
}
108+
109+
setSyncMessage("Sync finished.");
110+
setSyncStatus((currentStatus) => ({
111+
...currentStatus,
112+
lastSync: data,
113+
}));
114+
await Promise.all([loadRows(), loadSyncStatus()]);
115+
} catch (err) {
116+
setSyncError(err.message);
117+
} finally {
118+
setIsSyncing(false);
119+
}
120+
}
121+
122+
const lastSyncTime = syncStatus?.lastSync?.syncedAt
123+
? new Date(syncStatus.lastSync.syncedAt).toLocaleString()
124+
: "Never synced";
125+
41126
return (
42127
<main className="test-page">
43128
<section className="test-page__card">
44129
<h1>Test Table</h1>
45130
<p>Rows from the production Postgres <code>test</code> table.</p>
46131

132+
<div className="test-page__sync">
133+
<div>
134+
<strong>Airtable sync</strong>
135+
<span>Last update: {lastSyncTime}</span>
136+
</div>
137+
<button type="button" onClick={handleSyncNow} disabled={isSyncing}>
138+
{isSyncing ? "Syncing..." : "Sync now"}
139+
</button>
140+
</div>
141+
142+
{syncMessage && <p className="test-page__success">{syncMessage}</p>}
143+
{syncError && <p className="test-page__error">{syncError}</p>}
144+
47145
{status && <p>{status}</p>}
48146
{error && <p className="test-page__error">{error}</p>}
49147

package-lock.json

Lines changed: 100 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@
1313
},
1414
"devDependencies": {
1515
"concurrently": "^9.1.0"
16+
},
17+
"dependencies": {
18+
"airtable": "^0.12.2"
1619
}
1720
}

0 commit comments

Comments
 (0)