fix(admin): habilitar edicao de limites dos planos, auto-seed dos planos padrao e corrigir botao de adicionar recurso
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m42s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m42s
Details
This commit is contained in:
parent
cf4a838d9c
commit
c4cb13a912
|
|
@ -3,15 +3,48 @@ import { query } from '@/lib/db';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const res = await query(`
|
let res = await query(`
|
||||||
SELECT p.id, p.name, LOWER(p.name) as slug, p.price as "priceMonthly",
|
SELECT p.id, p.name, COALESCE(p.slug, LOWER(p.name)) as slug,
|
||||||
(p.price * 10) as "priceYearly",
|
COALESCE(p.price, p.price_monthly, 0) as "priceMonthly",
|
||||||
|
COALESCE(p.price_yearly, p.price * 10, 0) as "priceYearly",
|
||||||
|
COALESCE(p.max_professionals, 1) as "maxProfessionals",
|
||||||
|
COALESCE(p.max_services, 10) as "maxServices",
|
||||||
p.features, p.is_active as active,
|
p.features, p.is_active as active,
|
||||||
(SELECT COUNT(*) FROM tenants WHERE active_plan_id = p.id) as subscribers
|
(SELECT COUNT(*) FROM tenants WHERE active_plan_id = p.id) as subscribers
|
||||||
FROM plans p
|
FROM plans p
|
||||||
ORDER BY p.price ASC
|
ORDER BY p.price ASC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Auto-seed plans if empty
|
||||||
|
if (res.rows.length === 0) {
|
||||||
|
const defaultPlans = [
|
||||||
|
{ name: 'Starter', price: 49.90, priceYearly: 499.00, slug: 'starter', maxProf: 1, maxSvc: 10, features: ['1 profissional', '10 serviços', '200 agendamentos/mês', 'Agendamento online', 'Página de reservas', 'Lembretes por e-mail', 'Relatórios básicos'] },
|
||||||
|
{ name: 'Professional', price: 99.90, priceYearly: 999.00, slug: 'professional', maxProf: 5, maxSvc: 30, features: ['5 profissionais', '30 serviços', 'Agendamentos ilimitados', 'Tudo do Starter', 'Múltiplos profissionais', 'Comissionamento', 'Relatórios avançados', 'Personalização da marca'] },
|
||||||
|
{ name: 'Business', price: 199.90, priceYearly: 1990.00, slug: 'business', maxProf: 15, maxSvc: 100, features: ['15 profissionais', '100 serviços', 'Agendamentos ilimitados', 'Tudo do Professional', 'API de integração', 'Suporte prioritário', 'Multi-unidades', 'Automações avançadas'] },
|
||||||
|
{ name: 'Enterprise', price: 399.90, priceYearly: 3990.00, slug: 'enterprise', maxProf: 999, maxSvc: 999, features: ['Profissionais ilimitados', 'Serviços ilimitados', 'Agendamentos ilimitados', 'Tudo do Business', 'Gerente dedicado', 'SLA garantido', 'Integrações customizadas', 'White-label'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of defaultPlans) {
|
||||||
|
await query(
|
||||||
|
`INSERT INTO plans (name, price, price_monthly, price_yearly, slug, max_professionals, max_services, features)
|
||||||
|
VALUES ($1, $2, $2, $3, $4, $5, $6, $7)`,
|
||||||
|
[p.name, p.price, p.priceYearly, p.slug, p.maxProf, p.maxSvc, JSON.stringify(p.features)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res = await query(`
|
||||||
|
SELECT p.id, p.name, COALESCE(p.slug, LOWER(p.name)) as slug,
|
||||||
|
COALESCE(p.price, p.price_monthly, 0) as "priceMonthly",
|
||||||
|
COALESCE(p.price_yearly, p.price * 10, 0) as "priceYearly",
|
||||||
|
COALESCE(p.max_professionals, 1) as "maxProfessionals",
|
||||||
|
COALESCE(p.max_services, 10) as "maxServices",
|
||||||
|
p.features, p.is_active as active,
|
||||||
|
(SELECT COUNT(*) FROM tenants WHERE active_plan_id = p.id) as subscribers
|
||||||
|
FROM plans p
|
||||||
|
ORDER BY p.price ASC
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
const plans = res.rows.map(row => {
|
const plans = res.rows.map(row => {
|
||||||
let features = [];
|
let features = [];
|
||||||
try {
|
try {
|
||||||
|
|
@ -21,11 +54,11 @@ export async function GET() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
priceMonthly: Number(row.priceMonthly),
|
priceMonthly: Number(row.priceMonthly || 0),
|
||||||
priceYearly: Number(row.priceYearly),
|
priceYearly: Number(row.priceYearly || 0),
|
||||||
subscribers: Number(row.subscribers),
|
maxProfessionals: Number(row.maxProfessionals || 1),
|
||||||
maxProfessionals: row.name === 'Enterprise' ? 999 : (row.name === 'Business' ? 15 : (row.name === 'Professional' ? 5 : 1)),
|
maxServices: Number(row.maxServices || 10),
|
||||||
maxServices: row.name === 'Enterprise' ? 999 : (row.name === 'Business' ? 100 : (row.name === 'Professional' ? 30 : 10)),
|
subscribers: Number(row.subscribers || 0),
|
||||||
features: features.length > 0 ? features : ['Agendamento online']
|
features: features.length > 0 ? features : ['Agendamento online']
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -39,13 +72,19 @@ export async function GET() {
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { name, priceMonthly, features } = await req.json();
|
const { name, slug, priceMonthly, priceYearly, maxProfessionals, maxServices, features } = await req.json();
|
||||||
if (!name) return NextResponse.json({ error: 'Missing name' }, { status: 400 });
|
if (!name) return NextResponse.json({ error: 'Missing name' }, { status: 400 });
|
||||||
|
|
||||||
|
const safeSlug = slug || name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||||
|
const pMonthly = priceMonthly || 0;
|
||||||
|
const pYearly = priceYearly || pMonthly * 10;
|
||||||
|
const maxProf = maxProfessionals || 1;
|
||||||
|
const maxSvc = maxServices || 10;
|
||||||
|
|
||||||
const res = await query(
|
const res = await query(
|
||||||
`INSERT INTO plans (name, price, features)
|
`INSERT INTO plans (name, slug, price, price_monthly, price_yearly, max_professionals, max_services, features)
|
||||||
VALUES ($1, $2, $3) RETURNING *`,
|
VALUES ($1, $2, $3, $3, $4, $5, $6, $7) RETURNING *`,
|
||||||
[name, priceMonthly || 0, JSON.stringify(features || [])]
|
[name, safeSlug, pMonthly, pYearly, maxProf, maxSvc, JSON.stringify(features || [])]
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json(res.rows[0]);
|
return NextResponse.json(res.rows[0]);
|
||||||
|
|
@ -56,14 +95,19 @@ export async function POST(req: NextRequest) {
|
||||||
|
|
||||||
export async function PUT(req: NextRequest) {
|
export async function PUT(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { id, name, priceMonthly, features } = await req.json();
|
const { id, name, slug, priceMonthly, priceYearly, maxProfessionals, maxServices, features } = await req.json();
|
||||||
if (!id || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
if (!id || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
|
|
||||||
|
const pMonthly = priceMonthly || 0;
|
||||||
|
const pYearly = priceYearly || pMonthly * 10;
|
||||||
|
const maxProf = maxProfessionals || 1;
|
||||||
|
const maxSvc = maxServices || 10;
|
||||||
|
|
||||||
const res = await query(
|
const res = await query(
|
||||||
`UPDATE plans
|
`UPDATE plans
|
||||||
SET name = $1, price = $2, features = $3, updated_at = NOW()
|
SET name = $1, price = $2, price_monthly = $2, price_yearly = $3, max_professionals = $4, max_services = $5, features = $6, updated_at = NOW()
|
||||||
WHERE id = $4 RETURNING *`,
|
WHERE id = $7 RETURNING *`,
|
||||||
[name, priceMonthly || 0, JSON.stringify(features || []), id]
|
[name, pMonthly, pYearly, maxProf, maxSvc, JSON.stringify(features || []), id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json(res.rows[0]);
|
return NextResponse.json(res.rows[0]);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,29 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, X, Edit, Trash2, Check, Users, AlertTriangle } from 'lucide-react';
|
import { Plus, X, Edit, Trash2, Check, Users, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
interface Plan { id: string; name: string; slug: string; priceMonthly: number; priceYearly: number; maxProfessionals: number; maxServices: number; features: string[]; active: boolean; subscribers: number; }
|
interface Plan {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
priceMonthly: number;
|
||||||
|
priceYearly: number;
|
||||||
|
maxProfessionals: number;
|
||||||
|
maxServices: number;
|
||||||
|
features: string[];
|
||||||
|
active: boolean;
|
||||||
|
subscribers: number;
|
||||||
|
}
|
||||||
|
|
||||||
const emptyPlan: Partial<Plan> = { name: '', slug: '', priceMonthly: 0, priceYearly: 0, maxProfessionals: 1, maxServices: 10, features: [], active: true };
|
const emptyPlan: Partial<Plan> = {
|
||||||
|
name: '',
|
||||||
|
slug: '',
|
||||||
|
priceMonthly: 0,
|
||||||
|
priceYearly: 0,
|
||||||
|
maxProfessionals: 1,
|
||||||
|
maxServices: 10,
|
||||||
|
features: [],
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
|
||||||
export default function Plans() {
|
export default function Plans() {
|
||||||
const [plans, setPlans] = useState<Plan[]>([]);
|
const [plans, setPlans] = useState<Plan[]>([]);
|
||||||
|
|
@ -33,8 +53,17 @@ export default function Plans() {
|
||||||
fetchPlans();
|
fetchPlans();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openAdd = () => { setEditing(null); setForm({ ...emptyPlan, features: [] }); setShowModal(true); };
|
const openAdd = () => {
|
||||||
const openEdit = (p: Plan) => { setEditing(p); setForm({ ...p }); setShowModal(true); };
|
setEditing(null);
|
||||||
|
setForm({ ...emptyPlan, features: [] });
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (p: Plan) => {
|
||||||
|
setEditing(p);
|
||||||
|
setForm({ ...p, features: p.features || [] });
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!form.name?.trim()) return;
|
if (!form.name?.trim()) return;
|
||||||
|
|
@ -64,8 +93,16 @@ export default function Plans() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addFeature = () => { if (featureInput.trim()) { setForm(p => ({ ...p, features: [...(p.features || []), featureInput.trim()] })); setFeatureInput(''); } };
|
const addFeature = () => {
|
||||||
const removeFeature = (idx: number) => { setForm(p => ({ ...p, features: (p.features || []).filter((_, i) => i !== idx) })); };
|
if (featureInput.trim()) {
|
||||||
|
setForm(p => ({ ...p, features: [...(p.features || []), featureInput.trim()] }));
|
||||||
|
setFeatureInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFeature = (idx: number) => {
|
||||||
|
setForm(p => ({ ...p, features: (p.features || []).filter((_, i) => i !== idx) }));
|
||||||
|
};
|
||||||
|
|
||||||
const colors = ['#3b82f6', '#6C63FF', '#a855f7', '#f59e0b'];
|
const colors = ['#3b82f6', '#6C63FF', '#a855f7', '#f59e0b'];
|
||||||
|
|
||||||
|
|
@ -86,7 +123,10 @@ export default function Plans() {
|
||||||
<AlertTriangle size={32} color="var(--warning)" />
|
<AlertTriangle size={32} color="var(--warning)" />
|
||||||
<p style={{ fontWeight: 600 }}>Excluir plano {p.name}?</p>
|
<p style={{ fontWeight: 600 }}>Excluir plano {p.name}?</p>
|
||||||
<p style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{p.subscribers} assinantes serão afetados</p>
|
<p style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{p.subscribers} assinantes serão afetados</p>
|
||||||
<div style={{ display: 'flex', gap: 8 }}><button className="btn btn-sm btn-danger" onClick={() => handleDelete(p.id)}>Excluir</button><button className="btn btn-sm btn-secondary" onClick={() => setDeleteConfirm(null)}>Cancelar</button></div>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button className="btn btn-sm btn-danger" onClick={() => handleDelete(p.id)}>Excluir</button>
|
||||||
|
<button className="btn btn-sm btn-secondary" onClick={() => setDeleteConfirm(null)}>Cancelar</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-between mb-4">
|
<div className="flex-between mb-4">
|
||||||
|
|
@ -97,22 +137,22 @@ export default function Plans() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<span style={{ fontSize: '32px', fontWeight: 900 }}>R$ {p.priceMonthly.toFixed(2).replace('.', ',')}</span>
|
<span style={{ fontSize: '32px', fontWeight: 900 }}>R$ {Number(p.priceMonthly || 0).toFixed(2).replace('.', ',')}</span>
|
||||||
<span style={{ fontSize: '14px', color: 'var(--text-muted)' }}>/mês</span>
|
<span style={{ fontSize: '14px', color: 'var(--text-muted)' }}>/mês</span>
|
||||||
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: 2 }}>ou R$ {p.priceYearly.toFixed(2).replace('.', ',')} /ano</div>
|
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: 2 }}>ou R$ {Number(p.priceYearly || 0).toFixed(2).replace('.', ',')} /ano</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2 mb-4">
|
<div className="grid-2 mb-4">
|
||||||
<div style={{ padding: '10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }}>
|
<div style={{ padding: '10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: '16px', fontWeight: 800, color: colors[idx % colors.length] }}>{p.maxProfessionals === 999 ? '∞' : p.maxProfessionals}</div>
|
<div style={{ fontSize: '16px', fontWeight: 800, color: colors[idx % colors.length] }}>{p.maxProfessionals >= 999 ? '∞' : p.maxProfessionals}</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Profissionais</div>
|
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Profissionais</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }}>
|
<div style={{ padding: '10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: '16px', fontWeight: 800, color: colors[idx % colors.length] }}><Users size={14} style={{ display: 'inline' }} /> {p.subscribers}</div>
|
<div style={{ fontSize: '16px', fontWeight: 800, color: colors[idx % colors.length] }}>{p.maxServices >= 999 ? '∞' : p.maxServices}</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Assinantes</div>
|
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Serviços</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul style={{ listStyle: 'none', fontSize: '13px' }}>
|
<ul style={{ listStyle: 'none', fontSize: '13px' }}>
|
||||||
{p.features.map((f, i) => (
|
{(p.features || []).map((f, i) => (
|
||||||
<li key={i} style={{ padding: '4px 0', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
<li key={i} style={{ padding: '4px 0', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<Check size={14} color="var(--success)" /> {f}
|
<Check size={14} color="var(--success)" /> {f}
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -137,28 +177,61 @@ export default function Plans() {
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div className="form-group"><label className="form-label">Nome *</label><input className="form-input" value={form.name || ''} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} /></div>
|
<div className="form-group">
|
||||||
<div className="form-group"><label className="form-label">Slug</label><input className="form-input" value={form.slug || ''} onChange={e => setForm(p => ({ ...p, slug: e.target.value }))} disabled={!!editing} /></div>
|
<label className="form-label">Nome *</label>
|
||||||
|
<input className="form-input" value={form.name || ''} onChange={e => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setForm(p => ({
|
||||||
|
...p,
|
||||||
|
name: val,
|
||||||
|
slug: editing ? p.slug : val.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||||
|
}));
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Slug</label>
|
||||||
|
<input className="form-input" value={form.slug || ''} onChange={e => setForm(p => ({ ...p, slug: e.target.value }))} disabled={!!editing} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div className="form-group"><label className="form-label">Preço Mensal (R$)</label><input className="form-input" type="number" step="0.01" value={form.priceMonthly || 0} onChange={e => setForm(p => ({ ...p, priceMonthly: Number(e.target.value) }))} /></div>
|
<div className="form-group">
|
||||||
<div className="form-group"><label className="form-label">Preço Anual (R$)</label><input className="form-input" type="number" step="0.01" value={form.priceYearly || 0} onChange={e => setForm(p => ({ ...p, priceYearly: Number(e.target.value) }))} disabled /></div>
|
<label className="form-label">Preço Mensal (R$)</label>
|
||||||
|
<input className="form-input" type="number" step="0.01" value={form.priceMonthly || 0} onChange={e => {
|
||||||
|
const val = Number(e.target.value);
|
||||||
|
setForm(p => ({ ...p, priceMonthly: val, priceYearly: p.priceYearly || val * 10 }));
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Preço Anual (R$)</label>
|
||||||
|
<input className="form-input" type="number" step="0.01" value={form.priceYearly || 0} onChange={e => setForm(p => ({ ...p, priceYearly: Number(e.target.value) }))} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div className="form-group"><label className="form-label">Max Profissionais</label><input className="form-input" type="number" value={form.maxProfessionals || 1} onChange={e => setForm(p => ({ ...p, maxProfessionals: Number(e.target.value) }))} disabled /></div>
|
<div className="form-group">
|
||||||
<div className="form-group"><label className="form-label">Max Serviços</label><input className="form-input" type="number" value={form.maxServices || 10} onChange={e => setForm(p => ({ ...p, maxServices: Number(e.target.value) }))} disabled /></div>
|
<label className="form-label">Max Profissionais</label>
|
||||||
|
<input className="form-input" type="number" value={form.maxProfessionals || 1} onChange={e => setForm(p => ({ ...p, maxProfessionals: Number(e.target.value) }))} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Max Serviços</label>
|
||||||
|
<input className="form-input" type="number" value={form.maxServices || 10} onChange={e => setForm(p => ({ ...p, maxServices: Number(e.target.value) }))} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Recursos</label>
|
<label className="form-label">Recursos</label>
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
||||||
<input className="form-input" placeholder="Adicionar recurso..." value={featureInput} onChange={e => setFeatureInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && addFeature()} />
|
<input className="form-input" placeholder="Adicionar recurso..." value={featureInput} onChange={e => setFeatureInput(e.target.value)} onKeyDown={e => {
|
||||||
<button className="btn btn-sm btn-secondary" onClick={addFeature}>+</button>
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addFeature();
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
<button type="button" className="btn btn-sm btn-secondary" onClick={addFeature}>+</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
{(form.features || []).map((f, i) => (
|
{(form.features || []).map((f, i) => (
|
||||||
<div key={i} className="flex-between" style={{ padding: '6px 10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
|
<div key={i} className="flex-between" style={{ padding: '6px 10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
|
||||||
<span>{f}</span>
|
<span>{f}</span>
|
||||||
<button className="btn-ghost btn-icon" onClick={() => removeFeature(i)} style={{ color: 'var(--danger)' }}><X size={12} /></button>
|
<button type="button" className="btn-ghost btn-icon" onClick={() => removeFeature(i)} style={{ color: 'var(--danger)' }}><X size={12} /></button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue