Skip to content

Commit 332e6d5

Browse files
committed
feat: initial scaffolding for ecommerce manage page
1 parent 2b31c13 commit 332e6d5

File tree

12 files changed

+256
-0
lines changed

12 files changed

+256
-0
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ REDIS_URL=redis://localhost:7379
1818
# NEXT_PUBLIC_GTM_ID=""
1919
# NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS=""
2020
INVOICE_PROCESSING_TTL=""
21+
NEXT_PUBLIC_DEFAULT_ECOMMERCE_DOMAIN=http://localhost:3001

src/app/ecommerce/layout.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { BackgroundWrapper } from "@/components/background-wrapper";
2+
import { EcommerceNavigation } from "@/components/ecommerce/ecommerce-navigation";
3+
import { Footer } from "@/components/footer";
4+
import { Header } from "@/components/header";
5+
import { getCurrentSession } from "@/server/auth";
6+
import { PlusCircle } from "lucide-react";
7+
import Link from "next/link";
8+
import { redirect } from "next/navigation";
9+
10+
export default async function EcommerceLayout({
11+
children,
12+
}: {
13+
children: React.ReactNode;
14+
}) {
15+
const { user } = await getCurrentSession();
16+
if (!user) redirect("/");
17+
18+
return (
19+
<BackgroundWrapper
20+
topGradient={{ from: "orange-100", to: "orange-200" }}
21+
bottomGradient={{ from: "zinc-100", to: "zinc-200" }}
22+
>
23+
<Header user={user} />
24+
<main className="flex-grow flex flex-col w-full max-w-sm sm:max-w-2xl md:max-w-4xl lg:max-w-6xl xl:max-w-[72rem] mx-auto px-4 sm:px-6 lg:px-8 py-8 z-10">
25+
<div className="flex justify-between items-center mb-8">
26+
<h1 className="mb-2 text-4xl font-bold tracking-tight">Dashboard</h1>
27+
<Link
28+
href="/invoices/create"
29+
className="bg-black hover:bg-zinc-800 text-white transition-colors px-4 py-2 rounded-md flex items-center"
30+
>
31+
<PlusCircle className="mr-2 h-4 w-4" />
32+
Create Invoice
33+
</Link>
34+
</div>
35+
<EcommerceNavigation />
36+
{children}
37+
</main>
38+
<Footer />
39+
</BackgroundWrapper>
40+
);
41+
}

src/app/ecommerce/manage/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { EcommerceManage } from "@/components/ecommerce/manage";
2+
import { getCurrentSession } from "@/server/auth";
3+
import { api } from "@/trpc/server";
4+
import { redirect } from "next/navigation";
5+
6+
export default async function ManagePage() {
7+
const { user } = await getCurrentSession();
8+
9+
if (!user) {
10+
redirect("/");
11+
}
12+
13+
const clientIds = await api.clientId.getAll.query();
14+
15+
return <EcommerceManage initialClientIds={clientIds} />;
16+
}

src/app/ecommerce/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { redirect } from "next/navigation";
2+
3+
export default function EcommercePage() {
4+
redirect("/ecommerce/manage");
5+
}

src/app/ecommerce/sales/page.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getCurrentSession } from "@/server/auth";
2+
//import { api } from "@/trpc/server";
3+
import { redirect } from "next/navigation";
4+
5+
export default async function SalesPage() {
6+
const { user } = await getCurrentSession();
7+
8+
if (!user) {
9+
redirect("/");
10+
}
11+
12+
// TODO fetch sales data
13+
14+
return <div>Sales Page - to be implemented</div>;
15+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client";
2+
3+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
4+
import Link from "next/link";
5+
import { usePathname } from "next/navigation";
6+
import { useEffect, useState } from "react";
7+
8+
export function EcommerceNavigation() {
9+
const pathname = usePathname();
10+
const [activeTab, setActiveTab] = useState("manage");
11+
12+
useEffect(() => {
13+
if (pathname.includes("/sales")) {
14+
setActiveTab("sales");
15+
} else {
16+
setActiveTab("manage");
17+
}
18+
}, [pathname]);
19+
20+
return (
21+
<Tabs value={activeTab} className="w-full mb-8">
22+
<TabsList className="grid w-full grid-cols-3">
23+
<TabsTrigger value="manage" asChild>
24+
<Link href="/ecommerce/manage">Manage</Link>
25+
</TabsTrigger>
26+
<TabsTrigger value="sales" asChild>
27+
<Link href="/ecommerce/sales">Sales</Link>
28+
</TabsTrigger>
29+
</TabsList>
30+
</Tabs>
31+
);
32+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ClientId } from "@/server/db/schema";
2+
3+
interface EcommerceManageProps {
4+
initialClientIds: ClientId[];
5+
}
6+
7+
export function EcommerceManage({
8+
initialClientIds: _initialClientIds,
9+
}: EcommerceManageProps) {
10+
return (
11+
<div>
12+
<h1>Ecommerce Manage</h1>
13+
</div>
14+
);
15+
}

src/lib/constants/ecommerce.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const DEFAULT_CLIENT_ID_DOMAIN =
2+
process.env.NEXT_PUBLIC_DEFAULT_ECOMMERCE_DOMAIN ||
3+
"https://checkout.request.network";

src/lib/schemas/client-id.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { isEthereumAddress } from "validator";
2+
import { z } from "zod";
3+
4+
export const clientIdApiSchema = z.object({
5+
label: z.string().min(1, "Label is required"),
6+
domain: z.string().url(),
7+
feeAddress: z
8+
.string()
9+
.min(1, "Fee address is required")
10+
.refine(isEthereumAddress, "Invalid Ethereum address format")
11+
.optional(),
12+
feePercentage: z.coerce
13+
.number()
14+
.min(0, "Fee percentage must be at least 0")
15+
.max(100, "Fee percentage cannot exceed 100")
16+
.optional(),
17+
});

src/server/db/schema.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,21 @@ export const subscriptionPlanTable = createTable("subscription_plans", {
299299
createdAt: timestamp("created_at").defaultNow(),
300300
});
301301

302+
export const clientIdTable = createTable("client_id", {
303+
id: text().primaryKey().notNull(),
304+
clientId: text().notNull(),
305+
userId: text()
306+
.notNull()
307+
.references(() => userTable.id, {
308+
onDelete: "cascade",
309+
}),
310+
label: text().notNull(),
311+
domain: text().notNull(),
312+
feeAddress: text(),
313+
feePercentage: text(),
314+
createdAt: timestamp("created_at").defaultNow(),
315+
});
316+
302317
// Relationships
303318

304319
export const userRelations = relations(userTable, ({ many }) => ({
@@ -358,6 +373,13 @@ export const subscriptionPlanRelations = relations(
358373
}),
359374
);
360375

376+
export const clientIdRelations = relations(clientIdTable, ({ one }) => ({
377+
user: one(userTable, {
378+
fields: [clientIdTable.userId],
379+
references: [userTable.id],
380+
}),
381+
}));
382+
361383
export const paymentDetailsRelations = relations(
362384
paymentDetailsTable,
363385
({ one, many }) => ({
@@ -393,3 +415,4 @@ export type PaymentDetailsPayers = InferSelectModel<
393415
typeof paymentDetailsPayersTable
394416
>;
395417
export type RecurringPayment = InferSelectModel<typeof recurringPaymentTable>;
418+
export type ClientId = InferSelectModel<typeof clientIdTable>;

0 commit comments

Comments
 (0)