From ffe08caae967cf7cc7c80336bcc7c4f27bc36f30 Mon Sep 17 00:00:00 2001 From: vimtor Date: Sun, 17 May 2026 09:57:18 +0200 Subject: [PATCH] core: show Go reward usage relief before applying Users can preview how a Go referral reward lowers current usage limits before they spend it, and invite codes normalize consistently across entry points. Modal buttons now keep the same disabled, hover, and primary actions in Go rewards, workspace creation, and payment method dialogs. --- .../console/app/src/component/go-referral.css | 46 +----- .../console/app/src/component/go-referral.tsx | 137 +++--------------- packages/console/app/src/component/modal.css | 55 +++++++ packages/console/app/src/i18n/en.ts | 6 - packages/console/app/src/i18n/zh.ts | 6 - .../console/app/src/lib/format-reset-time.ts | 47 ++++++ .../console/app/src/lib/referral-invite.ts | 4 +- .../app/src/routes/workspace-picker.css | 50 ------- .../app/src/routes/workspace-picker.tsx | 21 +-- .../workspace/[id]/billing/black-section.tsx | 19 +-- .../workspace/[id]/go/lite-section.module.css | 39 ----- .../routes/workspace/[id]/go/lite-section.tsx | 77 +++------- packages/console/core/src/referral.ts | 101 ++++++++++++- 13 files changed, 261 insertions(+), 347 deletions(-) create mode 100644 packages/console/app/src/lib/format-reset-time.ts diff --git a/packages/console/app/src/component/go-referral.css b/packages/console/app/src/component/go-referral.css index 96d0e47d7f..c9050e6cf9 100644 --- a/packages/console/app/src/component/go-referral.css +++ b/packages/console/app/src/component/go-referral.css @@ -1,46 +1,3 @@ -[data-component="go-credit-confirm"], -[data-component="go-referral-section"] { - a[data-color], - button { - display: inline-flex; - align-items: center; - justify-content: center; - padding: var(--space-3) var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - background-color: var(--color-bg); - color: var(--color-text); - font-size: var(--font-size-sm); - font-family: var(--font-sans); - font-weight: 500; - line-height: 1; - cursor: pointer; - text-decoration: none; - transition: all 0.15s ease; - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - &:hover:not(:disabled) { - background-color: var(--color-surface-hover); - border-color: var(--color-accent); - } - - &[data-color="primary"] { - background-color: var(--color-primary); - border-color: var(--color-primary); - color: var(--color-primary-text); - - &:hover:not(:disabled) { - background-color: var(--color-primary-hover); - border-color: var(--color-primary-hover); - } - } - } -} - [data-component="go-credit-confirm"] { display: flex; flex-direction: column; @@ -177,6 +134,9 @@ } button { + display: inline-flex; + align-items: center; + justify-content: center; gap: var(--space-2); min-width: 130px; white-space: nowrap; diff --git a/packages/console/app/src/component/go-referral.tsx b/packages/console/app/src/component/go-referral.tsx index 1897caf33f..6f0b305bb8 100644 --- a/packages/console/app/src/component/go-referral.tsx +++ b/packages/console/app/src/component/go-referral.tsx @@ -2,20 +2,16 @@ import { action, createAsync, json, query, useAction, useSubmission } from "@sol import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js" import { getRequestEvent } from "solid-js/web" import { Referral } from "@opencode-ai/console-core/referral.js" -import { Database, and, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" -import { LiteData } from "@opencode-ai/console-core/lite.js" -import { LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js" -import { ReferralRewardTable } from "@opencode-ai/console-core/schema/referral.sql.js" -import { Subscription } from "@opencode-ai/console-core/subscription.js" import { withActor } from "~/context/auth.withActor" import { Modal } from "~/component/modal" import { IconCheck, IconCopy } from "~/component/icon" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" -import { formatResetTime, queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section" +import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time" +import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section" import "./go-referral.css" -export type GoReferralReward = { +type GoReferralReward = { id: string amount: number email: string @@ -25,7 +21,7 @@ export type GoReferralReward = { timeApplied: string | Date | null } -export type GoReferralSummary = { +type GoReferralSummary = { inviteCode: string inviteUrl: string validInviteCount: number @@ -36,12 +32,6 @@ export type GoReferralSummary = { rewards: GoReferralReward[] } -type AnalyzedUsage = { - status: "ok" | "rate-limited" - resetInSec: number - usagePercent: number -} - type GoReferralUsagePreview = { rollingUsage: GoReferralUsagePreviewItem weeklyUsage: GoReferralUsagePreviewItem @@ -74,76 +64,7 @@ export const queryGoReferral = query(async (workspaceID: string) => { export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => { "use server" if (!referralID) return null - return withActor(async () => { - const row = await Database.use((tx) => - tx - .select({ - rewardAmount: ReferralRewardTable.amount, - rollingUsage: LiteTable.rollingUsage, - weeklyUsage: LiteTable.weeklyUsage, - monthlyUsage: LiteTable.monthlyUsage, - timeRollingUpdated: LiteTable.timeRollingUpdated, - timeWeeklyUpdated: LiteTable.timeWeeklyUpdated, - timeMonthlyUpdated: LiteTable.timeMonthlyUpdated, - timeCreated: LiteTable.timeCreated, - }) - .from(ReferralRewardTable) - .innerJoin(LiteTable, eq(LiteTable.workspaceID, ReferralRewardTable.workspaceID)) - .where( - and( - eq(ReferralRewardTable.workspaceID, workspaceID), - eq(ReferralRewardTable.referralID, referralID), - isNull(ReferralRewardTable.timeApplied), - isNull(ReferralRewardTable.timeDeleted), - isNull(LiteTable.timeDeleted), - ), - ) - .then((rows) => rows[0]), - ) - if (!row) return null - - const limits = LiteData.getLimits() - const rollingBefore = Subscription.analyzeRollingUsage({ - limit: limits.rollingLimit, - window: limits.rollingWindow, - usage: row.rollingUsage ?? 0, - timeUpdated: row.timeRollingUpdated ?? new Date(), - }) - const rollingAfter = Subscription.analyzeRollingUsage({ - limit: limits.rollingLimit, - window: limits.rollingWindow, - usage: Math.max(0, (row.rollingUsage ?? 0) - row.rewardAmount), - timeUpdated: row.timeRollingUpdated ?? new Date(), - }) - const weeklyBefore = Subscription.analyzeWeeklyUsage({ - limit: limits.weeklyLimit, - usage: row.weeklyUsage ?? 0, - timeUpdated: row.timeWeeklyUpdated ?? new Date(), - }) - const weeklyAfter = Subscription.analyzeWeeklyUsage({ - limit: limits.weeklyLimit, - usage: Math.max(0, (row.weeklyUsage ?? 0) - row.rewardAmount), - timeUpdated: row.timeWeeklyUpdated ?? new Date(), - }) - const monthlyBefore = Subscription.analyzeMonthlyUsage({ - limit: limits.monthlyLimit, - usage: row.monthlyUsage ?? 0, - timeUpdated: row.timeMonthlyUpdated ?? new Date(), - timeSubscribed: row.timeCreated, - }) - const monthlyAfter = Subscription.analyzeMonthlyUsage({ - limit: limits.monthlyLimit, - usage: Math.max(0, (row.monthlyUsage ?? 0) - row.rewardAmount), - timeUpdated: row.timeMonthlyUpdated ?? new Date(), - timeSubscribed: row.timeCreated, - }) - - return { - rollingUsage: usagePreview(rollingBefore, rollingAfter), - weeklyUsage: usagePreview(weeklyBefore, weeklyAfter), - monthlyUsage: usagePreview(monthlyBefore, monthlyAfter), - } satisfies GoReferralUsagePreview - }, workspaceID) + return withActor(() => Referral.usagePreview({ referralID }), workspaceID) }, "go.referral.usagePreview") export const applyGoReferralReward = action(async (workspaceID: string, referralID: string) => { @@ -160,18 +81,14 @@ export const applyGoReferralReward = action(async (workspaceID: string, referral ) }, "go.referral.reward.apply") -function usagePreview(before: AnalyzedUsage, after: AnalyzedUsage) { +function currentUsagePreview(usage: { resetInSec: number; usagePercent: number }) { return { - beforePercent: before.usagePercent, - afterPercent: after.usagePercent, - resetInSec: after.resetInSec, + beforePercent: usage.usagePercent, + afterPercent: usage.usagePercent, + resetInSec: usage.resetInSec, } } -function currentUsagePreview(usage: AnalyzedUsage) { - return usagePreview(usage, usage) -} - function formatCurrency(amount: number) { if (amount % 100 === 0) return `$${amount / 100}` return `$${(amount / 100).toFixed(2)}` @@ -299,7 +216,14 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer - +
+
    +
  1. {i18n.t("workspace.referral.instructions.share")}
  2. +
  3. {i18n.t("workspace.referral.instructions.subscribe")}
  4. +
  5. {i18n.t("workspace.referral.instructions.claim")}
  6. +
  7. {i18n.t("workspace.referral.instructions.apply", { amount: formatCurrency(props.summary.rewardAmount) })}
  8. +
+

{i18n.t("workspace.referral.rewards.title")}

@@ -327,9 +251,9 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer {(reward) => { - const applied = createMemo(() => reward.status === "applied") - const pending = createMemo(() => reward.status === "pending") - const earnedAt = createMemo(() => formatDate(reward.timeCreated, language.tag(language.locale()))) + const applied = reward.status === "applied" + const pending = reward.status === "pending" + const earnedAt = () => formatDate(reward.timeCreated, language.tag(language.locale())) return ( {formatCurrency(reward.amount)} @@ -346,8 +270,8 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer disabled={reward.status !== "available" || !props.summary.hasActiveGo || submission.pending} onClick={() => setSelected(reward)} > - - {pending() + + {pending ? i18n.t(rewardPendingStatusKey(reward.source)) : props.summary.hasActiveGo ? i18n.t("workspace.referral.apply.action") @@ -424,23 +348,8 @@ function GoReferralUsagePreviewRow(props: { label: string; usage: GoReferralUsag
- {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(props.usage.resetInSec, i18n)} + {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)}
) } - -function InvitationInstructions(props: { rewardAmount: number }) { - const i18n = useI18n() - - return ( -
-
    -
  1. {i18n.t("workspace.referral.instructions.share")}
  2. -
  3. {i18n.t("workspace.referral.instructions.subscribe")}
  4. -
  5. {i18n.t("workspace.referral.instructions.claim")}
  6. -
  7. {i18n.t("workspace.referral.instructions.apply", { amount: formatCurrency(props.rewardAmount) })}
  8. -
-
- ) -} diff --git a/packages/console/app/src/component/modal.css b/packages/console/app/src/component/modal.css index cc7708bba8..ab7f222b02 100644 --- a/packages/console/app/src/component/modal.css +++ b/packages/console/app/src/component/modal.css @@ -55,6 +55,61 @@ @media (prefers-color-scheme: dark) { box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); } + + button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--space-3) var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-sans); + font-weight: 500; + line-height: 1; + cursor: pointer; + transition: all 0.15s ease; + + &:hover:not(:disabled) { + background-color: var(--color-surface-hover); + border-color: var(--color-accent); + } + + &:active:not(:disabled) { + transform: translateY(1px); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + } + + &[data-color="primary"] { + background-color: var(--color-primary); + border-color: var(--color-primary); + color: var(--color-primary-text); + + &:hover:not(:disabled) { + background-color: var(--color-primary-hover); + border-color: var(--color-primary-hover); + } + } + + &[data-color="ghost"] { + background-color: transparent; + border-color: transparent; + color: var(--color-text-muted); + + &:hover:not(:disabled) { + background-color: var(--color-surface-hover); + border-color: var(--color-border); + color: var(--color-text); + } + } + } } [data-slot="title"] { diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index e2fe92efb5..ad5bc7d5fe 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -662,7 +662,6 @@ export const dict = { "workspace.lite.promo.otherMethods": "Other payment methods", "workspace.lite.promo.selectMethod": "Select payment method", - "workspace.referral.inviteLink": "Invite link", "workspace.referral.copyLink": "Copy Link", "workspace.referral.copied": "Copied", "workspace.referral.overview.title": "Invite friends to Go", @@ -680,8 +679,6 @@ export const dict = { "workspace.referral.table.reward": "Reward", "workspace.referral.table.referral": "Description", "workspace.referral.table.date": "Date", - "workspace.referral.reward.source.inviter": "Earned by inviting a friend", - "workspace.referral.reward.source.invitee": "Received from an invite", "workspace.referral.reward.source.pendingInviter": "Waiting for them to subscribe", "workspace.referral.reward.source.pendingInvitee": "Subscribe to unlock reward", "workspace.referral.reward.source.available": "Reward ready to apply", @@ -689,9 +686,6 @@ export const dict = { "workspace.referral.reward.status.applied": "Applied", "workspace.referral.reward.status.pendingInviter": "Waiting for subscription", "workspace.referral.reward.status.pendingInvitee": "Subscribe to unlock", - "workspace.referral.reward.pending.inviter": "Reward unlocks when they subscribe to Go.", - "workspace.referral.reward.pending.invitee": "Subscribe to Go to unlock this reward.", - "workspace.referral.reward.earnedOn": "{{date}}", "workspace.referral.apply.noGo": "Subscribe to Go", "workspace.referral.apply.action": "Apply", "workspace.referral.apply.confirmTitle": "Apply Go reward", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 62a78a6f25..4fbb7d600e 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -643,7 +643,6 @@ export const dict = { "workspace.lite.promo.otherMethods": "其他付款方式", "workspace.lite.promo.selectMethod": "选择付款方式", - "workspace.referral.inviteLink": "邀请链接", "workspace.referral.copyLink": "复制链接", "workspace.referral.copied": "已复制", "workspace.referral.overview.title": "邀请好友使用 Go", @@ -661,8 +660,6 @@ export const dict = { "workspace.referral.table.reward": "奖励", "workspace.referral.table.referral": "描述", "workspace.referral.table.date": "日期", - "workspace.referral.reward.source.inviter": "邀请好友获得", - "workspace.referral.reward.source.invitee": "通过邀请获得", "workspace.referral.reward.source.pendingInviter": "等待对方订阅", "workspace.referral.reward.source.pendingInvitee": "订阅即可解锁奖励", "workspace.referral.reward.source.available": "奖励可使用", @@ -670,9 +667,6 @@ export const dict = { "workspace.referral.reward.status.applied": "已使用", "workspace.referral.reward.status.pendingInviter": "等待订阅", "workspace.referral.reward.status.pendingInvitee": "订阅后解锁", - "workspace.referral.reward.pending.inviter": "好友订阅 Go 后即可解锁奖励。", - "workspace.referral.reward.pending.invitee": "订阅 Go 后即可解锁此奖励。", - "workspace.referral.reward.earnedOn": "{{date}}", "workspace.referral.apply.noGo": "订阅 Go", "workspace.referral.apply.action": "使用", "workspace.referral.apply.confirmTitle": "使用 Go 奖励", diff --git a/packages/console/app/src/lib/format-reset-time.ts b/packages/console/app/src/lib/format-reset-time.ts new file mode 100644 index 0000000000..2462a87a3f --- /dev/null +++ b/packages/console/app/src/lib/format-reset-time.ts @@ -0,0 +1,47 @@ +import type { Key } from "~/i18n" +import type { useI18n } from "~/context/i18n" + +type ResetTimeKeys = { + day: Key + days: Key + hour: Key + hours: Key + minute: Key + minutes: Key + fewSeconds: Key +} + +export const liteResetTimeKeys = { + day: "workspace.lite.time.day", + days: "workspace.lite.time.days", + hour: "workspace.lite.time.hour", + hours: "workspace.lite.time.hours", + minute: "workspace.lite.time.minute", + minutes: "workspace.lite.time.minutes", + fewSeconds: "workspace.lite.time.fewSeconds", +} satisfies ResetTimeKeys + +export const blackResetTimeKeys = { + day: "workspace.black.time.day", + days: "workspace.black.time.days", + hour: "workspace.black.time.hour", + hours: "workspace.black.time.hours", + minute: "workspace.black.time.minute", + minutes: "workspace.black.time.minutes", + fewSeconds: "workspace.black.time.fewSeconds", +} satisfies ResetTimeKeys + +export function formatResetTime(seconds: number, i18n: ReturnType, keys: ResetTimeKeys) { + const days = Math.floor(seconds / 86400) + if (days >= 1) { + const hours = Math.floor((seconds % 86400) / 3600) + return `${days} ${days === 1 ? i18n.t(keys.day) : i18n.t(keys.days)} ${hours} ${hours === 1 ? i18n.t(keys.hour) : i18n.t(keys.hours)}` + } + + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + if (hours >= 1) + return `${hours} ${hours === 1 ? i18n.t(keys.hour) : i18n.t(keys.hours)} ${minutes} ${minutes === 1 ? i18n.t(keys.minute) : i18n.t(keys.minutes)}` + if (minutes === 0) return i18n.t(keys.fewSeconds) + return `${minutes} ${minutes === 1 ? i18n.t(keys.minute) : i18n.t(keys.minutes)}` +} diff --git a/packages/console/app/src/lib/referral-invite.ts b/packages/console/app/src/lib/referral-invite.ts index 835364d40c..f5bb96f065 100644 --- a/packages/console/app/src/lib/referral-invite.ts +++ b/packages/console/app/src/lib/referral-invite.ts @@ -1,8 +1,10 @@ +import { Referral } from "@opencode-ai/console-core/referral.js" + const INVITE_COOKIE = "opencode.go.invite" const INVITE_MAX_AGE = 60 * 60 * 24 * 30 export function normalizeInviteCode(code?: string | null) { - return code?.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 10) + return Referral.normalizeCode(code) } export function inviteCookie(code: string) { diff --git a/packages/console/app/src/routes/workspace-picker.css b/packages/console/app/src/routes/workspace-picker.css index e566e9665e..df7552fb75 100644 --- a/packages/console/app/src/routes/workspace-picker.css +++ b/packages/console/app/src/routes/workspace-picker.css @@ -39,56 +39,6 @@ [data-component="workspace-create-modal"] { width: 100%; - button { - padding: var(--space-3) var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - background-color: var(--color-bg); - color: var(--color-text); - font-size: var(--font-size-sm); - font-family: var(--font-sans); - font-weight: 500; - cursor: pointer; - transition: all 0.15s ease; - - &:hover:not(:disabled) { - background-color: var(--color-surface-hover); - border-color: var(--color-accent); - } - - &:active { - transform: translateY(1px); - } - - &:disabled { - opacity: 0.5; - transform: none; - } - - &[data-color="primary"] { - background-color: var(--color-primary); - border-color: var(--color-primary); - color: var(--color-primary-text); - - &:hover:not(:disabled) { - background-color: var(--color-primary-hover); - border-color: var(--color-primary-hover); - } - } - - &[data-color="ghost"] { - background-color: transparent; - border-color: transparent; - color: var(--color-text-muted); - - &:hover:not(:disabled) { - background-color: var(--color-surface-hover); - border-color: var(--color-border); - color: var(--color-text); - } - } - } - [data-slot="create-form"] { width: 100%; } diff --git a/packages/console/app/src/routes/workspace-picker.tsx b/packages/console/app/src/routes/workspace-picker.tsx index 16d0b8197b..83f5582ebe 100644 --- a/packages/console/app/src/routes/workspace-picker.tsx +++ b/packages/console/app/src/routes/workspace-picker.tsx @@ -1,6 +1,5 @@ import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router" -import { For, createEffect } from "solid-js" -import { createStore } from "solid-js/store" +import { For, createEffect, createSignal } from "solid-js" import { withActor } from "~/context/auth.withActor" import { Actor } from "@opencode-ai/console-core/actor.js" import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" @@ -51,9 +50,7 @@ export function WorkspacePicker() { const i18n = useI18n() const workspaces = createAsync(() => getWorkspaces()) const submission = useSubmission(createWorkspace) - const [store, setStore] = createStore({ - showForm: false, - }) + const [showForm, setShowForm] = createSignal(false) let inputRef: HTMLInputElement | undefined const currentWorkspace = () => { @@ -61,12 +58,8 @@ export function WorkspacePicker() { return ws ? ws.name : i18n.t("workspace.select") } - const handleWorkspaceNew = () => { - setStore("showForm", true) - } - createEffect(() => { - if (store.showForm && inputRef) { + if (showForm() && inputRef) { setTimeout(() => inputRef?.focus(), 0) } }) @@ -79,7 +72,7 @@ export function WorkspacePicker() { // Reset signals when workspace ID changes createEffect(() => { params.id - setStore("showForm", false) + setShowForm(false) }) return ( @@ -92,12 +85,12 @@ export function WorkspacePicker() { )} - - setStore("showForm", false)} title={i18n.t("workspace.modal.title")}> + setShowForm(false)} title={i18n.t("workspace.modal.title")}>
@@ -110,7 +103,7 @@ export function WorkspacePicker() { required />
-
{i18n.t("workspace.black.subscription.resetsIn")}{" "} - {formatResetTime(sub().rollingUsage.resetInSec, i18n)} + {formatResetTime(sub().rollingUsage.resetInSec, i18n, blackResetTimeKeys)}
@@ -222,7 +209,7 @@ export function BlackSection() {
{i18n.t("workspace.black.subscription.resetsIn")}{" "} - {formatResetTime(sub().weeklyUsage.resetInSec, i18n)} + {formatResetTime(sub().weeklyUsage.resetInSec, i18n, blackResetTimeKeys)}
diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css index c239e0461a..0904f8b4aa 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css @@ -215,45 +215,6 @@ } .paymentMethodModal { - button { - padding: var(--space-3) var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - background-color: var(--color-bg); - color: var(--color-text); - font-size: var(--font-size-sm); - font-family: var(--font-sans); - font-weight: 500; - cursor: pointer; - transition: all 0.15s ease; - - &:hover:not(:disabled) { - background-color: var(--color-surface-hover); - border-color: var(--color-accent); - } - - &:active { - transform: translateY(1px); - } - - &:disabled { - opacity: 0.5; - transform: none; - } - - &[data-color="ghost"] { - background-color: transparent; - border-color: transparent; - color: var(--color-text-muted); - - &:hover:not(:disabled) { - background-color: var(--color-surface-hover); - border-color: var(--color-border); - color: var(--color-text); - } - } - } - [data-slot="modal-actions"] { display: flex; gap: var(--space-3); diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 054752a7a9..04d45ce258 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -14,6 +14,7 @@ import styles from "./lite-section.module.css" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" import { formError } from "~/lib/form-error" +import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time" import { IconAlipay, IconUpi } from "~/component/icon" @@ -67,20 +68,6 @@ export const queryLiteSubscription = query(async (workspaceID: string) => { }, workspaceID) }, "lite.subscription.get") -export function formatResetTime(seconds: number, i18n: ReturnType) { - const days = Math.floor(seconds / 86400) - if (days >= 1) { - const hours = Math.floor((seconds % 86400) / 3600) - return `${days} ${days === 1 ? i18n.t("workspace.lite.time.day") : i18n.t("workspace.lite.time.days")} ${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")}` - } - const hours = Math.floor(seconds / 3600) - const minutes = Math.floor((seconds % 3600) / 60) - if (hours >= 1) - return `${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")} ${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` - if (minutes === 0) return i18n.t("workspace.lite.time.fewSeconds") - return `${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` -} - const createLiteCheckoutUrl = action( async (workspaceID: string, successUrl: string, cancelUrl: string, method?: "alipay" | "upi") => { "use server" @@ -140,6 +127,25 @@ const setLiteUseBalance = action(async (form: FormData) => { ) }, "setLiteUseBalance") +function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) { + const i18n = useI18n() + + return ( +
+
+ {props.label} + {props.usage.usagePercent}% +
+
+
+
+ + {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)} + +
+ ) +} + export function LiteSection() { const params = useParams() const i18n = useI18n() @@ -207,44 +213,9 @@ export function LiteSection() { .
-
-
- {i18n.t("workspace.lite.subscription.rollingUsage")} - {sub().rollingUsage.usagePercent}% -
-
-
-
- - {i18n.t("workspace.lite.subscription.resetsIn")}{" "} - {formatResetTime(sub().rollingUsage.resetInSec, i18n)} - -
-
-
- {i18n.t("workspace.lite.subscription.weeklyUsage")} - {sub().weeklyUsage.usagePercent}% -
-
-
-
- - {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(sub().weeklyUsage.resetInSec, i18n)} - -
-
-
- {i18n.t("workspace.lite.subscription.monthlyUsage")} - {sub().monthlyUsage.usagePercent}% -
-
-
-
- - {i18n.t("workspace.lite.subscription.resetsIn")}{" "} - {formatResetTime(sub().monthlyUsage.resetInSec, i18n)} - -
+ + +

{i18n.t("workspace.lite.subscription.useBalance")}

@@ -330,7 +301,7 @@ export function LiteSection() { onClose={() => setStore("showModal", false)} title={i18n.t("workspace.lite.promo.selectMethod")} > -
+