From cf6c4005dd4bcc73267a43905a662b1552eb64b6 Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Sat, 14 Mar 2026 08:51:48 -0400 Subject: [PATCH] implemented the udpated changes --- .dockerignore | 6 + Dockerfile | 23 + app/about/page.tsx | 406 ++++----- app/api/2fa/disable/route.ts | 6 + app/api/2fa/enable/route.ts | 6 + app/api/2fa/generate/route.ts | 6 + app/api/accounts/link/route.ts | 18 +- app/api/accounts/manual/route.ts | 20 +- app/api/accounts/route.ts | 17 +- app/api/auth/forgot-password/route.ts | 6 + app/api/auth/login/route.ts | 22 +- app/api/auth/logout/route.ts | 6 + app/api/auth/me/route.ts | 6 + app/api/auth/profile/route.ts | 27 +- app/api/auth/refresh/route.ts | 6 + app/api/auth/register/route.ts | 32 +- app/api/auth/reset-password/route.ts | 6 + app/api/auth/verify-email/route.ts | 6 + app/api/exports/csv/route.ts | 17 +- app/api/exports/sheets/route.ts | 6 + app/api/google/connect/route.ts | 6 + app/api/google/disconnect/route.ts | 6 + app/api/google/exchange/route.ts | 6 + app/api/google/status/route.ts | 6 + app/api/plaid/exchange/route.ts | 20 +- app/api/plaid/link-token/route.ts | 18 +- app/api/rules/route.ts | 37 +- app/api/rules/suggestions/route.ts | 17 +- app/api/stripe/checkout/route.ts | 6 + app/api/stripe/portal/route.ts | 6 + app/api/stripe/subscription/route.ts | 6 + app/api/tax/returns/[id]/export/route.ts | 24 +- app/api/tax/returns/route.ts | 37 +- app/api/transactions/[id]/derived/route.ts | 34 +- app/api/transactions/cashflow/route.ts | 27 +- app/api/transactions/import/route.ts | 6 + app/api/transactions/manual/route.ts | 31 +- app/api/transactions/merchants/route.ts | 27 +- app/api/transactions/route.ts | 33 +- app/api/transactions/summary/route.ts | 17 +- app/api/transactions/sync/route.ts | 20 +- app/app/page.tsx | 115 ++- app/auth/google/callback/page.tsx | 85 ++ app/blog/[slug]/page.tsx | 194 ++--- app/blog/page.tsx | 218 ++--- app/book-demo/page.tsx | 236 +++--- app/compare/vs-copilot/page.tsx | 172 ++-- app/compare/vs-quicken/page.tsx | 172 ++-- app/compare/vs-spreadsheets/page.tsx | 174 ++-- app/compare/vs-ynab/page.tsx | 172 ++-- app/exports/page.tsx | 379 +++++---- app/features/cash-flow/page.tsx | 168 ++-- app/features/reports/page.tsx | 168 ++-- app/forgot-password/page.tsx | 115 +++ app/login/page.tsx | 355 ++++---- app/page.tsx | 474 +++++------ app/pricing/page.tsx | 356 ++++---- app/profile/page.tsx | 356 ++++---- app/register/page.tsx | 347 ++++---- app/reset-password/page.tsx | 147 ++++ app/settings/2fa/page.tsx | 220 +++++ app/settings/page.tsx | 13 +- app/settings/profile/page.tsx | 150 +++- app/settings/subscription/page.tsx | 233 ++++-- app/transactions/page.tsx | 910 ++++++++------------- app/verify-email/page.tsx | 118 +++ components/app-shell.tsx | 198 ++++- components/contact-section.tsx | 150 ++-- components/currency-toggle.tsx | 116 +-- components/export-download-button.tsx | 112 +-- components/faq-section.tsx | 32 +- components/growth-simulator.tsx | 222 ++--- components/hero-actions.tsx | 182 ++--- components/page-schema.tsx | 22 +- lib/api.ts | 115 +++ lib/backend.ts | 73 ++ middleware.ts | 38 + package-lock.json | 1 + tsconfig.json | 7 +- write-frontend-1-lib.mjs | 240 ++++++ write-frontend-2-auth.mjs | 764 +++++++++++++++++ write-frontend-3-proxies.mjs | 370 +++++++++ write-frontend-4-appshell.mjs | 249 ++++++ write-frontend-6-settings.mjs | 594 ++++++++++++++ write-frontend-7-transactions.mjs | 517 ++++++++++++ write-frontend-8-exports.mjs | 221 +++++ 86 files changed, 7616 insertions(+), 3692 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 app/api/2fa/disable/route.ts create mode 100644 app/api/2fa/enable/route.ts create mode 100644 app/api/2fa/generate/route.ts create mode 100644 app/api/auth/forgot-password/route.ts create mode 100644 app/api/auth/logout/route.ts create mode 100644 app/api/auth/me/route.ts create mode 100644 app/api/auth/refresh/route.ts create mode 100644 app/api/auth/reset-password/route.ts create mode 100644 app/api/auth/verify-email/route.ts create mode 100644 app/api/exports/sheets/route.ts create mode 100644 app/api/google/connect/route.ts create mode 100644 app/api/google/disconnect/route.ts create mode 100644 app/api/google/exchange/route.ts create mode 100644 app/api/google/status/route.ts create mode 100644 app/api/stripe/checkout/route.ts create mode 100644 app/api/stripe/portal/route.ts create mode 100644 app/api/stripe/subscription/route.ts create mode 100644 app/api/transactions/import/route.ts create mode 100644 app/auth/google/callback/page.tsx create mode 100644 app/forgot-password/page.tsx create mode 100644 app/reset-password/page.tsx create mode 100644 app/settings/2fa/page.tsx create mode 100644 app/verify-email/page.tsx create mode 100644 lib/api.ts create mode 100644 lib/backend.ts create mode 100644 middleware.ts create mode 100644 write-frontend-1-lib.mjs create mode 100644 write-frontend-2-auth.mjs create mode 100644 write-frontend-3-proxies.mjs create mode 100644 write-frontend-4-appshell.mjs create mode 100644 write-frontend-6-settings.mjs create mode 100644 write-frontend-7-transactions.mjs create mode 100644 write-frontend-8-exports.mjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..df8552c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.next +.env +.env.local +*.log +.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..06c7f81 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# ─── Stage 1: Build ────────────────────────────────────────────────────────── +FROM node:20-alpine AS builder +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --legacy-peer-deps + +COPY . . +RUN npm run build + +# ─── Stage 2: Production ───────────────────────────────────────────────────── +FROM node:20-alpine AS production +WORKDIR /app + +ENV NODE_ENV=production + +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +EXPOSE 3052 + +CMD ["node", "server.js"] diff --git a/app/about/page.tsx b/app/about/page.tsx index 8c2b6ac..9a14e1e 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,203 +1,203 @@ -import Image from "next/image"; -import Link from "next/link"; -import { ContactSection } from "../../components/contact-section"; -import { DemoCta } from "../../components/demo-cta"; -import { FaqSection } from "../../components/faq-section"; -import { PageSchema } from "../../components/page-schema"; -import { SiteFooter } from "../../components/site-footer"; -import { SiteHeader } from "../../components/site-header"; -import { defaultFaqs } from "../../data/faq"; -import { siteInfo } from "../../data/site"; - -export const metadata = { - title: "About LedgerOne", - description: - "Learn how LedgerOne builds audit-ready ledgers for US finance, tax, and operations teams.", - keywords: siteInfo.keywords -}; - -const values = [ - { - title: "Data ownership", - detail: "Your data stays portable, exportable, and under your control." - }, - { - title: "Transparent automation", - detail: "Rules are visible, explainable, and ready for compliance review." - }, - { - title: "Operational clarity", - detail: "Keep finance, tax, and operations aligned with one ledger truth." - } -]; - -const milestones = [ - { - title: "Ledger-first architecture", - detail: "Every transaction starts immutable, then layers preserve every change." - }, - { - title: "Built for teams", - detail: "We design workflows for the handoffs between finance, tax, and ops." - }, - { - title: "US-ready exports", - detail: "Exports are formatted to support US tax and accounting workflows." - } -]; - -export default function AboutPage() { - const schema = [ - { - "@context": "https://schema.org", - "@type": "WebPage", - name: "About LedgerOne", - description: - "Learn how LedgerOne builds audit-ready ledgers for US finance, tax, and operations teams.", - url: `${siteInfo.url}/about` - }, - { - "@context": "https://schema.org", - "@type": "FAQPage", - mainEntity: defaultFaqs.map((item) => ({ - "@type": "Question", - name: item.question, - acceptedAnswer: { "@type": "Answer", text: item.answer } - })) - } - ]; - - return ( -
- - -
-
- -
-
-
-
- Our Story -
-

- LedgerOne keeps every transaction ready for audits, review, and action. -

-
-

- We built LedgerOne for teams that manage high volumes of transactions but - still need each decision documented. Our ledger-first workflow keeps the - raw truth intact while allowing intelligent categorization and rule-driven - automation. -

-

- The result is a single source of truth that helps US finance and tax teams - collaborate without losing evidence or context. -

-
-
- - View pricing - - - Explore FAQs - -
-
-
-
-
- Analyst reviewing charts on a laptop -
-

- Built for US operators -

-

- LedgerOne is built around US accounting workflows, audit readiness, and - tax reporting cycles. -

-
-
-
-
- -
- {values.map((value) => ( -
-
- -
-

{value.title}

-

{value.detail}

-
- ))} -
- -
-
- Leadership -
-

Built by finance experts.

-
-
- MM -
-

Manoj Mohan

-

Founder & CEO

-

- Leading the vision to bring audit-ready financial controls to modern businesses. -

-
-
- -
-
-
-
- What we built -
-

A ledger that holds the full story.

-

- Traditional tools collapse data into summaries. LedgerOne keeps each raw - entry intact and layers in decisions, reviews, and approvals. -

-
-
- {milestones.map((item, index) => ( -
-

- Focus {index + 1} -

-

{item.title}

-

{item.detail}

-
- ))} -
-
-
-
-
- - - -
- ); -} +import Image from "next/image"; +import Link from "next/link"; +import { ContactSection } from "../../components/contact-section"; +import { DemoCta } from "../../components/demo-cta"; +import { FaqSection } from "../../components/faq-section"; +import { PageSchema } from "../../components/page-schema"; +import { SiteFooter } from "../../components/site-footer"; +import { SiteHeader } from "../../components/site-header"; +import { defaultFaqs } from "../../data/faq"; +import { siteInfo } from "../../data/site"; + +export const metadata = { + title: "About LedgerOne", + description: + "Learn how LedgerOne builds audit-ready ledgers for US finance, tax, and operations teams.", + keywords: siteInfo.keywords +}; + +const values = [ + { + title: "Data ownership", + detail: "Your data stays portable, exportable, and under your control." + }, + { + title: "Transparent automation", + detail: "Rules are visible, explainable, and ready for compliance review." + }, + { + title: "Operational clarity", + detail: "Keep finance, tax, and operations aligned with one ledger truth." + } +]; + +const milestones = [ + { + title: "Ledger-first architecture", + detail: "Every transaction starts immutable, then layers preserve every change." + }, + { + title: "Built for teams", + detail: "We design workflows for the handoffs between finance, tax, and ops." + }, + { + title: "US-ready exports", + detail: "Exports are formatted to support US tax and accounting workflows." + } +]; + +export default function AboutPage() { + const schema = [ + { + "@context": "https://schema.org", + "@type": "WebPage", + name: "About LedgerOne", + description: + "Learn how LedgerOne builds audit-ready ledgers for US finance, tax, and operations teams.", + url: `${siteInfo.url}/about` + }, + { + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: defaultFaqs.map((item) => ({ + "@type": "Question", + name: item.question, + acceptedAnswer: { "@type": "Answer", text: item.answer } + })) + } + ]; + + return ( +
+ + +
+
+ +
+
+
+
+ Our Story +
+

+ LedgerOne keeps every transaction ready for audits, review, and action. +

+
+

+ We built LedgerOne for teams that manage high volumes of transactions but + still need each decision documented. Our ledger-first workflow keeps the + raw truth intact while allowing intelligent categorization and rule-driven + automation. +

+

+ The result is a single source of truth that helps US finance and tax teams + collaborate without losing evidence or context. +

+
+
+ + View pricing + + + Explore FAQs + +
+
+
+
+
+ Analyst reviewing charts on a laptop +
+

+ Built for US operators +

+

+ LedgerOne is built around US accounting workflows, audit readiness, and + tax reporting cycles. +

+
+
+
+
+ +
+ {values.map((value) => ( +
+
+ +
+

{value.title}

+

{value.detail}

+
+ ))} +
+ +
+
+ Leadership +
+

Built by finance experts.

+
+
+ MM +
+

Manoj Mohan

+

Founder & CEO

+

+ Leading the vision to bring audit-ready financial controls to modern businesses. +

+
+
+ +
+
+
+
+ What we built +
+

A ledger that holds the full story.

+

+ Traditional tools collapse data into summaries. LedgerOne keeps each raw + entry intact and layers in decisions, reviews, and approvals. +

+
+
+ {milestones.map((item, index) => ( +
+

+ Focus {index + 1} +

+

{item.title}

+

{item.detail}

+
+ ))} +
+
+
+
+
+ + + +
+ ); +} diff --git a/app/api/2fa/disable/route.ts b/app/api/2fa/disable/route.ts new file mode 100644 index 0000000..3d6ee9b --- /dev/null +++ b/app/api/2fa/disable/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function DELETE(req: NextRequest) { + return proxyRequest(req, "2fa/disable"); +} diff --git a/app/api/2fa/enable/route.ts b/app/api/2fa/enable/route.ts new file mode 100644 index 0000000..fa0bfc5 --- /dev/null +++ b/app/api/2fa/enable/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "2fa/enable"); +} diff --git a/app/api/2fa/generate/route.ts b/app/api/2fa/generate/route.ts new file mode 100644 index 0000000..5d009ec --- /dev/null +++ b/app/api/2fa/generate/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "2fa/generate"); +} diff --git a/app/api/accounts/link/route.ts b/app/api/accounts/link/route.ts index 8c68816..250c02f 100644 --- a/app/api/accounts/link/route.ts +++ b/app/api/accounts/link/route.ts @@ -1,12 +1,6 @@ -export async function POST() { - const res = await fetch("http://localhost:3051/api/accounts/link", { - method: "POST" - }); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "accounts/link-token"); +} diff --git a/app/api/accounts/manual/route.ts b/app/api/accounts/manual/route.ts index e66e2af..1f213ce 100644 --- a/app/api/accounts/manual/route.ts +++ b/app/api/accounts/manual/route.ts @@ -1,16 +1,6 @@ -export async function POST(req: Request) { - const body = await req.text(); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/accounts/manual`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "accounts/manual"); } diff --git a/app/api/accounts/route.ts b/app/api/accounts/route.ts index 74572ae..5b1b440 100644 --- a/app/api/accounts/route.ts +++ b/app/api/accounts/route.ts @@ -1,13 +1,6 @@ -export async function GET(req: Request) { - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051" - const url = new URL(req.url); - const query = url.searchParams.toString(); - const res = await fetch(`${baseUrl}/api/accounts${query ? `?${query}` : ""}`); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "accounts"); } diff --git a/app/api/auth/forgot-password/route.ts b/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..61dc455 --- /dev/null +++ b/app/api/auth/forgot-password/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "auth/forgot-password"); +} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 36b902e..5acfb79 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,16 +1,6 @@ -export async function POST(req: Request) { - const body = await req.text(); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "auth/login"); +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..c8009a2 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "auth/logout"); +} diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts new file mode 100644 index 0000000..f449c8b --- /dev/null +++ b/app/api/auth/me/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "auth/me"); +} diff --git a/app/api/auth/profile/route.ts b/app/api/auth/profile/route.ts index 4bca881..73d44d8 100644 --- a/app/api/auth/profile/route.ts +++ b/app/api/auth/profile/route.ts @@ -1,17 +1,10 @@ -export async function PATCH(req: Request) { - const body = await req.text(); - const auth = req.headers.get("authorization") ?? ""; - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/auth/profile`, { - method: "PATCH", - headers: { "Content-Type": "application/json", Authorization: auth }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "auth/me"); +} + +export async function PATCH(req: NextRequest) { + return proxyRequest(req, "auth/profile"); +} diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts new file mode 100644 index 0000000..864d056 --- /dev/null +++ b/app/api/auth/refresh/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "auth/refresh"); +} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 8f36336..c630edd 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,26 +1,6 @@ -export async function POST(req: Request) { - const body = await req.text(); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/auth/register`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} - -export async function GET() { - return new Response( - JSON.stringify({ - ok: true, - message: "POST JSON { email, password } to /api/auth/register." - }), - { status: 200, headers: { "Content-Type": "application/json" } } - ); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "auth/register"); +} diff --git a/app/api/auth/reset-password/route.ts b/app/api/auth/reset-password/route.ts new file mode 100644 index 0000000..bb9bf9c --- /dev/null +++ b/app/api/auth/reset-password/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "auth/reset-password"); +} diff --git a/app/api/auth/verify-email/route.ts b/app/api/auth/verify-email/route.ts new file mode 100644 index 0000000..4f4b5a3 --- /dev/null +++ b/app/api/auth/verify-email/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "auth/verify-email"); +} diff --git a/app/api/exports/csv/route.ts b/app/api/exports/csv/route.ts index ccfa7ef..6b19705 100644 --- a/app/api/exports/csv/route.ts +++ b/app/api/exports/csv/route.ts @@ -1,11 +1,6 @@ -export async function GET(req: Request) { - const url = new URL(req.url); - const res = await fetch(`http://localhost:3051/api/exports/csv${url.search}`); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "exports/csv"); +} diff --git a/app/api/exports/sheets/route.ts b/app/api/exports/sheets/route.ts new file mode 100644 index 0000000..2f3b85a --- /dev/null +++ b/app/api/exports/sheets/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "exports/sheets"); +} diff --git a/app/api/google/connect/route.ts b/app/api/google/connect/route.ts new file mode 100644 index 0000000..b4af629 --- /dev/null +++ b/app/api/google/connect/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "google/connect"); +} diff --git a/app/api/google/disconnect/route.ts b/app/api/google/disconnect/route.ts new file mode 100644 index 0000000..c83a9dd --- /dev/null +++ b/app/api/google/disconnect/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function DELETE(req: NextRequest) { + return proxyRequest(req, "google/disconnect"); +} diff --git a/app/api/google/exchange/route.ts b/app/api/google/exchange/route.ts new file mode 100644 index 0000000..136ce43 --- /dev/null +++ b/app/api/google/exchange/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "google/exchange"); +} diff --git a/app/api/google/status/route.ts b/app/api/google/status/route.ts new file mode 100644 index 0000000..21867b8 --- /dev/null +++ b/app/api/google/status/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "google/status"); +} diff --git a/app/api/plaid/exchange/route.ts b/app/api/plaid/exchange/route.ts index 2e60404..9578cc5 100644 --- a/app/api/plaid/exchange/route.ts +++ b/app/api/plaid/exchange/route.ts @@ -1,16 +1,6 @@ -export async function POST(req: Request) { - const body = await req.text(); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/plaid/exchange`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "plaid/exchange"); } diff --git a/app/api/plaid/link-token/route.ts b/app/api/plaid/link-token/route.ts index 3334357..109c043 100644 --- a/app/api/plaid/link-token/route.ts +++ b/app/api/plaid/link-token/route.ts @@ -1,14 +1,6 @@ -export async function POST() { - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/plaid/link-token`, { - method: "POST", - headers: { "Content-Type": "application/json" } - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "plaid/link-token"); } diff --git a/app/api/rules/route.ts b/app/api/rules/route.ts index fb31ecf..4edf09f 100644 --- a/app/api/rules/route.ts +++ b/app/api/rules/route.ts @@ -1,27 +1,10 @@ -export async function GET(req: Request) { - const url = new URL(req.url); - const res = await fetch(`http://localhost:3051/api/rules${url.search}`); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} - -export async function POST(req: Request) { - const body = await req.text(); - const res = await fetch("http://localhost:3051/api/rules", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "rules"); +} + +export async function POST(req: NextRequest) { + return proxyRequest(req, "rules"); +} diff --git a/app/api/rules/suggestions/route.ts b/app/api/rules/suggestions/route.ts index d25de53..5ddfeaa 100644 --- a/app/api/rules/suggestions/route.ts +++ b/app/api/rules/suggestions/route.ts @@ -1,11 +1,6 @@ -export async function GET(req: Request) { - const url = new URL(req.url); - const res = await fetch(`http://localhost:3051/api/rules/suggestions${url.search}`); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "rules/suggestions"); +} diff --git a/app/api/stripe/checkout/route.ts b/app/api/stripe/checkout/route.ts new file mode 100644 index 0000000..214296b --- /dev/null +++ b/app/api/stripe/checkout/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "stripe/checkout"); +} diff --git a/app/api/stripe/portal/route.ts b/app/api/stripe/portal/route.ts new file mode 100644 index 0000000..a45df15 --- /dev/null +++ b/app/api/stripe/portal/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "stripe/portal"); +} diff --git a/app/api/stripe/subscription/route.ts b/app/api/stripe/subscription/route.ts new file mode 100644 index 0000000..f561308 --- /dev/null +++ b/app/api/stripe/subscription/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "stripe/subscription"); +} diff --git a/app/api/tax/returns/[id]/export/route.ts b/app/api/tax/returns/[id]/export/route.ts index 2c351a0..290f99f 100644 --- a/app/api/tax/returns/[id]/export/route.ts +++ b/app/api/tax/returns/[id]/export/route.ts @@ -1,15 +1,9 @@ -export async function POST( - _req: Request, - context: { params: { id: string } } -) { - const res = await fetch(`http://localhost:3051/api/tax/returns/${context.params.id}/export`, { - method: "POST" - }); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET( + req: NextRequest, + { params }: { params: { id: string } } +) { + return proxyRequest(req, `tax/returns/${params.id}/export`); +} diff --git a/app/api/tax/returns/route.ts b/app/api/tax/returns/route.ts index 0a05cb8..078db69 100644 --- a/app/api/tax/returns/route.ts +++ b/app/api/tax/returns/route.ts @@ -1,27 +1,10 @@ -export async function GET(req: Request) { - const url = new URL(req.url); - const res = await fetch(`http://localhost:3051/api/tax/returns${url.search}`); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} - -export async function POST(req: Request) { - const body = await req.text(); - const res = await fetch("http://localhost:3051/api/tax/returns", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "tax/returns"); +} + +export async function POST(req: NextRequest) { + return proxyRequest(req, "tax/returns"); +} diff --git a/app/api/transactions/[id]/derived/route.ts b/app/api/transactions/[id]/derived/route.ts index c384cfc..c4447c6 100644 --- a/app/api/transactions/[id]/derived/route.ts +++ b/app/api/transactions/[id]/derived/route.ts @@ -1,27 +1,9 @@ -export async function PATCH(req: Request, context: { params: { id: string } }) { - try { - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const payload = await req.text(); - const res = await fetch(`${baseUrl}/api/transactions/${context.params.id}/derived`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: payload - }); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); - } catch { - return new Response( - JSON.stringify({ - data: null, - meta: { timestamp: new Date().toISOString(), version: "v1" }, - error: { message: "Backend unavailable." } - }), - { status: 503, headers: { "Content-Type": "application/json" } } - ); - } +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function PATCH( + req: NextRequest, + { params }: { params: { id: string } } +) { + return proxyRequest(req, `transactions/${params.id}/derived`); } diff --git a/app/api/transactions/cashflow/route.ts b/app/api/transactions/cashflow/route.ts index 835a247..f84bc4e 100644 --- a/app/api/transactions/cashflow/route.ts +++ b/app/api/transactions/cashflow/route.ts @@ -1,23 +1,6 @@ -export async function GET(req: Request) { - try { - const url = new URL(req.url); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/transactions/cashflow${url.search}`); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); - } catch { - return new Response( - JSON.stringify({ - data: [], - meta: { timestamp: new Date().toISOString(), version: "v1" }, - error: { message: "Backend unavailable." } - }), - { status: 503, headers: { "Content-Type": "application/json" } } - ); - } +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "transactions/cashflow"); } diff --git a/app/api/transactions/import/route.ts b/app/api/transactions/import/route.ts new file mode 100644 index 0000000..d9ab8de --- /dev/null +++ b/app/api/transactions/import/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "transactions/import"); +} diff --git a/app/api/transactions/manual/route.ts b/app/api/transactions/manual/route.ts index 7427563..1cee988 100644 --- a/app/api/transactions/manual/route.ts +++ b/app/api/transactions/manual/route.ts @@ -1,27 +1,6 @@ -export async function POST(req: Request) { - try { - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const payload = await req.text(); - const res = await fetch(`${baseUrl}/api/transactions/manual`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: payload - }); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); - } catch { - return new Response( - JSON.stringify({ - data: null, - meta: { timestamp: new Date().toISOString(), version: "v1" }, - error: { message: "Backend unavailable." } - }), - { status: 503, headers: { "Content-Type": "application/json" } } - ); - } +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "transactions/manual"); } diff --git a/app/api/transactions/merchants/route.ts b/app/api/transactions/merchants/route.ts index 994f2ce..00ca9c4 100644 --- a/app/api/transactions/merchants/route.ts +++ b/app/api/transactions/merchants/route.ts @@ -1,23 +1,6 @@ -export async function GET(req: Request) { - try { - const url = new URL(req.url); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/transactions/merchants${url.search}`); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); - } catch { - return new Response( - JSON.stringify({ - data: [], - meta: { timestamp: new Date().toISOString(), version: "v1" }, - error: { message: "Backend unavailable." } - }), - { status: 503, headers: { "Content-Type": "application/json" } } - ); - } +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "transactions/merchants"); } diff --git a/app/api/transactions/route.ts b/app/api/transactions/route.ts index cec99ba..79477f2 100644 --- a/app/api/transactions/route.ts +++ b/app/api/transactions/route.ts @@ -1,23 +1,10 @@ -export async function GET(req: Request) { - try { - const url = new URL(req.url); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/transactions${url.search}`); - const body = await res.text(); - return new Response(body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); - } catch { - return new Response( - JSON.stringify({ - data: [], - meta: { timestamp: new Date().toISOString(), version: "v1" }, - error: { message: "Backend unavailable." } - }), - { status: 503, headers: { "Content-Type": "application/json" } } - ); - } -} +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "transactions"); +} + +export async function POST(req: NextRequest) { + return proxyRequest(req, "transactions"); +} diff --git a/app/api/transactions/summary/route.ts b/app/api/transactions/summary/route.ts index 66a1c0a..51403de 100644 --- a/app/api/transactions/summary/route.ts +++ b/app/api/transactions/summary/route.ts @@ -1,13 +1,6 @@ -export async function GET(req: Request) { - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const url = new URL(req.url); - const query = url.searchParams.toString(); - const res = await fetch(`${baseUrl}/api/transactions/summary${query ? `?${query}` : ""}`); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "transactions/summary"); } diff --git a/app/api/transactions/sync/route.ts b/app/api/transactions/sync/route.ts index 7ecc2f6..7896454 100644 --- a/app/api/transactions/sync/route.ts +++ b/app/api/transactions/sync/route.ts @@ -1,16 +1,6 @@ -export async function POST(req: Request) { - const body = await req.text(); - const baseUrl = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; - const res = await fetch(`${baseUrl}/api/transactions/sync`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body - }); - const payload = await res.text(); - return new Response(payload, { - status: res.status, - headers: { - "Content-Type": res.headers.get("content-type") ?? "application/json" - } - }); +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function POST(req: NextRequest) { + return proxyRequest(req, "transactions/sync"); } diff --git a/app/app/page.tsx b/app/app/page.tsx index 7c7be1a..2701ba4 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { AppShell } from "../../components/app-shell"; +import { apiFetch } from "@/lib/api"; type ApiResponse = { data: T; @@ -31,36 +32,14 @@ type MerchantInsight = { count: number; }; -const recentTransactions = [ - { - date: "Oct 24, 2023", - description: "Whole Foods Market", - category: "Groceries", - account: "Chase Sapphire", - amount: "-$142.30" - }, - { - date: "Oct 23, 2023", - description: "Apple Subscription", - category: "Services", - account: "Apple Card", - amount: "-$14.99" - }, - { - date: "Oct 22, 2023", - description: "Stripe Payout", - category: "Income", - account: "Mercury Business", - amount: "+$4,200.00" - }, - { - date: "Oct 22, 2023", - description: "Shell Gasoline", - category: "Transport", - account: "Chase Sapphire", - amount: "-$52.12" - } -]; +type TxRow = { + id: string; + date: string; + description: string; + category?: string | null; + accountId?: string | null; + amount: string; +}; // ---------- helpers ---------- function formatMonthLabel(yyyyMm: string) { @@ -520,27 +499,23 @@ export default function AppHomePage() { const [cashflow, setCashflow] = useState([]); const [merchants, setMerchants] = useState([]); const [accountCount, setAccountCount] = useState(null); + const [recentTxs, setRecentTxs] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { - const userId = localStorage.getItem("ledgerone_user_id"); - if (!userId) { - setLoading(false); - return; - } - const query = `?user_id=${encodeURIComponent(userId)}`; - Promise.all([ - fetch(`/api/transactions/summary${query}`).then((res) => res.json() as Promise>), - fetch(`/api/transactions/cashflow${query}&months=6`).then((res) => res.json() as Promise>), - fetch(`/api/transactions/merchants${query}&limit=5`).then((res) => res.json() as Promise>), - fetch(`/api/accounts${query}`).then((res) => res.json() as Promise>) + apiFetch("/api/transactions/summary"), + apiFetch("/api/transactions/cashflow?months=6"), + apiFetch("/api/transactions/merchants?limit=5"), + apiFetch<{ accounts: { id: string }[]; total: number }>("/api/accounts"), + apiFetch<{ transactions: TxRow[]; total: number }>("/api/transactions?limit=5"), ]) - .then(([summaryRes, cashflowRes, merchantsRes, accountsRes]) => { + .then(([summaryRes, cashflowRes, merchantsRes, accountsRes, txRes]) => { if (!summaryRes.error) setSummary(summaryRes.data); - if (!cashflowRes.error) setCashflow(cashflowRes.data); - if (!merchantsRes.error) setMerchants(merchantsRes.data); - if (!accountsRes.error) setAccountCount(accountsRes.data.length); + if (!cashflowRes.error) setCashflow(cashflowRes.data ?? []); + if (!merchantsRes.error) setMerchants(merchantsRes.data ?? []); + if (!accountsRes.error) setAccountCount(accountsRes.data?.accounts?.length ?? 0); + if (!txRes.error) setRecentTxs(txRes.data?.transactions ?? []); }) .catch(() => undefined) .finally(() => setLoading(false)); @@ -743,26 +718,48 @@ export default function AppHomePage() { Date Description Category - Account Amount - {recentTransactions.map((tx) => ( - - {tx.date} - {tx.description} - - - {tx.category} - - - {tx.account} - - {tx.amount} + {loading ? ( + Array.from({ length: 4 }).map((_, i) => ( + +
+
+
+
+ + )) + ) : recentTxs.length ? ( + recentTxs.map((tx) => { + const amt = Number.parseFloat(tx.amount ?? "0"); + const fmtAmt = formatCurrency(amt); + const isIncome = amt >= 0; + return ( + + {new Date(tx.date).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })} + {tx.description} + + {tx.category ? ( + + {tx.category} + + ) : } + + + {fmtAmt} + + + ); + }) + ) : ( + + + No transactions yet. Connect a bank account to get started. - ))} + )}
diff --git a/app/auth/google/callback/page.tsx b/app/auth/google/callback/page.tsx new file mode 100644 index 0000000..5a864ce --- /dev/null +++ b/app/auth/google/callback/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { apiFetch } from "@/lib/api"; + +export default function GoogleCallbackPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [status, setStatus] = useState<"loading" | "success" | "error">("loading"); + const [message, setMessage] = useState(""); + + useEffect(() => { + const code = searchParams.get("code"); + const error = searchParams.get("error"); + + if (error) { + setStatus("error"); + setMessage(error === "access_denied" ? "You declined Google access." : `Google returned an error: ${error}`); + return; + } + + if (!code) { + setStatus("error"); + setMessage("No authorization code received from Google."); + return; + } + + apiFetch<{ connected: boolean; googleEmail: string }>("/api/google/exchange", { + method: "POST", + body: JSON.stringify({ code }), + }).then((res) => { + if (res.error) { + setStatus("error"); + setMessage(res.error.message ?? "Failed to connect Google account."); + } else { + setStatus("success"); + setMessage(`Connected as ${res.data?.googleEmail ?? "your Google account"}.`); + setTimeout(() => router.replace("/exports"), 2000); + } + }); + }, [searchParams, router]); + + return ( +
+
+ {status === "loading" && ( + <> +
+

Connecting your Google account...

+ + )} + {status === "success" && ( + <> +
+ + + +
+

Google Connected!

+

{message}

+

Redirecting to Exports...

+ + )} + {status === "error" && ( + <> +
+ + + +
+

Connection Failed

+

{message}

+ + + )} +
+
+ ); +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 92fdf96..db0bd37 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -1,106 +1,106 @@ -import Image from "next/image"; -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { ContactSection } from "../../../components/contact-section"; -import { DemoCta } from "../../../components/demo-cta"; -import { FaqSection } from "../../../components/faq-section"; -import { PageSchema } from "../../../components/page-schema"; -import { SiteFooter } from "../../../components/site-footer"; -import { SiteHeader } from "../../../components/site-header"; -import { blogPosts } from "../../../data/blog"; -import { defaultFaqs } from "../../../data/faq"; -import { siteInfo } from "../../../data/site"; - -type PageProps = { - params: { slug: string }; -}; - -export function generateStaticParams() { - return blogPosts.map((post) => ({ slug: post.slug })); -} - -export function generateMetadata({ params }: PageProps) { - const post = blogPosts.find((item) => item.slug === params.slug); - if (!post) { - return { title: "Blog Post" }; - } - return { - title: post.title, - description: post.excerpt, - keywords: [...siteInfo.keywords, "finance blog", "ledger insights"] - }; -} - +import Image from "next/image"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ContactSection } from "../../../components/contact-section"; +import { DemoCta } from "../../../components/demo-cta"; +import { FaqSection } from "../../../components/faq-section"; +import { PageSchema } from "../../../components/page-schema"; +import { SiteFooter } from "../../../components/site-footer"; +import { SiteHeader } from "../../../components/site-header"; +import { blogPosts } from "../../../data/blog"; +import { defaultFaqs } from "../../../data/faq"; +import { siteInfo } from "../../../data/site"; + +type PageProps = { + params: { slug: string }; +}; + +export function generateStaticParams() { + return blogPosts.map((post) => ({ slug: post.slug })); +} + +export function generateMetadata({ params }: PageProps) { + const post = blogPosts.find((item) => item.slug === params.slug); + if (!post) { + return { title: "Blog Post" }; + } + return { + title: post.title, + description: post.excerpt, + keywords: [...siteInfo.keywords, "finance blog", "ledger insights"] + }; +} + export default function BlogPostPage({ params }: PageProps) { - const post = blogPosts.find((item) => item.slug === params.slug); - if (!post) { - notFound(); - } - - const schema = [ - { - "@context": "https://schema.org", - "@type": "Article", - headline: post.title, - description: post.excerpt, - datePublished: post.date, - author: { - "@type": "Organization", - name: siteInfo.name - }, - image: post.image, - mainEntityOfPage: `${siteInfo.url}/blog/${post.slug}` - }, - { - "@context": "https://schema.org", - "@type": "FAQPage", - mainEntity: defaultFaqs.map((item) => ({ - "@type": "Question", - name: item.question, - acceptedAnswer: { "@type": "Answer", text: item.answer } - })) - } - ]; - + const post = blogPosts.find((item) => item.slug === params.slug); + if (!post) { + notFound(); + } + + const schema = [ + { + "@context": "https://schema.org", + "@type": "Article", + headline: post.title, + description: post.excerpt, + datePublished: post.date, + author: { + "@type": "Organization", + name: siteInfo.name + }, + image: post.image, + mainEntityOfPage: `${siteInfo.url}/blog/${post.slug}` + }, + { + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: defaultFaqs.map((item) => ({ + "@type": "Question", + name: item.question, + acceptedAnswer: { "@type": "Answer", text: item.answer } + })) + } + ]; + return (
-
-
-
- - -
+
+
+
+ + +
<- Back to blog -
+

{post.date} - {post.readTime}

-

{post.title}

-

{post.excerpt}

-
- {post.title} -
-
- {post.content.map((paragraph) => ( -

{paragraph}

- ))} -
-
-
-
- - - - - -
- ); -} +

{post.title}

+

{post.excerpt}

+
+ {post.title} +
+
+ {post.content.map((paragraph) => ( +

{paragraph}

+ ))} +
+ +
+
+ + + + + +
+ ); +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx index c1bc358..8390cd1 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,109 +1,109 @@ -import Link from "next/link"; -import { PageSchema } from "../../components/page-schema"; -import { SiteFooter } from "../../components/site-footer"; -import { SiteHeader } from "../../components/site-header"; -import { siteInfo } from "../../data/site"; - -export const metadata = { - title: "Blog", - description: "Insights on financial control, audit readiness, and ledger automation.", - keywords: siteInfo.keywords -}; - -const posts = [ - { - title: "Why 'Audit-Ready' Matters More Than Ever", - excerpt: - "As regulatory scrutiny increases, the ability to produce a clean, traceable ledger is becoming a competitive advantage.", - date: "Oct 24, 2023", - readTime: "5 min read", - slug: "audit-ready-matters" - }, - { - title: "The Hidden Cost of Spreadsheet Chaos", - excerpt: - "Manual reconciliation isn't just slow—it's a liability. Here's how to move to a system of record.", - date: "Oct 12, 2023", - readTime: "4 min read", - slug: "spreadsheet-chaos" - }, - { - title: "Automating the Month-End Close", - excerpt: - "How to use rules and categories to reduce your close time from days to hours.", - date: "Sep 28, 2023", - readTime: "6 min read", - slug: "automating-month-end" - } -]; - -export default function BlogPage() { - const schema = [ - { - "@context": "https://schema.org", - "@type": "WebPage", - name: "LedgerOne Blog", - description: "Insights on financial control, audit readiness, and ledger automation.", - url: `${siteInfo.url}/blog` - }, - { - "@context": "https://schema.org", - "@type": "Blog", - blogPost: posts.map((post) => ({ - "@type": "BlogPosting", - headline: post.title, - description: post.excerpt, - datePublished: post.date, - url: `${siteInfo.url}/blog/${post.slug}` - })) - } - ]; - - return ( -
- - -
-
- -
-
-

- Insights for modern finance teams. -

-

- Best practices for financial control, audit readiness, and automation. -

-
- -
- {posts.map((post) => ( - -
-
- - - {post.readTime} -
-

- {post.title} -

-

- {post.excerpt} -

-
- Read article - -
-
- - ))} -
-
-
- - - -
- ); -} +import Link from "next/link"; +import { PageSchema } from "../../components/page-schema"; +import { SiteFooter } from "../../components/site-footer"; +import { SiteHeader } from "../../components/site-header"; +import { siteInfo } from "../../data/site"; + +export const metadata = { + title: "Blog", + description: "Insights on financial control, audit readiness, and ledger automation.", + keywords: siteInfo.keywords +}; + +const posts = [ + { + title: "Why 'Audit-Ready' Matters More Than Ever", + excerpt: + "As regulatory scrutiny increases, the ability to produce a clean, traceable ledger is becoming a competitive advantage.", + date: "Oct 24, 2023", + readTime: "5 min read", + slug: "audit-ready-matters" + }, + { + title: "The Hidden Cost of Spreadsheet Chaos", + excerpt: + "Manual reconciliation isn't just slow—it's a liability. Here's how to move to a system of record.", + date: "Oct 12, 2023", + readTime: "4 min read", + slug: "spreadsheet-chaos" + }, + { + title: "Automating the Month-End Close", + excerpt: + "How to use rules and categories to reduce your close time from days to hours.", + date: "Sep 28, 2023", + readTime: "6 min read", + slug: "automating-month-end" + } +]; + +export default function BlogPage() { + const schema = [ + { + "@context": "https://schema.org", + "@type": "WebPage", + name: "LedgerOne Blog", + description: "Insights on financial control, audit readiness, and ledger automation.", + url: `${siteInfo.url}/blog` + }, + { + "@context": "https://schema.org", + "@type": "Blog", + blogPost: posts.map((post) => ({ + "@type": "BlogPosting", + headline: post.title, + description: post.excerpt, + datePublished: post.date, + url: `${siteInfo.url}/blog/${post.slug}` + })) + } + ]; + + return ( +
+ + +
+
+ +
+
+

+ Insights for modern finance teams. +

+

+ Best practices for financial control, audit readiness, and automation. +

+
+ +
+ {posts.map((post) => ( + +
+
+ + + {post.readTime} +
+

+ {post.title} +

+

+ {post.excerpt} +

+
+ Read article + +
+
+ + ))} +
+
+
+ + + +
+ ); +} diff --git a/app/book-demo/page.tsx b/app/book-demo/page.tsx index 9e4d123..df237a8 100644 --- a/app/book-demo/page.tsx +++ b/app/book-demo/page.tsx @@ -5,132 +5,132 @@ import { FaqSection } from "../../components/faq-section"; import { PageSchema } from "../../components/page-schema"; import { SiteFooter } from "../../components/site-footer"; import { SiteHeader } from "../../components/site-header"; -import { defaultFaqs } from "../../data/faq"; -import { siteInfo } from "../../data/site"; - +import { defaultFaqs } from "../../data/faq"; +import { siteInfo } from "../../data/site"; + export const metadata = { title: "Book a Demo", description: "Book a LedgerOne demo to see audit-ready ledgering and transparent rules in action.", keywords: siteInfo.keywords }; - + export default function BookDemoPage() { - const schema = [ - { - "@context": "https://schema.org", - "@type": "WebPage", - name: "Book a LedgerOne Demo", - description: - "Book a LedgerOne demo to see audit-ready ledgering and transparent rules in action.", - url: `${siteInfo.url}/book-demo` - }, - { - "@context": "https://schema.org", - "@type": "FAQPage", - mainEntity: defaultFaqs.map((item) => ({ - "@type": "Question", - name: item.question, - acceptedAnswer: { "@type": "Answer", text: item.answer } - })) - } - ]; - + const schema = [ + { + "@context": "https://schema.org", + "@type": "WebPage", + name: "Book a LedgerOne Demo", + description: + "Book a LedgerOne demo to see audit-ready ledgering and transparent rules in action.", + url: `${siteInfo.url}/book-demo` + }, + { + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: defaultFaqs.map((item) => ({ + "@type": "Question", + name: item.question, + acceptedAnswer: { "@type": "Answer", text: item.answer } + })) + } + ]; + return (
-
-
-
- - -
-
-
-

Book a demo

-

- Schedule time with the LedgerOne team. -

-

- We will walk you through account connections, rule automation, and - audit-ready exports based on your workflow. -

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -