deep research addressement
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { createSignal, createResource } from "solid-js";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { A } from "@solidjs/router";
|
||||
import { Sidebar, TopBar } from "~/components/dashboard";
|
||||
import { Button, Card, Input } from "~/components/ui";
|
||||
import { Button, Card, Input, Badge } from "~/components/ui";
|
||||
import { useAuth, useSubscription } from "~/hooks";
|
||||
import { api } from "~/lib/api";
|
||||
|
||||
@@ -11,6 +12,8 @@ export default function SettingsPage() {
|
||||
const subscription = useSubscription();
|
||||
const [name, setName] = createSignal(auth.user()?.name ?? "");
|
||||
const [saving, setSaving] = createSignal(false);
|
||||
const [portalLoading, setPortalLoading] = createSignal(false);
|
||||
const [cancelLoading, setCancelLoading] = createSignal(false);
|
||||
|
||||
async function saveProfile() {
|
||||
setSaving(true);
|
||||
@@ -21,6 +24,45 @@ export default function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openBillingPortal() {
|
||||
setPortalLoading(true);
|
||||
try {
|
||||
const result = await api.billing.createPortalSession.mutate({
|
||||
returnUrl: `${window.location.origin}/settings`,
|
||||
});
|
||||
window.location.href = result.url;
|
||||
} catch {
|
||||
setPortalLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancelSubscription() {
|
||||
const sub = subscription.subscription();
|
||||
if (!sub || !sub.stripeId) return;
|
||||
|
||||
setCancelLoading(true);
|
||||
try {
|
||||
await api.billing.cancelSubscription.mutate({
|
||||
subscriptionId: sub.stripeId,
|
||||
});
|
||||
} catch {
|
||||
// Error handled by trpc
|
||||
} finally {
|
||||
setCancelLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusBadgeClass(status: string): string {
|
||||
switch (status) {
|
||||
case "active": return "bg-green-100 text-green-800";
|
||||
case "trialing": return "bg-blue-100 text-blue-800";
|
||||
case "past_due": return "bg-yellow-100 text-yellow-800";
|
||||
case "canceled": return "bg-red-100 text-red-800";
|
||||
case "unpaid": return "bg-red-100 text-red-800";
|
||||
default: return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
||||
<Title>Settings — Kordant</Title>
|
||||
@@ -50,12 +92,89 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="p-4">
|
||||
<Card class="p-4 mb-6">
|
||||
<h2 class="text-sm font-semibold text-[var(--color-text-primary)] mb-4">Subscription</h2>
|
||||
<p class="text-sm text-[var(--color-text-secondary)] mb-1">Current Plan</p>
|
||||
<p class="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||
{(subscription.tier().charAt(0).toUpperCase() + subscription.tier().slice(1)) || "Free"}
|
||||
</p>
|
||||
|
||||
<Show
|
||||
when={subscription.subscription()}
|
||||
fallback={
|
||||
<div>
|
||||
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||
You're on the free plan. Upgrade to unlock all features.
|
||||
</p>
|
||||
<A href="/pricing">
|
||||
<Button variant="primary">View Plans</Button>
|
||||
</A>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(sub) => (
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p class="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||
{sub().tier?.charAt(0).toUpperCase() + sub().tier?.slice(1) || "Free"}
|
||||
{sub().isTrialing ? " (Trial)" : ""}
|
||||
</p>
|
||||
<p class="text-sm text-[var(--color-text-secondary)]">
|
||||
<span class={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium capitalize ${getStatusBadgeClass(sub().status ?? "active")}`}>
|
||||
{sub().status}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<Show when={sub().cancelAtPeriodEnd}>
|
||||
<Badge variant="info">Cancels at period end</Badge>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={sub().currentPeriodEnd}>
|
||||
{(end) => (
|
||||
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||
{sub().isTrialing && sub().trialEnd
|
||||
? `Trial ends ${new Date(sub().trialEnd as any).toLocaleDateString()}`
|
||||
: `Next billing date: ${new Date(end() as any).toLocaleDateString()}`}
|
||||
</p>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={sub().defaultPaymentMethodLast4}>
|
||||
{(last4) => (
|
||||
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||
Payment method: •••• {last4()}
|
||||
</p>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={openBillingPortal}
|
||||
loading={portalLoading()}
|
||||
>
|
||||
Manage Billing
|
||||
</Button>
|
||||
|
||||
<Show when={sub().status === "active" || sub().status === "trialing"}>
|
||||
<Show when={!sub().cancelAtPeriodEnd}>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleCancelSubscription}
|
||||
loading={cancelLoading()}
|
||||
>
|
||||
Cancel Subscription
|
||||
</Button>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={sub().cancelAtPeriodEnd}>
|
||||
<p class="text-xs text-[var(--color-text-secondary)] mt-3">
|
||||
Your subscription will remain active until the end of your billing period.
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user