Initial Commit
This commit is contained in:
233
src/components/PlanDialog.js
Normal file
233
src/components/PlanDialog.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from './ui/dialog';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from './ui/select';
|
||||
import { toast } from 'sonner';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import api from '../utils/api';
|
||||
|
||||
const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
price_cents: '',
|
||||
billing_cycle: 'yearly',
|
||||
stripe_price_id: '',
|
||||
active: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (plan) {
|
||||
setFormData({
|
||||
name: plan.name,
|
||||
description: plan.description || '',
|
||||
price_cents: plan.price_cents,
|
||||
billing_cycle: plan.billing_cycle,
|
||||
stripe_price_id: plan.stripe_price_id || '',
|
||||
active: plan.active
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
price_cents: '',
|
||||
billing_cycle: 'yearly',
|
||||
stripe_price_id: '',
|
||||
active: true
|
||||
});
|
||||
}
|
||||
}, [plan, open]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const endpoint = plan
|
||||
? `/admin/subscriptions/plans/${plan.id}`
|
||||
: '/admin/subscriptions/plans';
|
||||
|
||||
const method = plan ? 'put' : 'post';
|
||||
|
||||
await api[method](endpoint, {
|
||||
...formData,
|
||||
price_cents: parseInt(formData.price_cents)
|
||||
});
|
||||
|
||||
toast.success(plan ? 'Plan updated successfully' : 'Plan created successfully');
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to save plan');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatPriceForDisplay = (cents) => {
|
||||
return (cents / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const handlePriceChange = (e) => {
|
||||
const dollars = parseFloat(e.target.value) || 0;
|
||||
setFormData({ ...formData, price_cents: Math.round(dollars * 100) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="fraunces text-2xl">
|
||||
{plan ? 'Edit Plan' : 'Create New Plan'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{plan ? 'Update plan details below' : 'Enter plan details to create a new subscription plan'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<Label htmlFor="name">Plan Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="e.g., Annual Membership"
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Brief description of the plan benefits"
|
||||
rows={3}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Price & Billing Cycle - Side by side */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="price">Price (USD) *</Label>
|
||||
<Input
|
||||
id="price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={formatPriceForDisplay(formData.price_cents)}
|
||||
onChange={handlePriceChange}
|
||||
placeholder="50.00"
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="billing_cycle">Billing Cycle *</Label>
|
||||
<Select
|
||||
value={formData.billing_cycle}
|
||||
onValueChange={(value) => setFormData({ ...formData, billing_cycle: value })}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="quarterly">Quarterly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
<SelectItem value="lifetime">Lifetime</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stripe Price ID */}
|
||||
<div>
|
||||
<Label htmlFor="stripe_price_id">Stripe Price ID</Label>
|
||||
<Input
|
||||
id="stripe_price_id"
|
||||
value={formData.stripe_price_id}
|
||||
onChange={(e) => setFormData({ ...formData, stripe_price_id: e.target.value })}
|
||||
placeholder="price_xxxxxxxxxxxxx"
|
||||
className="mt-2 font-mono text-sm"
|
||||
/>
|
||||
<p className="text-sm text-[#6B708D] mt-1">
|
||||
Optional. Leave empty for manual/test plans.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Active Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor="active">Active Status</Label>
|
||||
<p className="text-sm text-[#6B708D]">
|
||||
Inactive plans won't appear for new subscriptions
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="active"
|
||||
checked={formData.active}
|
||||
onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#E07A5F]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#81B29A]"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[#E07A5F] hover:bg-[#D0694E]"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
plan ? 'Update Plan' : 'Create Plan'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanDialog;
|
||||
Reference in New Issue
Block a user