feat(settings): sidebar layout with profile, security, AI, notifications, nutrition

Sticky sidebar nav. Sections: Profile (name/language), Security (email/password change),
AI & Models (BYOK keys + per-use-case model prefs), Notifications (push subscribe),
Nutrition goals. Sub-pages: API keys, Webhooks.
This commit is contained in:
Arnaud
2026-07-01 08:10:51 +02:00
parent e179692adf
commit b2d592afe8
17 changed files with 1491 additions and 0 deletions
@@ -0,0 +1,142 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Webhook Docs — Epicure",
};
export default function WebhookDocsPage() {
return (
<div className="max-w-3xl mx-auto space-y-10 py-8">
<div>
<h1 className="text-2xl font-bold mb-2">Webhook Documentation</h1>
<p className="text-muted-foreground">
Learn how to integrate Epicure webhooks with your own tools, Zapier, and Make.
</p>
</div>
{/* Section 1: Webhook Events */}
<section>
<h2 className="text-xl font-semibold mb-4">Webhook Events</h2>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-muted">
<th className="text-left p-3 border border-border font-medium">Event</th>
<th className="text-left p-3 border border-border font-medium">Description</th>
<th className="text-left p-3 border border-border font-medium">Payload Fields</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 border border-border font-mono text-sm">recipe.created</td>
<td className="p-3 border border-border text-sm">Fired when a recipe is created</td>
<td className="p-3 border border-border font-mono text-sm text-muted-foreground">id, title, authorId</td>
</tr>
<tr>
<td className="p-3 border border-border font-mono text-sm">recipe.updated</td>
<td className="p-3 border border-border text-sm">Fired when a recipe is updated</td>
<td className="p-3 border border-border font-mono text-sm text-muted-foreground">id, title, authorId</td>
</tr>
<tr>
<td className="p-3 border border-border font-mono text-sm">recipe.published</td>
<td className="p-3 border border-border text-sm">Fired when recipe visibility becomes public</td>
<td className="p-3 border border-border font-mono text-sm text-muted-foreground">id, title, authorId</td>
</tr>
<tr>
<td className="p-3 border border-border font-mono text-sm">recipe.deleted</td>
<td className="p-3 border border-border text-sm">Fired when a recipe is deleted</td>
<td className="p-3 border border-border font-mono text-sm text-muted-foreground">id, title</td>
</tr>
<tr>
<td className="p-3 border border-border font-mono text-sm">meal_plan.updated</td>
<td className="p-3 border border-border text-sm">Fired when a meal plan entry changes</td>
<td className="p-3 border border-border font-mono text-sm text-muted-foreground">weekStart, day, mealType</td>
</tr>
<tr>
<td className="p-3 border border-border font-mono text-sm">shopping_list.completed</td>
<td className="p-3 border border-border text-sm">Fired when shopping list marked complete</td>
<td className="p-3 border border-border font-mono text-sm text-muted-foreground">id, name</td>
</tr>
<tr>
<td className="p-3 border border-border font-mono text-sm">comment.added</td>
<td className="p-3 border border-border text-sm">Fired when a comment is added to your recipe</td>
<td className="p-3 border border-border font-mono text-sm text-muted-foreground">commentId, recipeId, recipeTitle</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Section 2: Payload Format */}
<section>
<h2 className="text-xl font-semibold mb-4">Payload Format</h2>
<p className="text-sm text-muted-foreground mb-3">
Every webhook request is a POST with a JSON body in the following shape:
</p>
<pre className="bg-muted rounded-lg p-4 overflow-x-auto font-mono text-sm whitespace-pre">{`{
"event": "recipe.created",
"payload": { "id": "...", "title": "Pasta Carbonara", "authorId": "..." },
"timestamp": "2024-01-15T10:30:00.000Z"
}`}</pre>
</section>
{/* Section 3: Signature Verification */}
<section>
<h2 className="text-xl font-semibold mb-4">Signature Verification</h2>
<p className="text-sm text-muted-foreground mb-4">
Every request includes an{" "}
<code className="font-mono bg-muted px-1.5 py-0.5 rounded text-xs">X-Epicure-Signature</code>{" "}
header containing an HMAC-SHA256 digest of the raw request body, signed with your webhook
secret. Always verify this signature before processing the payload.
</p>
<p className="text-sm font-medium mb-2">TypeScript / Node.js</p>
<pre className="bg-muted rounded-lg p-4 overflow-x-auto font-mono text-sm whitespace-pre mb-4">{`import crypto from "crypto";
function verify(body: string, secret: string, signature: string): boolean {
const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}`}</pre>
<p className="text-sm font-medium mb-2">Python</p>
<pre className="bg-muted rounded-lg p-4 overflow-x-auto font-mono text-sm whitespace-pre">{`import hmac, hashlib
def verify(body: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)`}</pre>
</section>
{/* Section 4: Zapier Integration */}
<section>
<h2 className="text-xl font-semibold mb-4">Zapier Integration</h2>
<ol className="list-decimal pl-6 space-y-2 text-sm mb-6">
<li>Go to <span className="font-medium">zapier.com</span> and create a new Zap.</li>
<li>Choose <span className="font-medium">Webhooks by Zapier</span> as the trigger and select <span className="font-medium">Catch Hook</span>.</li>
<li>Copy the webhook URL provided by Zapier.</li>
<li>Go to <span className="font-medium">Epicure Settings Webhooks</span> and add that URL.</li>
<li>Test the trigger in Zapier to confirm it receives the payload.</li>
</ol>
<h3 className="text-base font-semibold mb-3">Example workflows</h3>
<ul className="list-disc pl-6 space-y-2 text-sm text-muted-foreground">
<li><span className="font-medium text-foreground">New Recipe Published</span> Add row to Notion database</li>
<li><span className="font-medium text-foreground">Shopping List Completed</span> Send Slack message to #groceries</li>
<li><span className="font-medium text-foreground">Comment Added</span> Send email notification via Gmail</li>
</ul>
</section>
{/* Section 5: Make (Integromat) */}
<section>
<h2 className="text-xl font-semibold mb-4">Make (Integromat)</h2>
<ol className="list-decimal pl-6 space-y-2 text-sm">
<li>Go to <span className="font-medium">make.com</span> and create a new scenario.</li>
<li>Add an <span className="font-medium">HTTP &gt; Watch for Incoming Webhooks</span> module as the trigger.</li>
<li>Copy the generated webhook URL from the module settings.</li>
<li>Go to <span className="font-medium">Epicure Settings Webhooks</span> and paste that URL.</li>
<li>Run the scenario and trigger a test event in Epicure to verify the connection.</li>
<li>Add downstream modules (Notion, Slack, Gmail, etc.) to act on the incoming data.</li>
</ol>
</section>
</div>
);
}
@@ -0,0 +1,45 @@
import type { Metadata } from "next";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
import { db, webhooks, eq } from "@epicure/db";
import { WebhooksManager } from "@/components/settings/webhooks-manager";
export const metadata: Metadata = { title: "Webhooks" };
export default async function WebhooksPage() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return null;
const rows = await db
.select({
id: webhooks.id,
url: webhooks.url,
events: webhooks.events,
active: webhooks.active,
createdAt: webhooks.createdAt,
})
.from(webhooks)
.where(eq(webhooks.userId, session.user.id));
return (
<div className="space-y-8">
<section className="rounded-xl border p-6 space-y-4">
<div>
<h2 className="font-semibold text-lg">Webhooks</h2>
<p className="text-sm text-muted-foreground mt-1">
Receive HTTP callbacks when events happen in your Epicure account.
</p>
</div>
<WebhooksManager
initialWebhooks={rows.map((w) => ({
id: w.id,
url: w.url,
events: w.events,
active: w.active,
createdAt: w.createdAt.toISOString(),
}))}
/>
</section>
</div>
);
}