Skip to content

Commit 3148cc2

Browse files
committed
open stripe urls
1 parent e3e5b12 commit 3148cc2

File tree

6 files changed

+364
-18
lines changed

6 files changed

+364
-18
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Breadcrumb } from '@/components/layout/breadcrumb';
2+
import { PriceDetail } from '@/components/subscriptions/price-detail';
3+
4+
export default function PriceDetailPage({ params }: { params: { id: string } }) {
5+
return (
6+
<div className="bg-background">
7+
<div className="container mx-auto px-4 py-8 space-y-6">
8+
<Breadcrumb
9+
items={[
10+
{ label: 'Subscriptions', href: '/subscriptions' },
11+
{ label: 'Price Details', href: `/subscriptions/prices/${params.id}` },
12+
]}
13+
/>
14+
<PriceDetail priceId={params.id} />
15+
</div>
16+
</div>
17+
);
18+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
'use client';
2+
3+
import { api } from '@/trpc/react';
4+
import { Badge } from '@onlook/ui/badge';
5+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@onlook/ui/card';
6+
import { Skeleton } from '@onlook/ui/skeleton';
7+
import {
8+
Table,
9+
TableBody,
10+
TableCell,
11+
TableHead,
12+
TableHeader,
13+
TableRow,
14+
} from '@onlook/ui/table';
15+
import { ExternalLink } from 'lucide-react';
16+
import { useRouter } from 'next/navigation';
17+
import { Button } from '@onlook/ui/button';
18+
19+
interface PriceDetailProps {
20+
priceId: string;
21+
}
22+
23+
export function PriceDetail({ priceId }: PriceDetailProps) {
24+
const router = useRouter();
25+
const { data, isLoading, error } = api.subscriptions.getPriceDetail.useQuery({ priceId });
26+
27+
if (error) {
28+
return (
29+
<Card className="border-destructive/50">
30+
<CardHeader>
31+
<CardTitle className="text-destructive">Error Loading Price Details</CardTitle>
32+
<CardDescription>{error.message}</CardDescription>
33+
</CardHeader>
34+
</Card>
35+
);
36+
}
37+
38+
if (isLoading || !data) {
39+
return (
40+
<div className="space-y-6">
41+
<Card>
42+
<CardHeader>
43+
<Skeleton className="h-8 w-48" />
44+
<Skeleton className="h-4 w-64 mt-2" />
45+
</CardHeader>
46+
<CardContent>
47+
<div className="space-y-2">
48+
<Skeleton className="h-4 w-full" />
49+
<Skeleton className="h-4 w-full" />
50+
<Skeleton className="h-4 w-full" />
51+
</div>
52+
</CardContent>
53+
</Card>
54+
<Card>
55+
<CardHeader>
56+
<Skeleton className="h-6 w-32" />
57+
</CardHeader>
58+
<CardContent>
59+
<Skeleton className="h-64 w-full" />
60+
</CardContent>
61+
</Card>
62+
</div>
63+
);
64+
}
65+
66+
return (
67+
<div className="space-y-6">
68+
{/* Price Info Card */}
69+
<Card>
70+
<CardHeader>
71+
<CardTitle>{data.price.key}</CardTitle>
72+
<CardDescription>
73+
{data.price.monthlyMessageLimit.toLocaleString()} messages per month
74+
</CardDescription>
75+
</CardHeader>
76+
<CardContent className="space-y-3">
77+
<div>
78+
<p className="text-sm font-medium text-muted-foreground">Product</p>
79+
<p className="text-sm">{data.price.productName}</p>
80+
<Button
81+
variant="link"
82+
className="h-auto p-0 text-xs font-mono mt-1"
83+
onClick={() => window.open(`https://dashboard.stripe.com/products/${data.price.stripeProductId}`, '_blank')}
84+
>
85+
{data.price.stripeProductId}
86+
<ExternalLink className="ml-1 size-3" />
87+
</Button>
88+
</div>
89+
<div>
90+
<p className="text-sm font-medium text-muted-foreground">Stripe Price ID</p>
91+
<Button
92+
variant="link"
93+
className="h-auto p-0 text-xs font-mono"
94+
onClick={() => window.open(`https://dashboard.stripe.com/prices/${data.price.stripePriceId}`, '_blank')}
95+
>
96+
{data.price.stripePriceId}
97+
<ExternalLink className="ml-1 size-3" />
98+
</Button>
99+
</div>
100+
</CardContent>
101+
</Card>
102+
103+
{/* Subscribers Card */}
104+
<Card>
105+
<CardHeader>
106+
<CardTitle>Subscribers</CardTitle>
107+
<CardDescription>
108+
{data.users.length} {data.users.length === 1 ? 'user' : 'users'} subscribed to this plan
109+
</CardDescription>
110+
</CardHeader>
111+
<CardContent>
112+
{data.users.length === 0 ? (
113+
<div className="text-center py-8 text-muted-foreground">
114+
No users subscribed to this plan yet
115+
</div>
116+
) : (
117+
<Table>
118+
<TableHeader>
119+
<TableRow>
120+
<TableHead>User</TableHead>
121+
<TableHead>Status</TableHead>
122+
<TableHead>Started</TableHead>
123+
<TableHead>Current Period</TableHead>
124+
</TableRow>
125+
</TableHeader>
126+
<TableBody>
127+
{data.users.map((user) => (
128+
<TableRow
129+
key={user.subscriptionId}
130+
className="cursor-pointer"
131+
onClick={() => router.push(`/users/${user.userId}`)}
132+
>
133+
<TableCell>
134+
<div>
135+
<p className="font-medium">{user.userName}</p>
136+
<p className="text-xs text-muted-foreground">
137+
{user.userEmail}
138+
</p>
139+
</div>
140+
</TableCell>
141+
<TableCell>
142+
<Badge variant={user.subscriptionStatus === 'active' ? 'default' : 'secondary'}>
143+
{user.subscriptionStatus}
144+
</Badge>
145+
</TableCell>
146+
<TableCell>
147+
<p className="text-sm">
148+
{new Date(user.startedAt).toLocaleDateString('en-US', {
149+
month: 'short',
150+
day: 'numeric',
151+
year: 'numeric',
152+
})}
153+
</p>
154+
</TableCell>
155+
<TableCell>
156+
<div className="text-sm">
157+
<p>
158+
{new Date(user.stripeCurrentPeriodStart).toLocaleDateString('en-US', {
159+
month: 'short',
160+
day: 'numeric',
161+
})}
162+
</p>
163+
<p className="text-xs text-muted-foreground">
164+
to {new Date(user.stripeCurrentPeriodEnd).toLocaleDateString('en-US', {
165+
month: 'short',
166+
day: 'numeric',
167+
year: 'numeric',
168+
})}
169+
</p>
170+
</div>
171+
</TableCell>
172+
</TableRow>
173+
))}
174+
</TableBody>
175+
</Table>
176+
)}
177+
</CardContent>
178+
</Card>
179+
</div>
180+
);
181+
}

apps/admin/src/components/subscriptions/products-list.tsx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import { api } from '@/trpc/react';
44
import { Badge } from '@onlook/ui/badge';
5+
import { Button } from '@onlook/ui/button';
56
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@onlook/ui/card';
67
import { Skeleton } from '@onlook/ui/skeleton';
8+
import { ExternalLink } from 'lucide-react';
9+
import { useRouter } from 'next/navigation';
710

811
export function ProductsList() {
12+
const router = useRouter();
913
const { data: products, isLoading, error } = api.subscriptions.listProducts.useQuery();
1014

1115
if (error) {
@@ -52,9 +56,20 @@ export function ProductsList() {
5256
<div key={product.id} className="border rounded-lg p-4 space-y-3">
5357
<div>
5458
<h3 className="font-semibold text-lg">{product.name}</h3>
55-
<p className="text-xs text-muted-foreground mt-1 font-mono">
56-
Stripe Product ID: {product.stripeProductId}
57-
</p>
59+
<div className="flex items-center gap-2 mt-1">
60+
<span className="text-xs text-muted-foreground">Stripe Product ID:</span>
61+
<Button
62+
variant="link"
63+
className="h-auto p-0 text-xs font-mono"
64+
onClick={(e) => {
65+
e.stopPropagation();
66+
window.open(`https://dashboard.stripe.com/products/${product.stripeProductId}`, '_blank');
67+
}}
68+
>
69+
{product.stripeProductId}
70+
<ExternalLink className="ml-1 size-3" />
71+
</Button>
72+
</div>
5873
</div>
5974
{product.prices.length > 0 && (
6075
<div className="space-y-2">
@@ -63,7 +78,8 @@ export function ProductsList() {
6378
{product.prices.map((price) => (
6479
<div
6580
key={price.id}
66-
className="flex items-center justify-between p-3 bg-muted rounded-lg"
81+
className="flex items-center justify-between p-3 bg-muted rounded-lg cursor-pointer hover:bg-muted/80 transition-colors"
82+
onClick={() => router.push(`/subscriptions/prices/${price.id}`)}
6783
>
6884
<div>
6985
<p className="font-medium text-sm">{price.key}</p>
@@ -73,9 +89,19 @@ export function ProductsList() {
7389
{price.subscriberCount} {price.subscriberCount === 1 ? 'subscriber' : 'subscribers'}
7490
</p>
7591
</div>
76-
<Badge variant="outline" className="font-mono text-xs">
77-
{price.stripePriceId}
78-
</Badge>
92+
<Button
93+
variant="link"
94+
className="h-auto p-0"
95+
onClick={(e) => {
96+
e.stopPropagation();
97+
window.open(`https://dashboard.stripe.com/prices/${price.stripePriceId}`, '_blank');
98+
}}
99+
>
100+
<Badge variant="outline" className="font-mono text-xs">
101+
{price.stripePriceId}
102+
<ExternalLink className="ml-1 size-3" />
103+
</Badge>
104+
</Button>
79105
</div>
80106
))}
81107
</div>

apps/admin/src/components/subscriptions/subscriptions-list.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,37 @@ import {
1313
TableHeader,
1414
TableRow,
1515
} from '@onlook/ui/table';
16-
import { ChevronLeft, ChevronRight } from 'lucide-react';
16+
import { ArrowUpDown, ChevronLeft, ChevronRight } from 'lucide-react';
1717
import { useRouter } from 'next/navigation';
1818
import { useState } from 'react';
1919

20+
type SortBy = 'startedAt' | 'priceKey' | 'status';
21+
type SortOrder = 'asc' | 'desc';
22+
2023
export function SubscriptionsList() {
2124
const router = useRouter();
2225
const [page, setPage] = useState(1);
2326
const [pageSize] = useState(20);
27+
const [sortBy, setSortBy] = useState<SortBy>('startedAt');
28+
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
2429

2530
const { data, isLoading, error } = api.subscriptions.listSubscriptions.useQuery({
2631
page,
2732
pageSize,
33+
sortBy,
34+
sortOrder,
2835
});
2936

37+
const handleSort = (column: SortBy) => {
38+
if (sortBy === column) {
39+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
40+
} else {
41+
setSortBy(column);
42+
setSortOrder('desc');
43+
}
44+
setPage(1); // Reset to first page when sorting changes
45+
};
46+
3047
if (error) {
3148
return (
3249
<Card className="border-destructive/50">
@@ -53,9 +70,39 @@ export function SubscriptionsList() {
5370
<TableRow>
5471
<TableHead>User</TableHead>
5572
<TableHead>Product</TableHead>
56-
<TableHead>Plan</TableHead>
57-
<TableHead>Status</TableHead>
58-
<TableHead>Period</TableHead>
73+
<TableHead>
74+
<Button
75+
variant="ghost"
76+
size="sm"
77+
className="-ml-3 h-8 data-[state=open]:bg-accent"
78+
onClick={() => handleSort('priceKey')}
79+
>
80+
Plan
81+
<ArrowUpDown className="ml-2 size-4" />
82+
</Button>
83+
</TableHead>
84+
<TableHead>
85+
<Button
86+
variant="ghost"
87+
size="sm"
88+
className="-ml-3 h-8 data-[state=open]:bg-accent"
89+
onClick={() => handleSort('status')}
90+
>
91+
Status
92+
<ArrowUpDown className="ml-2 size-4" />
93+
</Button>
94+
</TableHead>
95+
<TableHead>
96+
<Button
97+
variant="ghost"
98+
size="sm"
99+
className="-ml-3 h-8 data-[state=open]:bg-accent"
100+
onClick={() => handleSort('startedAt')}
101+
>
102+
Period
103+
<ArrowUpDown className="ml-2 size-4" />
104+
</Button>
105+
</TableHead>
59106
</TableRow>
60107
</TableHeader>
61108
<TableBody>

apps/admin/src/components/users/user-detail.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Badge } from '@onlook/ui/badge';
66
import { Button } from '@onlook/ui/button';
77
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@onlook/ui/card';
88
import { Skeleton } from '@onlook/ui/skeleton';
9-
import { ChevronDown, ChevronRight } from 'lucide-react';
9+
import { ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
1010
import { useRouter } from 'next/navigation';
1111
import { useState } from 'react';
1212
import { EditRateLimit } from './edit-rate-limit';
@@ -89,9 +89,18 @@ export function UserDetail({ userId }: UserDetailProps) {
8989
<div className="grid grid-cols-2 gap-4">
9090
<div>
9191
<p className="text-sm font-medium text-muted-foreground">Stripe Customer ID</p>
92-
<p className="text-sm font-mono mt-1">
93-
{user.stripeCustomerId || '—'}
94-
</p>
92+
{user.stripeCustomerId ? (
93+
<Button
94+
variant="link"
95+
className="h-auto p-0 text-sm font-mono mt-1"
96+
onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank')}
97+
>
98+
{user.stripeCustomerId}
99+
<ExternalLink className="ml-1 size-3" />
100+
</Button>
101+
) : (
102+
<p className="text-sm font-mono mt-1"></p>
103+
)}
95104
</div>
96105
<div>
97106
<p className="text-sm font-medium text-muted-foreground">GitHub Installation ID</p>

0 commit comments

Comments
 (0)