Skip to content

Commit b688754

Browse files
committed
feat: add display of clientids and creation
1 parent f192df1 commit b688754

File tree

7 files changed

+406
-20
lines changed

7 files changed

+406
-20
lines changed

src/app/ecommerce/layout.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { EcommerceNavigation } from "@/components/ecommerce/ecommerce-navigation
33
import { Footer } from "@/components/footer";
44
import { Header } from "@/components/header";
55
import { getCurrentSession } from "@/server/auth";
6-
import { PlusCircle } from "lucide-react";
7-
import Link from "next/link";
86
import { redirect } from "next/navigation";
97

108
export default async function EcommerceLayout({
@@ -22,16 +20,10 @@ export default async function EcommerceLayout({
2220
>
2321
<Header user={user} />
2422
<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>
23+
<h1 className="mb-2 text-4xl font-bold tracking-tight">Ecommerce</h1>
24+
<p className="mb-8 text-lg text-muted-foreground">
25+
Create and manage your ecommerce clients and view sales
26+
</p>
3527
<EcommerceNavigation />
3628
{children}
3729
</main>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"use client";
2+
3+
import { ShortAddress } from "@/components/short-address";
4+
import { Card, CardContent } from "@/components/ui/card";
5+
import {
6+
Select,
7+
SelectContent,
8+
SelectItem,
9+
SelectTrigger,
10+
SelectValue,
11+
} from "@/components/ui/select";
12+
import { EmptyState } from "@/components/ui/table/empty-state";
13+
import {
14+
Table,
15+
TableBody,
16+
TableCell,
17+
TableHeader,
18+
TableRow,
19+
} from "@/components/ui/table/table";
20+
import { TableHeadCell } from "@/components/ui/table/table-head-cell";
21+
import type { ClientId } from "@/server/db/schema";
22+
import { format } from "date-fns";
23+
import { CreditCard, Filter } from "lucide-react";
24+
import { useState } from "react";
25+
26+
interface ClientIdsTableProps {
27+
clientIds: ClientId[];
28+
}
29+
30+
const ClientIdTableColumns = () => (
31+
<TableRow className="hover:bg-transparent border-none">
32+
<TableHeadCell>Created Date</TableHeadCell>
33+
<TableHeadCell>Label</TableHeadCell>
34+
<TableHeadCell>Domain</TableHeadCell>
35+
<TableHeadCell>Client ID</TableHeadCell>
36+
<TableHeadCell>Fee Address</TableHeadCell>
37+
<TableHeadCell>Fee Percentage</TableHeadCell>
38+
</TableRow>
39+
);
40+
41+
const ClientIdRow = ({ clientId }: { clientId: ClientId }) => {
42+
return (
43+
<TableRow className="hover:bg-zinc-50/50">
44+
<TableCell>
45+
{clientId.createdAt
46+
? format(new Date(clientId.createdAt), "do MMM yyyy")
47+
: "N/A"}
48+
</TableCell>
49+
<TableCell className="font-medium">{clientId.label}</TableCell>
50+
<TableCell>{clientId.domain}</TableCell>
51+
<TableCell>
52+
<ShortAddress address={clientId.clientId} />
53+
</TableCell>
54+
<TableCell>
55+
{clientId.feeAddress ? (
56+
<ShortAddress address={clientId.feeAddress} />
57+
) : (
58+
<span className="text-zinc-500">-</span>
59+
)}
60+
</TableCell>
61+
<TableCell>
62+
{clientId.feePercentage ? (
63+
<span>{clientId.feePercentage}%</span>
64+
) : (
65+
<span className="text-zinc-500">-</span>
66+
)}
67+
</TableCell>
68+
</TableRow>
69+
);
70+
};
71+
72+
export function ClientIdsTable({ clientIds }: ClientIdsTableProps) {
73+
const [activeClient, setActiveClient] = useState<string | null>(null);
74+
75+
const filteredClientIds = activeClient
76+
? clientIds.filter(({ clientId }) => clientId === activeClient)
77+
: clientIds;
78+
79+
return (
80+
<div className="space-y-6 w-full">
81+
<div className="flex items-center gap-4 mb-6">
82+
<div className="flex items-center gap-2">
83+
<Filter className="h-4 w-4 text-zinc-600" />
84+
<span className="text-sm font-medium text-zinc-700">
85+
Filter by client:
86+
</span>
87+
</div>
88+
<Select
89+
value={activeClient || "all"}
90+
onValueChange={(value) =>
91+
setActiveClient(value === "all" ? null : value)
92+
}
93+
>
94+
<SelectTrigger className="w-[200px]">
95+
<SelectValue placeholder="All Client Ids" />
96+
</SelectTrigger>
97+
<SelectContent>
98+
<SelectItem value="all">All Clients</SelectItem>
99+
{clientIds.map(({ clientId, label }) => (
100+
<SelectItem key={clientId} value={clientId}>
101+
{label}
102+
</SelectItem>
103+
))}
104+
</SelectContent>
105+
</Select>
106+
</div>
107+
108+
<Card className="border border-zinc-100">
109+
<CardContent className="p-0">
110+
<Table>
111+
<TableHeader>
112+
<ClientIdTableColumns />
113+
</TableHeader>
114+
<TableBody>
115+
{filteredClientIds.length === 0 ? (
116+
<TableRow>
117+
<TableCell colSpan={6} className="p-0">
118+
<EmptyState
119+
icon={<CreditCard className="h-6 w-6 text-zinc-600" />}
120+
title="No client IDs"
121+
subtitle={
122+
activeClient
123+
? "No client IDs found for the selected client"
124+
: "No client IDs found"
125+
}
126+
/>
127+
</TableCell>
128+
</TableRow>
129+
) : (
130+
filteredClientIds.map((clientId) => (
131+
<ClientIdRow key={clientId.id} clientId={clientId} />
132+
))
133+
)}
134+
</TableBody>
135+
</Table>
136+
</CardContent>
137+
</Card>
138+
</div>
139+
);
140+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogHeader,
8+
DialogTitle,
9+
DialogTrigger,
10+
} from "@/components/ui/dialog";
11+
import {
12+
Form,
13+
FormControl,
14+
FormField,
15+
FormItem,
16+
FormLabel,
17+
FormMessage,
18+
} from "@/components/ui/form";
19+
import { Input } from "@/components/ui/input";
20+
import { clientIdApiSchema } from "@/lib/schemas/client-id";
21+
import { api } from "@/trpc/react";
22+
import { zodResolver } from "@hookform/resolvers/zod";
23+
import { Loader2, Plus } from "lucide-react";
24+
import { useState } from "react";
25+
import { useForm } from "react-hook-form";
26+
import { toast } from "sonner";
27+
import type { z } from "zod";
28+
29+
const createClientIdFormSchema = clientIdApiSchema;
30+
31+
type CreateClientIdFormValues = z.infer<typeof createClientIdFormSchema>;
32+
33+
export function CreateClientId() {
34+
const [isOpen, setIsOpen] = useState(false);
35+
const utils = api.useUtils();
36+
37+
const { mutate: createClientIdMutation, isLoading } =
38+
api.clientId.create.useMutation({
39+
onSuccess: () => {
40+
toast.success("Client ID created successfully");
41+
utils.clientId.getAll.invalidate();
42+
setIsOpen(false);
43+
form.reset();
44+
},
45+
onError: (error) => {
46+
toast.error(error.message || "Failed to create client ID");
47+
},
48+
});
49+
50+
const form = useForm<CreateClientIdFormValues>({
51+
resolver: zodResolver(createClientIdFormSchema),
52+
defaultValues: {
53+
label: "",
54+
domain: "",
55+
feeAddress: undefined,
56+
feePercentage: undefined,
57+
},
58+
});
59+
60+
const onSubmit = async (data: CreateClientIdFormValues) => {
61+
createClientIdMutation(data);
62+
};
63+
64+
return (
65+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
66+
<DialogTrigger asChild>
67+
<Button>
68+
<Plus className="mr-2 h-4 w-4" />
69+
Create Client ID
70+
</Button>
71+
</DialogTrigger>
72+
<DialogContent>
73+
<DialogHeader>
74+
<DialogTitle>Create New Client ID</DialogTitle>
75+
</DialogHeader>
76+
<Form {...form}>
77+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
78+
<FormField
79+
control={form.control}
80+
name="label"
81+
render={({ field }) => (
82+
<FormItem>
83+
<FormLabel>Label</FormLabel>
84+
<FormControl>
85+
<Input
86+
placeholder="My Ecommerce Store"
87+
disabled={isLoading}
88+
{...field}
89+
/>
90+
</FormControl>
91+
<FormMessage />
92+
</FormItem>
93+
)}
94+
/>
95+
96+
<FormField
97+
control={form.control}
98+
name="domain"
99+
render={({ field }) => (
100+
<FormItem>
101+
<FormLabel>Domain</FormLabel>
102+
<FormControl>
103+
<Input
104+
placeholder="https://mystore.com"
105+
disabled={isLoading}
106+
{...field}
107+
/>
108+
</FormControl>
109+
<FormMessage />
110+
</FormItem>
111+
)}
112+
/>
113+
114+
<FormField
115+
control={form.control}
116+
name="feeAddress"
117+
render={({ field }) => (
118+
<FormItem>
119+
<FormLabel>Fee Address (Optional)</FormLabel>
120+
<FormControl>
121+
<Input
122+
placeholder="0x..."
123+
className="font-mono"
124+
disabled={isLoading}
125+
{...field}
126+
/>
127+
</FormControl>
128+
<FormMessage />
129+
</FormItem>
130+
)}
131+
/>
132+
133+
<FormField
134+
control={form.control}
135+
name="feePercentage"
136+
render={({ field }) => (
137+
<FormItem>
138+
<FormLabel>Fee Percentage (Optional)</FormLabel>
139+
<FormControl>
140+
<Input
141+
type="number"
142+
placeholder="5"
143+
min="0"
144+
max="100"
145+
step="0.1"
146+
disabled={isLoading}
147+
value={field.value || ""}
148+
onChange={(e) =>
149+
field.onChange(e.target.value ?? undefined)
150+
}
151+
/>
152+
</FormControl>
153+
<FormMessage />
154+
</FormItem>
155+
)}
156+
/>
157+
158+
<div className="flex justify-end gap-2 pt-4">
159+
<Button
160+
type="button"
161+
variant="outline"
162+
onClick={() => setIsOpen(false)}
163+
disabled={isLoading}
164+
>
165+
Cancel
166+
</Button>
167+
<Button type="submit" disabled={isLoading}>
168+
{isLoading ? (
169+
<>
170+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
171+
Creating...
172+
</>
173+
) : (
174+
<>
175+
<Plus className="mr-2 h-4 w-4" />
176+
Create Client ID
177+
</>
178+
)}
179+
</Button>
180+
</div>
181+
</form>
182+
</Form>
183+
</DialogContent>
184+
</Dialog>
185+
);
186+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { DEFAULT_CLIENT_ID_DOMAIN } from "@/lib/constants/ecommerce";
5+
import { api } from "@/trpc/react";
6+
import { Loader2 } from "lucide-react";
7+
import { toast } from "sonner";
8+
9+
export function CreateDefaultClientId() {
10+
const utils = api.useUtils();
11+
12+
const createClientId = api.clientId.create.useMutation({
13+
onSuccess: () => {
14+
toast.success("Default client ID created successfully");
15+
utils.clientId.getAll.invalidate();
16+
},
17+
onError: (error) => {
18+
toast.error(error.message || "Failed to create default client ID");
19+
},
20+
});
21+
22+
const handleCreateDefault = () => {
23+
createClientId.mutate({
24+
label: "Default Ecommerce",
25+
domain: DEFAULT_CLIENT_ID_DOMAIN,
26+
});
27+
};
28+
29+
return (
30+
<Button
31+
onClick={handleCreateDefault}
32+
disabled={createClientId.isLoading}
33+
variant="default"
34+
>
35+
{createClientId.isLoading && (
36+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
37+
)}
38+
Create Default Client ID
39+
</Button>
40+
);
41+
}

0 commit comments

Comments
 (0)