225 lines
9.1 KiB
TypeScript
225 lines
9.1 KiB
TypeScript
import Link from "next/link";
|
|
import { redirect } from "next/navigation";
|
|
import { getServerSession } from "next-auth";
|
|
import { authOptions } from "@/lib/auth";
|
|
import { prisma } from "@/lib/db";
|
|
import { AccountPrivacyActions } from "@/components/AccountPrivacyActions";
|
|
import { PlanBillingActions } from "@/components/PlanBillingActions";
|
|
import { DashboardShell } from "@/components/DashboardShell";
|
|
import { UpgradeProLink } from "@/components/UpgradeProLink";
|
|
import { getUserApiKeyStatus } from "@/lib/api-keys";
|
|
import { getApiRateLimitStatus } from "@/lib/api-rate-limit";
|
|
import { ApiKeySettings } from "@/components/ApiKeySettings";
|
|
import { hasProFeatures, isSelfHostedEdition } from "@/lib/edition";
|
|
import { EXTENSION_KIND, getQuotaExtensionUsage } from "@/lib/quota-extensions";
|
|
import { getQuotaInfo } from "@/lib/quota";
|
|
|
|
const PLAN_LABELS = {
|
|
FREE: "Bedroom Producer",
|
|
PREMIUM: "Pro",
|
|
} as const;
|
|
|
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="flex justify-between gap-4">
|
|
<dt className="text-gray-400">{label}</dt>
|
|
<dd className="text-right text-white">{value}</dd>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default async function SettingsPage() {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user?.email) redirect("/");
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { email: session.user.email },
|
|
include: { youtubeConnection: true },
|
|
});
|
|
if (!user) redirect("/");
|
|
|
|
const quota = await getQuotaInfo(user.id);
|
|
const selfHosted = isSelfHostedEdition();
|
|
const proFeatures = hasProFeatures(user.plan);
|
|
const extensionUsage =
|
|
!selfHosted && user.plan === "PREMIUM" ? await getQuotaExtensionUsage(user.id) : null;
|
|
const apiRateExtensionUsage = proFeatures
|
|
? await getQuotaExtensionUsage(user.id, EXTENSION_KIND.API_RATE_LIMIT)
|
|
: null;
|
|
const apiKeyStatus = proFeatures ? await getUserApiKeyStatus(user.id) : null;
|
|
const apiRateLimit = proFeatures ? await getApiRateLimitStatus(user.id) : null;
|
|
const planLabel = selfHosted ? "Self-hosted" : PLAN_LABELS[user.plan];
|
|
const youtube = user.youtubeConnection;
|
|
const channelUrl = youtube ? `https://www.youtube.com/channel/${youtube.channelId}` : null;
|
|
|
|
return (
|
|
<DashboardShell channelTitle={youtube?.channelTitle}>
|
|
<div className="mx-auto max-w-4xl px-6 py-8">
|
|
<h1 className="mb-6 text-2xl font-bold text-white">Settings</h1>
|
|
|
|
<div className="space-y-6">
|
|
<section className="rounded-lg border border-gray-700 bg-surface-light p-6">
|
|
<h2 className="mb-4 text-lg font-semibold text-white">Account information</h2>
|
|
<dl className="space-y-3 text-sm">
|
|
<InfoRow label="Name" value={user.name || "Not set"} />
|
|
<InfoRow label="Email" value={user.email} />
|
|
</dl>
|
|
|
|
<div className="mt-6 border-t border-gray-800 pt-6">
|
|
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-500">
|
|
YouTube
|
|
</h3>
|
|
{youtube ? (
|
|
<dl className="space-y-3 text-sm">
|
|
<InfoRow label="Channel name" value={youtube.channelTitle} />
|
|
<InfoRow label="Channel ID" value={youtube.channelId} />
|
|
</dl>
|
|
) : (
|
|
<p className="text-sm text-red-400">YouTube account not connected.</p>
|
|
)}
|
|
|
|
{channelUrl && (
|
|
<a
|
|
href={channelUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="mt-4 inline-flex rounded bg-accent px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-hover"
|
|
>
|
|
Go to your channel
|
|
</a>
|
|
)}
|
|
|
|
<p className="mt-4 text-sm text-gray-400">
|
|
To refresh your YouTube permissions,{" "}
|
|
<Link href="/" className="text-accent hover:underline">
|
|
sign out and sign in again
|
|
</Link>
|
|
.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="rounded-lg border border-gray-700 bg-surface-light p-6">
|
|
<h2 className="mb-4 text-lg font-semibold text-white">
|
|
{selfHosted ? "Usage" : "Plan & billing"}
|
|
</h2>
|
|
|
|
<div className="mb-6 rounded border border-gray-800 bg-surface p-4">
|
|
<div className="flex flex-wrap items-end justify-between gap-2">
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-gray-500">
|
|
Videos processed
|
|
</p>
|
|
<p className="mt-1 text-2xl font-semibold text-white">
|
|
{selfHosted ? (
|
|
quota.used
|
|
) : (
|
|
<>
|
|
{quota.remaining}
|
|
<span className="text-base font-normal text-gray-400">
|
|
{" "}
|
|
of {quota.limit} videos
|
|
</span>
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
{!selfHosted && (
|
|
<p className="text-sm text-gray-400">
|
|
{quota.used} used
|
|
{quota.bonusQuota > 0 ? ` · +${quota.bonusQuota} bonus` : ""}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{!selfHosted && (
|
|
<>
|
|
<div className="mt-3 h-2 overflow-hidden rounded bg-gray-800">
|
|
<div
|
|
className={`h-full rounded ${
|
|
quota.remaining <= 0
|
|
? "bg-red-500"
|
|
: quota.remaining / Math.max(1, quota.limit) <= 0.2
|
|
? "bg-amber-500"
|
|
: "bg-accent"
|
|
}`}
|
|
style={{
|
|
width: `${Math.min(100, Math.round((quota.used / Math.max(1, quota.limit)) * 100))}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
<p className="mt-2 text-xs text-gray-500">
|
|
{user.plan === "PREMIUM"
|
|
? `Monthly quota resets on ${quota.resetsIn}`
|
|
: `Quota resets in ${quota.resetsIn}`}
|
|
</p>
|
|
</>
|
|
)}
|
|
{selfHosted && (
|
|
<p className="mt-2 text-xs text-gray-500">
|
|
Self-hosted edition: no video or API quotas.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<dl className="space-y-3 text-sm">
|
|
<InfoRow label="Current plan" value={planLabel} />
|
|
{!selfHosted && user.plan === "PREMIUM" && user.subscribedAt && (
|
|
<InfoRow
|
|
label="Subscribed since"
|
|
value={user.subscribedAt.toLocaleDateString(undefined, {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
})}
|
|
/>
|
|
)}
|
|
{!selfHosted && user.plan === "PREMIUM" && (
|
|
<InfoRow
|
|
label="Payment method"
|
|
value={user.cardLast4 ? `•••• ${user.cardLast4}` : "No card on file"}
|
|
/>
|
|
)}
|
|
{extensionUsage && (
|
|
<InfoRow
|
|
label="Extension requests (this year)"
|
|
value={`${extensionUsage.used} / ${extensionUsage.limit}`}
|
|
/>
|
|
)}
|
|
<InfoRow label="Max batch size" value={`${quota.maxBatchSize} videos`} />
|
|
<InfoRow label="Max resolution" value={`${quota.maxResolutionHeight}p`} />
|
|
</dl>
|
|
|
|
{!selfHosted && user.plan === "FREE" && (
|
|
<p className="mt-4 text-sm text-gray-400">
|
|
<UpgradeProLink /> for more videos, 1080p, and lossless audio.
|
|
</p>
|
|
)}
|
|
{!selfHosted && user.plan === "PREMIUM" && extensionUsage && (
|
|
<PlanBillingActions initialUsage={extensionUsage} />
|
|
)}
|
|
</section>
|
|
|
|
{proFeatures && apiKeyStatus && apiRateLimit && apiRateExtensionUsage && (
|
|
<section className="rounded-lg border border-gray-700 bg-surface-light p-6">
|
|
<h2 className="mb-4 text-lg font-semibold text-white">API access</h2>
|
|
<ApiKeySettings
|
|
initialStatus={apiKeyStatus}
|
|
initialRateLimit={apiRateLimit}
|
|
initialExtensionUsage={apiRateExtensionUsage}
|
|
/>
|
|
</section>
|
|
)}
|
|
|
|
<section className="rounded-lg border border-gray-700 bg-surface-light p-6">
|
|
<h2 className="mb-2 text-lg font-semibold text-white">Account deletion & data</h2>
|
|
<p className="mb-4 text-sm text-gray-400">
|
|
Request a copy of your data or permanently delete your account and associated uploads.
|
|
</p>
|
|
<AccountPrivacyActions email={user.email} />
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</DashboardShell>
|
|
);
|
|
}
|