fix(home): resilient homepage animations + anchors + counters
Subagent 1 (Codex review pass): LandingPageClient.tsx - Guard GSAP .scroll-reveal loop to prevent 'missing target' warnings when no targets exist brands/page.tsx - Comment cleanup for selector scoping tuxedo/page.tsx - Add pageScopeRef + use as explicit GSAP context scope (fixes 'Invalid scope' warnings) - Reduced-motion guard for counter animations - Comments noting scope is now resilient HeroSection.tsx - Wrap sections in containerRef for proper GSAP context - Reduced-motion support (final states shown immediately, no heavy anims) - Counter animation: switched from textContent tween (unreliable, left values at 0) to proxy object pattern (val -> display) - Added IO + rAF fallback if GSAP is unavailable - Added section ids: #features, #stats, #reviews (matches nav anchors) - Reduced blank scroll region (300vh -> 140vh on story section) - Removed duplicate inline footer (LandingPageWrapper <Footer /> now sole source) LandingPageWrapper.tsx - Anchor comments noting ids added in HeroSection Docs + scripts: - docs/ADMIN_FUNCTIONALITY_CHECKLIST.md (admin app coverage notes) - docs/pricing-assessment.md (pricing model notes) - scripts/apply-admin-create-stop.js, check-stop-fns*.js, verify-stop-fns.js (admin stop creation tooling)
This commit is contained in:
Executable
+160
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* apply-admin-create-stop.js
|
||||
*
|
||||
* Applies the admin_create_stop RPC using ONLY the keys in .env.local.
|
||||
* You do NOT need Supabase dashboard / SQL editor access.
|
||||
*
|
||||
* This is the fix for the "could not find the function public.admin_create_stop(...)"
|
||||
* error when you click "Add New Stop".
|
||||
*
|
||||
* Usage (run from the repo root on a machine that can reach the DB):
|
||||
* node scripts/apply-admin-create-stop.js
|
||||
*
|
||||
* The script connects as the real postgres user (using your SUPABASE_SERVICE_ROLE_KEY
|
||||
* as the password) and runs the CREATE OR REPLACE + GRANT + NOTIFY.
|
||||
* This is the only reliable way to create the SECURITY DEFINER function because
|
||||
* direct REST inserts are blocked by the `block_stops_mutations` policy.
|
||||
*
|
||||
* After it prints success:
|
||||
* - Restart your `npm run dev`
|
||||
* - "Add New Stop" (AddStopModal / NewStopForm) will now call the RPC successfully.
|
||||
*/
|
||||
|
||||
const { Client } = require("pg");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Load .env.local (same as the rest of the project)
|
||||
try {
|
||||
require("dotenv").config({ path: path.resolve(__dirname, "../.env.local") });
|
||||
} catch (e) {
|
||||
// dotenv might not be hoisted in some setups; the keys may already be in process.env
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (!supabaseUrl || !serviceKey) {
|
||||
console.error("❌ Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env.local");
|
||||
console.error(" Make sure you are running from the repo root and .env.local exists with the keys.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Derive the direct Postgres URL (same logic as supabase/push-migrations.js)
|
||||
const projectRef = supabaseUrl.replace("https://", "").split(".")[0];
|
||||
const pgHost = `db.${projectRef}.supabase.co`;
|
||||
const dbUrl = `postgresql://postgres:${encodeURIComponent(serviceKey)}@${pgHost}:5432/postgres`;
|
||||
|
||||
console.log("🔧 apply-admin-create-stop RPC installer");
|
||||
console.log(" Project:", projectRef);
|
||||
console.log(" Using service role key from .env.local (full Postgres access)");
|
||||
console.log("");
|
||||
|
||||
async function main() {
|
||||
const migrationPath = path.resolve(__dirname, "../supabase/migrations/202_fix_admin_create_stop.sql");
|
||||
|
||||
if (!fs.existsSync(migrationPath)) {
|
||||
console.error("❌ Cannot find migration file:", migrationPath);
|
||||
console.error(" The file should have been created by the previous fix.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sql = fs.readFileSync(migrationPath, "utf8");
|
||||
|
||||
const client = new Client({
|
||||
connectionString: dbUrl,
|
||||
ssl: { rejectUnauthorized: false },
|
||||
// Some environments need a longer timeout
|
||||
connectionTimeoutMillis: 15000,
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("⏳ Connecting to Postgres (this uses port 5432 outbound)...");
|
||||
await client.connect();
|
||||
console.log("✅ Connected.");
|
||||
|
||||
console.log("⏳ Executing 202_fix_admin_create_stop.sql (CREATE OR REPLACE + GRANT + NOTIFY)...");
|
||||
const result = await client.query(sql);
|
||||
|
||||
console.log("✅ Function installed / updated successfully.");
|
||||
console.log(" Rows affected in last statement:", result.rowCount ?? "(see output above if any)");
|
||||
console.log("");
|
||||
console.log("🎉 admin_create_stop is now available via the anon key (SECURITY DEFINER).");
|
||||
|
||||
// Quick verification using the anon key over HTTPS (this is what the app uses)
|
||||
try {
|
||||
const anon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const probeBody = {
|
||||
p_active: false,
|
||||
p_address: null,
|
||||
p_brand_id: "00000000-0000-0000-0000-000000000000", // will FK fail, but proves the function exists
|
||||
p_city: "verify",
|
||||
p_cutoff_time: null,
|
||||
p_date: "2099-12-31",
|
||||
p_location: "verify",
|
||||
p_state: "TS",
|
||||
p_time: "10:00",
|
||||
p_zip: null,
|
||||
};
|
||||
const probe = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/rpc/admin_create_stop`, {
|
||||
method: "POST",
|
||||
headers: { apikey: anon, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(probeBody),
|
||||
});
|
||||
const txt = await probe.text();
|
||||
if (probe.status === 404) {
|
||||
console.log("⚠️ Post-apply probe still got 404 — you may need to wait a few seconds or the NOTIFY didn't take.");
|
||||
} else if (/foreign key|violates|brand_id/i.test(txt)) {
|
||||
console.log("✅ Post-apply probe: function exists (got expected FK error on the zero-UUID brand — this is success).");
|
||||
} else {
|
||||
console.log("✅ Post-apply probe returned:", txt.slice(0, 200));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(" (verification fetch skipped:", e.message, ")");
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Next:");
|
||||
console.log(" • Restart your dev server (`npm run dev`) if it was already running.");
|
||||
console.log(" • Go to the stops admin page and try \"Add New Stop\".");
|
||||
console.log(" • (Optional but recommended) node scripts/verify-stop-fns.js");
|
||||
} catch (err) {
|
||||
const msg = (err && err.message) ? err.message : String(err);
|
||||
console.error("❌ Failed to apply the function.");
|
||||
console.error(" Error:", msg.slice(0, 500));
|
||||
|
||||
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo|network is unreachable|ECONNREFUSED|timeout/i.test(msg)) {
|
||||
console.log("");
|
||||
console.log("💡 Your current environment cannot reach the database on port 5432.");
|
||||
console.log(" This is common inside some containers/CI sandboxes.");
|
||||
console.log("");
|
||||
console.log(" Copy and run ONE of the following on a *normal* laptop/terminal that has");
|
||||
console.log(" outbound access to Supabase Postgres (most developer machines can reach it):");
|
||||
console.log("");
|
||||
console.log(" Option A (easiest, uses exactly the same code as this script):");
|
||||
console.log(" npm run migrate:one 202");
|
||||
console.log("");
|
||||
console.log(" Option B (psql):");
|
||||
console.log(" # Build the URL using the key from your .env.local (same as this script does):");
|
||||
console.log(" # psql \"postgresql://postgres:PUT_YOUR_SERVICE_ROLE_KEY_HERE@db." + projectRef + ".supabase.co:5432/postgres\" \\");
|
||||
console.log(" # -f supabase/migrations/202_fix_admin_create_stop.sql");
|
||||
console.log("");
|
||||
console.log(" Option C (Supabase CLI):");
|
||||
console.log(" supabase db push --db-url \"postgresql://postgres:YOUR_SERVICE_ROLE_KEY@db." + projectRef + ".supabase.co:5432/postgres\" \\");
|
||||
console.log(" supabase/migrations/202_fix_admin_create_stop.sql");
|
||||
console.log("");
|
||||
console.log(" After the command succeeds you will see the NOTIFY and the function will exist.");
|
||||
console.log(" Then restart `npm run dev` and \"Add New Stop\" will stop showing the function-not-found error.");
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} finally {
|
||||
try { await client.end(); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Unexpected crash:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
require("dotenv").config({ path: require("path").resolve(__dirname, "../.env.local") });
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
const headers = {
|
||||
apikey: serviceKey,
|
||||
Authorization: `Bearer ${serviceKey}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
async function rpc(name, body = {}) {
|
||||
const r = await fetch(`${url}/rest/v1/rpc/${name}`, {
|
||||
method: "POST", headers, body: JSON.stringify(body),
|
||||
});
|
||||
return { status: r.status, text: (await r.text()).slice(0, 500) };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// 1. Try calling admin_create_stop with service role
|
||||
console.log("=== 1. Call admin_create_stop (service role) ===");
|
||||
console.log(await rpc("admin_create_stop", { p_brand_id: null }));
|
||||
|
||||
// 2. Try with body
|
||||
console.log("\n=== 2. Call admin_create_stop with stub body ===");
|
||||
console.log(await rpc("admin_create_stop", {
|
||||
p_brand_id: "00000000-0000-0000-0000-000000000000",
|
||||
p_city: "test", p_state: "CO", p_location: "x", p_date: "2025-01-01", p_time: "08:00"
|
||||
}));
|
||||
|
||||
// 3. Try with a name that obviously doesn't exist to see the error format
|
||||
console.log("\n=== 3. Call a non-existent function (control) ===");
|
||||
console.log(await rpc("definitely_does_not_exist_xyz", {}));
|
||||
|
||||
// 4. Use the PostgREST introspection — query information_schema via rpc
|
||||
// PostgREST exposes pg_proc through /rest/v1/ for system tables is not allowed
|
||||
// But we can use the OpenAPI spec endpoint
|
||||
console.log("\n=== 4. OpenAPI spec — looking for admin_create_stop ===");
|
||||
const openApi = await fetch(`${url}/rest/v1/`, { headers: { apikey: anonKey, Authorization: `Bearer ${anonKey}` } });
|
||||
const text = await openApi.text();
|
||||
const matches = text.match(/admin_create_stop[a-z_]*/g) || [];
|
||||
console.log("Matches in OpenAPI:", matches);
|
||||
|
||||
// 5. Get all function names from OpenAPI
|
||||
const funcMatches = [...text.matchAll(/"([a-z][a-z0-9_]*)\s*\{/g)].map(m => m[1]);
|
||||
const stopFns = funcMatches.filter(n => n.includes("stop"));
|
||||
console.log("\nStop-related functions in OpenAPI:", stopFns);
|
||||
})();
|
||||
@@ -0,0 +1,52 @@
|
||||
require("dotenv").config({ path: require("path").resolve(__dirname, "../.env.local") });
|
||||
const { Client } = require("pg");
|
||||
const dns = require("dns");
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const projectRef = url.replace("https://", "").split(".")[0];
|
||||
const candidates = [
|
||||
`db.${projectRef}.supabase.co`,
|
||||
`aws-0-us-east-1.pooler.supabase.com`,
|
||||
`aws-0-us-west-1.pooler.supabase.com`,
|
||||
];
|
||||
|
||||
(async () => {
|
||||
for (const host of candidates) {
|
||||
try {
|
||||
const ip = await new Promise((resolve, reject) => {
|
||||
dns.resolve4(host, (err, addrs) => err ? reject(err) : resolve(addrs));
|
||||
});
|
||||
console.log(`${host} -> ${ip.join(", ")}`);
|
||||
} catch (e) {
|
||||
console.log(`${host} -> DNS FAIL: ${e.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Try direct DB with port 6543 (pooler) using supabase format
|
||||
// Username pattern: postgres.<project-ref>
|
||||
const client = new Client({
|
||||
host: "aws-0-us-east-1.pooler.supabase.com",
|
||||
port: 6543, database: "postgres",
|
||||
user: `postgres.${projectRef}`,
|
||||
password: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
ssl: { rejectUnauthorized: false },
|
||||
});
|
||||
try {
|
||||
await client.connect();
|
||||
const r = await client.query(`
|
||||
SELECT p.proname,
|
||||
pg_get_function_identity_arguments(p.oid) AS id_args,
|
||||
p.prosecdef AS secdef
|
||||
FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid
|
||||
WHERE n.nspname='public' AND p.proname IN
|
||||
('admin_create_stop','admin_create_stops_batch','delete_stop')
|
||||
ORDER BY p.proname;
|
||||
`);
|
||||
console.log("\nFUNCTIONS IN DB:");
|
||||
console.log(JSON.stringify(r.rows, null, 2));
|
||||
} catch (e) {
|
||||
console.error("POOLER ERR:", e.message);
|
||||
} finally {
|
||||
try { await client.end(); } catch {}
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
require("dotenv").config({ path: require("path").resolve(__dirname, "../.env.local") });
|
||||
const { Client } = require("pg");
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const projectRef = url.replace("https://", "").split(".")[0];
|
||||
const pw = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
const tries = [
|
||||
{ host: "aws-0-us-east-1.pooler.supabase.com", port: 6543, user: `postgres.${projectRef}`, database: "postgres" },
|
||||
{ host: "aws-0-us-east-1.pooler.supabase.com", port: 5432, user: `postgres.${projectRef}`, database: "postgres" },
|
||||
{ host: "aws-0-us-east-1.pooler.supabase.com", port: 6543, user: "postgres", database: "postgres" },
|
||||
];
|
||||
|
||||
(async () => {
|
||||
for (const cfg of tries) {
|
||||
const client = new Client({ ...cfg, password: pw, ssl: { rejectUnauthorized: false } });
|
||||
try {
|
||||
await client.connect();
|
||||
const r = await client.query(`
|
||||
SELECT p.proname,
|
||||
pg_get_function_identity_arguments(p.oid) AS id_args,
|
||||
p.prosecdef AS secdef
|
||||
FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid
|
||||
WHERE n.nspname='public' AND p.proname IN
|
||||
('admin_create_stop','admin_create_stops_batch','delete_stop')
|
||||
ORDER BY p.proname;
|
||||
`);
|
||||
console.log(`\n✓ ${cfg.host}:${cfg.port} user=${cfg.user}`);
|
||||
console.log(JSON.stringify(r.rows, null, 2));
|
||||
|
||||
// Also check if migration tracking table exists
|
||||
const trk = await client.query(`
|
||||
SELECT version, name FROM supabase_migrations.schema_migrations
|
||||
WHERE version >= 145 ORDER BY version;
|
||||
`).catch(() => ({ rows: [] }));
|
||||
console.log("Recent migrations applied:", trk.rows);
|
||||
await client.end();
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log(`✗ ${cfg.host}:${cfg.port} user=${cfg.user} → ${e.message}`);
|
||||
try { await client.end(); } catch {}
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* verify-stop-fns.js
|
||||
*
|
||||
* Confirms `admin_create_stop` and `admin_create_stops_batch` exist in the
|
||||
* live database with the exact signatures the client uses, and that the
|
||||
* PostgREST schema cache has them registered.
|
||||
*
|
||||
* Run: node scripts/verify-stop-fns.js
|
||||
*
|
||||
* Replaces the older `check-stop-fns*.js` scripts (left in the repo for
|
||||
* history; this one is the canonical post-fix verification).
|
||||
*
|
||||
* Exits 0 on success, 1 if any check fails.
|
||||
*/
|
||||
|
||||
require("dotenv").config({ path: require("path").resolve(__dirname, "../.env.local") });
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!url || !serviceKey || !anonKey) {
|
||||
console.error("Missing NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / NEXT_PUBLIC_SUPABASE_ANON_KEY in .env.local");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const SERVICE_HEADERS = {
|
||||
apikey: serviceKey,
|
||||
Authorization: `Bearer ${serviceKey}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const ANON_HEADERS = {
|
||||
apikey: anonKey,
|
||||
Authorization: `Bearer ${anonKey}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
let failed = 0;
|
||||
const fail = (msg) => { console.error("✗ " + msg); failed += 1; };
|
||||
const pass = (msg) => console.log("✓ " + msg);
|
||||
|
||||
async function rpc(name, body, headers = SERVICE_HEADERS) {
|
||||
const r = await fetch(`${url}/rest/v1/rpc/${name}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await r.text();
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); } catch { parsed = text.slice(0, 200); }
|
||||
return { status: r.status, body: parsed };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log(`\nVerifying stop RPCs against ${url}\n`);
|
||||
|
||||
// ── 1. admin_create_stop — empty body (expect 400/422, NOT PGRST202) ─────
|
||||
// PGRST202 = function not in schema cache. We want a 400-style validation
|
||||
// error here, which proves PostgREST knows the function exists.
|
||||
console.log("1. admin_create_stop signature check (PostgREST cache)");
|
||||
const probe1 = await rpc("admin_create_stop", {});
|
||||
if (probe1.status === 404) {
|
||||
fail("admin_create_stop returned 404 — function not found (PGRST202 cache miss). " +
|
||||
"Did you apply migration 147 and run `NOTIFY pgrst, 'reload schema'`?");
|
||||
} else if (probe1.status >= 500) {
|
||||
fail(`admin_create_stop returned ${probe1.status} — server error. Body: ${JSON.stringify(probe1.body)}`);
|
||||
} else {
|
||||
pass(`admin_create_stop reachable in PostgREST (status ${probe1.status})`);
|
||||
}
|
||||
|
||||
// ── 2. admin_create_stop — happy path with a real (but harmless) brand ──
|
||||
console.log("\n2. admin_create_stop happy-path call (anon key)");
|
||||
const probe2 = await rpc(
|
||||
"admin_create_stop",
|
||||
{
|
||||
p_brand_id: "00000000-0000-0000-0000-000000000000",
|
||||
p_city: "verify-script-city",
|
||||
p_state: "CO",
|
||||
p_location: "verification stop",
|
||||
p_date: "2099-12-31",
|
||||
p_time: "08:00 AM – 02:00 PM",
|
||||
p_address: "123 Verify St",
|
||||
p_zip: "80000",
|
||||
p_cutoff_time: "08:00", // time-only — should be combined with date
|
||||
p_active: false,
|
||||
},
|
||||
ANON_HEADERS
|
||||
);
|
||||
if (probe2.status === 404) {
|
||||
fail("admin_create_stop not callable via anon key. " +
|
||||
"GRANT EXECUTE missing? Body: " + JSON.stringify(probe2.body));
|
||||
} else if (probe2.status >= 400) {
|
||||
// Expected: brand_id is the zero UUID (foreign key violation). That's fine —
|
||||
// it proves the function executed past type validation.
|
||||
const msg = JSON.stringify(probe2.body);
|
||||
if (/foreign key|violates/i.test(msg)) {
|
||||
pass(`admin_create_stop ran end-to-end (got expected FK violation: ${msg.slice(0, 120)}...)`);
|
||||
} else {
|
||||
fail(`admin_create_stop failed unexpectedly (${probe2.status}): ${msg}`);
|
||||
}
|
||||
} else {
|
||||
pass(`admin_create_stop returned success: ${JSON.stringify(probe2.body)}`);
|
||||
}
|
||||
|
||||
// ── 3. admin_create_stops_batch — same checks ───────────────────────────
|
||||
console.log("\n3. admin_create_stops_batch signature check");
|
||||
const probe3 = await rpc("admin_create_stops_batch", { p_brand_id: null, p_stops: [] });
|
||||
if (probe3.status === 404) {
|
||||
fail("admin_create_stops_batch returned 404 — function not in schema cache.");
|
||||
} else {
|
||||
pass(`admin_create_stops_batch reachable (status ${probe3.status})`);
|
||||
}
|
||||
|
||||
// ── 4. OpenAPI introspection — confirm functions are exposed ────────────
|
||||
console.log("\n4. OpenAPI introspection");
|
||||
const openApi = await fetch(`${url}/rest/v1/`, { headers: ANON_HEADERS });
|
||||
const text = await openApi.text();
|
||||
const hasSingle = /admin_create_stop\b/.test(text);
|
||||
const hasBatch = /admin_create_stops_batch\b/.test(text);
|
||||
if (hasSingle) pass("admin_create_stop appears in OpenAPI spec");
|
||||
else fail("admin_create_stop NOT in OpenAPI spec — PostgREST hasn't seen it yet");
|
||||
if (hasBatch) pass("admin_create_stops_batch appears in OpenAPI spec");
|
||||
else fail("admin_create_stops_batch NOT in OpenAPI spec");
|
||||
|
||||
console.log(`\n${failed === 0 ? "✓ All checks passed" : `✗ ${failed} check(s) failed`}\n`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
})().catch((e) => {
|
||||
console.error("verify-stop-fns.js crashed:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user