add column for email expiry date
Members > Invite member says invite Staff in dialog resend email button Update form member form to say member and not staff review application function manual payment functionality basic implementation of theme actions dropdown
This commit is contained in:
576
src/components/CreateSubscriptionDialog.js
Normal file
576
src/components/CreateSubscriptionDialog.js
Normal file
@@ -0,0 +1,576 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import api from '../utils/api';
|
||||
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 { Card } from './ui/card';
|
||||
import { toast } from 'sonner';
|
||||
import { Loader2, Repeat, Search, Calendar, Heart, X, User } from 'lucide-react';
|
||||
|
||||
const CreateSubscriptionDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
// Search state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [allUsers, setAllUsers] = useState([]);
|
||||
|
||||
// Plan state
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||
const [useCustomPeriod, setUseCustomPeriod] = useState(false);
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
plan_id: '',
|
||||
amount: '',
|
||||
payment_date: new Date().toISOString().split('T')[0],
|
||||
payment_method: 'cash',
|
||||
custom_period_start: new Date().toISOString().split('T')[0],
|
||||
custom_period_end: '',
|
||||
notes: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Fetch users and plans when dialog opens
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!open) return;
|
||||
|
||||
try {
|
||||
const [usersResponse, plansResponse] = await Promise.all([
|
||||
api.get('/admin/users'),
|
||||
api.get('/admin/subscriptions/plans')
|
||||
]);
|
||||
setAllUsers(usersResponse.data);
|
||||
setPlans(plansResponse.data.filter(p => p.active));
|
||||
} catch (error) {
|
||||
toast.error('Failed to load data');
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [open]);
|
||||
|
||||
// Filter users based on search query
|
||||
useEffect(() => {
|
||||
if (!searchQuery.trim()) {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchLoading(true);
|
||||
const query = searchQuery.toLowerCase();
|
||||
const filtered = allUsers.filter(user =>
|
||||
user.first_name?.toLowerCase().includes(query) ||
|
||||
user.last_name?.toLowerCase().includes(query) ||
|
||||
user.email?.toLowerCase().includes(query)
|
||||
).slice(0, 10); // Limit to 10 results
|
||||
|
||||
setSearchResults(filtered);
|
||||
setSearchLoading(false);
|
||||
}, [searchQuery, allUsers]);
|
||||
|
||||
// Update amount when plan changes
|
||||
useEffect(() => {
|
||||
if (selectedPlan && !formData.amount) {
|
||||
const suggestedAmount = (selectedPlan.suggested_price_cents || selectedPlan.minimum_price_cents || selectedPlan.price_cents) / 100;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
amount: suggestedAmount.toFixed(2)
|
||||
}));
|
||||
}
|
||||
}, [selectedPlan]);
|
||||
|
||||
// Calculate donation breakdown
|
||||
const getAmountBreakdown = () => {
|
||||
if (!selectedPlan || !formData.amount) return null;
|
||||
|
||||
const totalCents = Math.round(parseFloat(formData.amount) * 100);
|
||||
const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
|
||||
const donationCents = Math.max(0, totalCents - minimumCents);
|
||||
|
||||
return {
|
||||
total: totalCents,
|
||||
base: minimumCents,
|
||||
donation: donationCents
|
||||
};
|
||||
};
|
||||
|
||||
const formatPrice = (cents) => {
|
||||
return `$${(cents / 100).toFixed(2)}`;
|
||||
};
|
||||
|
||||
const breakdown = getAmountBreakdown();
|
||||
|
||||
const handleSelectUser = (user) => {
|
||||
setSelectedUser(user);
|
||||
setSearchQuery('');
|
||||
setSearchResults([]);
|
||||
};
|
||||
|
||||
const handleClearUser = () => {
|
||||
setSelectedUser(null);
|
||||
setFormData({
|
||||
plan_id: '',
|
||||
amount: '',
|
||||
payment_date: new Date().toISOString().split('T')[0],
|
||||
payment_method: 'cash',
|
||||
custom_period_start: new Date().toISOString().split('T')[0],
|
||||
custom_period_end: '',
|
||||
notes: ''
|
||||
});
|
||||
setSelectedPlan(null);
|
||||
setUseCustomPeriod(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!selectedUser) {
|
||||
toast.error('Please select a user');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.plan_id) {
|
||||
toast.error('Please select a subscription plan');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.amount || parseFloat(formData.amount) <= 0) {
|
||||
toast.error('Please enter a valid payment amount');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate minimum amount
|
||||
const amountCents = Math.round(parseFloat(formData.amount) * 100);
|
||||
const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
|
||||
if (amountCents < minimumCents) {
|
||||
toast.error(`Amount must be at least ${formatPrice(minimumCents)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (useCustomPeriod && (!formData.custom_period_start || !formData.custom_period_end)) {
|
||||
toast.error('Please specify both start and end dates for custom period');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
plan_id: formData.plan_id,
|
||||
amount_cents: amountCents,
|
||||
payment_date: new Date(formData.payment_date).toISOString(),
|
||||
payment_method: formData.payment_method,
|
||||
override_plan_dates: useCustomPeriod,
|
||||
notes: formData.notes || null
|
||||
};
|
||||
|
||||
if (useCustomPeriod) {
|
||||
payload.custom_period_start = new Date(formData.custom_period_start).toISOString();
|
||||
payload.custom_period_end = new Date(formData.custom_period_end).toISOString();
|
||||
}
|
||||
|
||||
await api.post(`/admin/users/${selectedUser.id}/activate-payment`, payload);
|
||||
toast.success(`Subscription created for ${selectedUser.first_name} ${selectedUser.last_name}!`);
|
||||
|
||||
// Reset form
|
||||
handleClearUser();
|
||||
onOpenChange(false);
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || 'Failed to create subscription';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
handleClearUser();
|
||||
setSearchQuery('');
|
||||
setSearchResults([]);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[700px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Repeat className="h-6 w-6" />
|
||||
Create Subscription
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Search for an existing member and create a subscription with manual payment processing.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-6 py-4">
|
||||
{/* User Search Section */}
|
||||
{!selectedUser ? (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Search Member
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple" />
|
||||
<Input
|
||||
placeholder="Search by name or email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
/>
|
||||
{searchLoading && (
|
||||
<Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 animate-spin text-brand-purple" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Results */}
|
||||
{searchResults.length > 0 && (
|
||||
<Card className="border-2 border-[var(--neutral-800)] rounded-xl overflow-hidden">
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
{searchResults.map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
type="button"
|
||||
onClick={() => handleSelectUser(user)}
|
||||
className="w-full p-3 text-left hover:bg-[var(--lavender-400)] transition-colors border-b border-[var(--neutral-800)] last:border-b-0"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
|
||||
<User className="h-5 w-5 text-brand-purple" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{searchQuery && !searchLoading && searchResults.length === 0 && (
|
||||
<p className="text-sm text-brand-purple text-center py-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
No members found matching "{searchQuery}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* Selected User Card */
|
||||
<Card className="p-4 bg-[var(--lavender-400)] border-2 border-[var(--neutral-800)] rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-12 w-12 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
|
||||
<User className="h-6 w-6 text-brand-purple" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedUser.first_name} {selectedUser.last_name}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedUser.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearUser}
|
||||
className="text-brand-purple hover:bg-[var(--neutral-800)]/20"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Payment Form - Only show when user is selected */}
|
||||
{selectedUser && (
|
||||
<>
|
||||
{/* Plan Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="plan_id" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Subscription Plan
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.plan_id}
|
||||
onValueChange={(value) => {
|
||||
const plan = plans.find(p => p.id === value);
|
||||
setSelectedPlan(plan);
|
||||
const suggestedAmount = plan ? (plan.suggested_price_cents || plan.minimum_price_cents || plan.price_cents) / 100 : '';
|
||||
setFormData({
|
||||
...formData,
|
||||
plan_id: value,
|
||||
amount: suggestedAmount ? suggestedAmount.toFixed(2) : ''
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectValue placeholder="Select subscription plan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{plans.map(plan => {
|
||||
const minPrice = (plan.minimum_price_cents || plan.price_cents) / 100;
|
||||
const sugPrice = plan.suggested_price_cents ? (plan.suggested_price_cents / 100) : null;
|
||||
return (
|
||||
<SelectItem key={plan.id} value={plan.id}>
|
||||
{plan.name} - ${minPrice.toFixed(2)}{sugPrice && sugPrice > minPrice ? ` (Suggested: $${sugPrice.toFixed(2)})` : ''}/{plan.billing_cycle}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedPlan && (
|
||||
<p className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedPlan.description || `${selectedPlan.billing_cycle} subscription`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payment Amount */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="amount" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Payment Amount ($)
|
||||
</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="Enter amount"
|
||||
value={formData.amount}
|
||||
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
required
|
||||
/>
|
||||
{selectedPlan && (
|
||||
<p className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Amount Breakdown */}
|
||||
{breakdown && breakdown.total >= breakdown.base && (
|
||||
<Card className="p-4 bg-[var(--lavender-400)] border border-[var(--neutral-800)]">
|
||||
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex justify-between text-[var(--purple-ink)]">
|
||||
<span>Membership Fee:</span>
|
||||
<span className="font-semibold">{formatPrice(breakdown.base)}</span>
|
||||
</div>
|
||||
{breakdown.donation > 0 && (
|
||||
<div className="flex justify-between text-[var(--orange-light)]">
|
||||
<span className="flex items-center gap-1">
|
||||
<Heart className="h-4 w-4" />
|
||||
Additional Donation:
|
||||
</span>
|
||||
<span className="font-semibold">{formatPrice(breakdown.donation)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-[var(--purple-ink)] font-bold text-base pt-2 border-t border-[var(--neutral-800)]">
|
||||
<span>Total:</span>
|
||||
<span>{formatPrice(breakdown.total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Payment Date */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment_date" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Payment Date
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple" />
|
||||
<Input
|
||||
id="payment_date"
|
||||
type="date"
|
||||
value={formData.payment_date}
|
||||
onChange={(e) => setFormData({ ...formData, payment_date: e.target.value })}
|
||||
className="pl-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment_method" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Payment Method
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.payment_method}
|
||||
onValueChange={(value) => setFormData({ ...formData, payment_method: value })}
|
||||
>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectValue placeholder="Select payment method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cash">Cash</SelectItem>
|
||||
<SelectItem value="bank_transfer">Bank Transfer</SelectItem>
|
||||
<SelectItem value="check">Check</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Subscription Period */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Subscription Period
|
||||
</Label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="use_custom_period"
|
||||
checked={useCustomPeriod}
|
||||
onChange={(e) => setUseCustomPeriod(e.target.checked)}
|
||||
className="rounded border-[var(--neutral-800)]"
|
||||
/>
|
||||
<Label htmlFor="use_custom_period" className="text-sm text-brand-purple font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Use custom dates instead of plan's billing cycle
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{useCustomPeriod ? (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom_period_start" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Start Date
|
||||
</Label>
|
||||
<Input
|
||||
id="custom_period_start"
|
||||
type="date"
|
||||
value={formData.custom_period_start}
|
||||
onChange={(e) => setFormData({ ...formData, custom_period_start: e.target.value })}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
required={useCustomPeriod}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom_period_end" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
End Date
|
||||
</Label>
|
||||
<Input
|
||||
id="custom_period_end"
|
||||
type="date"
|
||||
value={formData.custom_period_end}
|
||||
onChange={(e) => setFormData({ ...formData, custom_period_end: e.target.value })}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
required={useCustomPeriod}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
selectedPlan && (
|
||||
<div className="text-sm text-brand-purple bg-[var(--lavender-300)] p-3 rounded-lg space-y-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedPlan.custom_cycle_enabled ? (
|
||||
<>
|
||||
<p>
|
||||
<span className="font-medium text-[var(--purple-ink)]">Plan uses custom billing cycle:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const startMonth = months[(selectedPlan.custom_cycle_start_month || 1) - 1];
|
||||
const endMonth = months[(selectedPlan.custom_cycle_end_month || 12) - 1];
|
||||
return `${startMonth} ${selectedPlan.custom_cycle_start_day} - ${endMonth} ${selectedPlan.custom_cycle_end_day} (recurring annually)`;
|
||||
})()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Subscription will end on the upcoming cycle end date based on today's date.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Will use plan's billing cycle: <span className="font-medium">{selectedPlan.billing_cycle}</span>
|
||||
<br />
|
||||
Starts today, ends {selectedPlan.billing_cycle === 'monthly' ? '30 days' :
|
||||
selectedPlan.billing_cycle === 'quarterly' ? '90 days' :
|
||||
selectedPlan.billing_cycle === 'yearly' ? '1 year' :
|
||||
selectedPlan.billing_cycle === 'lifetime' ? 'lifetime' : '1 year'} from now
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="notes" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Notes (Optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
placeholder="Additional notes about the payment..."
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
className="rounded-xl"
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
disabled={loading || !selectedUser}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Repeat className="h-4 w-4 mr-2" />
|
||||
Create Subscription
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateSubscriptionDialog;
|
||||
Reference in New Issue
Block a user