Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertTriangle,
|
|
RefreshCw,
|
|
ExternalLink,
|
|
Package,
|
|
ShoppingCart,
|
|
CreditCard
|
|
} from 'lucide-react'
|
|
|
|
interface StripeStatus {
|
|
success: boolean
|
|
account?: {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
country: string
|
|
charges_enabled: boolean
|
|
payouts_enabled: boolean
|
|
}
|
|
products?: {
|
|
total: number
|
|
active: number
|
|
sample: Array<{
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
price?: {
|
|
id: string
|
|
unit_amount: number
|
|
currency: string
|
|
}
|
|
}>
|
|
}
|
|
paymentMethods?: {
|
|
total: number
|
|
types: string[]
|
|
}
|
|
recentInvoices: number
|
|
environment: string
|
|
apiVersion: string
|
|
timestamp: string
|
|
error?: string
|
|
}
|
|
|
|
export function StripeStatus() {
|
|
const [status, setStatus] = useState<StripeStatus | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [lastUpdated, setLastUpdated] = useState<string>('')
|
|
|
|
const fetchStatus = async () => {
|
|
try {
|
|
const response = await fetch('/api/stripe/test', {
|
|
method: 'POST',
|
|
cache: 'no-store'
|
|
})
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setStatus(data)
|
|
setLastUpdated(new Date().toLocaleTimeString())
|
|
} else {
|
|
setStatus({
|
|
success: false,
|
|
error: 'Failed to fetch status',
|
|
timestamp: new Date().toISOString(),
|
|
recentInvoices: 0,
|
|
environment: process.env.NODE_ENV || 'development',
|
|
apiVersion: 'unknown'
|
|
})
|
|
}
|
|
} catch (error) {
|
|
setStatus({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Network error',
|
|
timestamp: new Date().toISOString(),
|
|
recentInvoices: 0,
|
|
environment: process.env.NODE_ENV || 'development',
|
|
apiVersion: 'unknown'
|
|
})
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchStatus()
|
|
const interval = setInterval(fetchStatus, 30000) // Refresh every 30 seconds
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-4 flex items-center justify-center">
|
|
<RefreshCw className="h-4 w-4 animate-spin mr-2" />
|
|
<span className="text-sm">Checking Stripe connection...</span>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
if (!status) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 text-red-600">
|
|
<XCircle className="h-4 w-4" />
|
|
<span className="text-sm">Unable to connect to Stripe</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const stripeWorking = status.success
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Status Overview */}
|
|
<Card className={stripeWorking ? 'border-green-200' : 'border-red-200'}>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{stripeWorking ? (
|
|
<CheckCircle className="h-5 w-5 text-green-600" />
|
|
) : (
|
|
<XCircle className="h-5 w-5 text-red-600" />
|
|
)}
|
|
<CardTitle className="text-lg">
|
|
Stripe {stripeWorking ? 'Connected' : 'Not Connected'}
|
|
</CardTitle>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={fetchStatus}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<RefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
<CardDescription className="text-xs">
|
|
Last updated: {lastUpdated}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{stripeWorking ? (
|
|
<p className="text-green-700">
|
|
Your Stripe account is connected and ready for products.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<p className="text-red-700">
|
|
{status.error || 'Unable to connect to Stripe. Please check your configuration.'}
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline" className="text-yellow-600">
|
|
<AlertTriangle className="h-3 w-3 mr-1" />
|
|
Setup Required
|
|
</Badge>
|
|
<Button size="sm" variant="outline" asChild>
|
|
<a href="https://dashboard.stripe.com/login">
|
|
<ExternalLink className="h-3 w-3 mr-1" />
|
|
Go to Stripe
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Account Info */}
|
|
{stripeWorking && status.account && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<CreditCard className="h-4 w-4" />
|
|
Account Information
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">Account ID:</span>
|
|
<div className="font-medium">{status.account.id}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">Business Name:</span>
|
|
<div className="font-medium">{status.account.name || 'Not set'}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">Country:</span>
|
|
<div className="font-medium">{status.account.country}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">Charges Enabled:</span>
|
|
<div>
|
|
{status.account.charges_enabled ? (
|
|
<Badge variant="default">Yes</Badge>
|
|
) : (
|
|
<Badge variant="destructive">No</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Products Status */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Package className="h-4 w-4" />
|
|
Products Status
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
{stripeWorking && status.products ? (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="default">
|
|
{status.products.total} total products
|
|
</Badge>
|
|
<Badge variant="outline">
|
|
{status.products.active} active
|
|
</Badge>
|
|
</div>
|
|
|
|
{status.products.sample.length > 0 && (
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-muted-foreground">Recent products:</p>
|
|
{status.products.sample.map((product) => (
|
|
<div key={product.id} className="flex items-center justify-between text-sm p-2 bg-muted rounded">
|
|
<div>
|
|
<div className="font-medium">{product.name}</div>
|
|
{product.price && (
|
|
<div className="text-xs text-muted-foreground">
|
|
${(product.price.unit_amount / 100).toFixed(2)} {product.price.currency.toUpperCase()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{product.price ? (
|
|
<Badge variant="outline">Available</Badge>
|
|
) : (
|
|
<Badge variant="destructive">No Price</Badge>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{status.products.total === 0 && (
|
|
<div className="text-center py-4">
|
|
<Package className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
|
<p className="text-sm text-muted-foreground">
|
|
No products found. Add some products in Stripe Dashboard.
|
|
</p>
|
|
<Button size="sm" className="mt-2" asChild>
|
|
<a href="https://dashboard.stripe.com/products/new">
|
|
<ExternalLink className="h-3 w-3 mr-1" />
|
|
Add Products
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : !stripeWorking ? (
|
|
<div className="text-center py-4">
|
|
<AlertTriangle className="h-8 w-8 text-yellow-600 mx-auto mb-2" />
|
|
<p className="text-sm text-muted-foreground">
|
|
Connect to Stripe to view product status
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-4">
|
|
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground mx-auto mb-2" />
|
|
<p className="text-sm text-muted-foreground">
|
|
Loading products...
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Payment Methods */}
|
|
{stripeWorking && status.paymentMethods && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<CreditCard className="h-4 w-4" />
|
|
Payment Methods
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="default">
|
|
{status.paymentMethods.total} methods available
|
|
</Badge>
|
|
<div className="flex gap-1">
|
|
{status.paymentMethods.types.map((type) => (
|
|
<Badge key={type} variant="outline" className="text-xs">
|
|
{type}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Quick Actions */}
|
|
{stripeWorking && (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base">Quick Actions</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
<div className="flex gap-2 flex-wrap">
|
|
<Button size="sm" variant="outline" asChild>
|
|
<a href="https://dashboard.stripe.com/products/new">
|
|
<Package className="h-3 w-3 mr-1" />
|
|
Add Products
|
|
</a>
|
|
</Button>
|
|
<Button size="sm" variant="outline" asChild>
|
|
<a href="/products">
|
|
<ShoppingCart className="h-3 w-3 mr-1" />
|
|
View Store
|
|
</a>
|
|
</Button>
|
|
<Button size="sm" variant="outline" asChild>
|
|
<a href="/admin">
|
|
<CreditCard className="h-3 w-3 mr-1" />
|
|
Admin Panel
|
|
</a>
|
|
</Button>
|
|
<Button size="sm" variant="outline" asChild>
|
|
<a href="/stripe-setup">
|
|
<RefreshCw className="h-3 w-3 mr-1" />
|
|
Setup Guide
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|