mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 16:20:32 +00:00
ca565eaa1a
## Summary - Tier upgrade/downgrade in the Store. The Subscriptions tab now shows all tiers including the user's current one. Other tiers swap "Subscribe" → "Switch" when the user already has a sub, and clicking them calls the new `POST /subscriptions/@me/change-tier` endpoint with a direction-aware confirm (upgrade charges prorated diff now, downgrade gives account credit). - Owned-tier card renders a **Current Plan** badge in place of the purchase button. Resolution logic in `resolveCosmetics` now reads `userMeResponse.player.subscription.tier` (with flare fallback) and marks that tier as `owned`. - AccountModal's `<subscription-panel>` reworked into a proper two-column layout: - **Left**: tier name, `$X.XX/mo` price, description, daily Pu/Caps amounts. - **Right**: status badge (Active / Renews date / Cancels date), `[Manage] [Change Tier]` button row, `[Cancel]` centered underneath. When `cancelAtPeriodEnd === true`, the row collapses to a single `[Reactivate]` button (opens the Stripe portal). - New `<o-button size="xs">` variant (`py-2 px-3 text-xs`) for the compact panel buttons. - Store dollar-purchase price label now supports an optional suffix (`/mo` for subs only) via a `priceSuffix` prop plumbed through `CosmeticContainer` → `PurchaseButton`. - `Api.ts` gains `changeSubscriptionTier(tierName)` with the same 401-handling pattern as the existing subscription helpers. <img width="1114" height="728" alt="Screenshot 2026-05-14 at 7 09 20 PM" src="https://github.com/user-attachments/assets/688f83d5-4010-4580-9214-6885af8ec98e" /> <img width="1038" height="276" alt="Screenshot 2026-05-14 at 7 09 33 PM" src="https://github.com/user-attachments/assets/458197f5-a0d4-4c32-bc55-31e5679629b5" /> <img width="887" height="286" alt="Screenshot 2026-05-14 at 7 09 55 PM" src="https://github.com/user-attachments/assets/8149ed82-89cc-4bbe-83de-3614f886b331" /> ## Discord evan
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import { LitElement, TemplateResult, html, nothing } from "lit";
|
|
import { customElement, property } from "lit/decorators.js";
|
|
import { translateText } from "../../Utils";
|
|
|
|
type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
|
|
type ButtonSize = "xs" | "sm" | "md" | "lg";
|
|
type ButtonWidth = "auto" | "block" | "blockDesktop" | "fill";
|
|
type IconPosition = "left" | "right" | "only";
|
|
|
|
@customElement("o-button")
|
|
export class OButton extends LitElement {
|
|
@property() title = "";
|
|
@property() translationKey = "";
|
|
@property() variant: ButtonVariant = "primary";
|
|
@property() size: ButtonSize = "md";
|
|
@property() width: ButtonWidth = "auto";
|
|
@property() iconPosition: IconPosition = "left";
|
|
@property({ attribute: false }) icon?: TemplateResult;
|
|
@property({ type: Boolean }) disable = false;
|
|
@property({ type: Boolean }) submit = false;
|
|
|
|
createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
private readonly BASE =
|
|
"font-bold uppercase tracking-wider rounded-xl border border-transparent " +
|
|
"transition-all duration-300 transform hover:-translate-y-px " +
|
|
"outline-none text-center whitespace-normal break-words leading-tight overflow-hidden relative " +
|
|
"disabled:cursor-not-allowed disabled:hover:translate-y-0 disabled:opacity-70";
|
|
|
|
private variantClasses(): string {
|
|
switch (this.variant) {
|
|
case "primary":
|
|
return "bg-malibu-blue hover:bg-aquarius text-white disabled:bg-gray-600 disabled:text-gray-300";
|
|
case "secondary":
|
|
return "bg-gray-700 hover:bg-gray-600 text-white disabled:bg-gray-800 disabled:text-gray-400";
|
|
case "danger":
|
|
return "bg-red-600 hover:bg-red-500 text-white disabled:bg-red-900 disabled:text-gray-300";
|
|
case "ghost":
|
|
return "bg-transparent hover:bg-white/10 text-malibu-blue disabled:text-gray-500 disabled:hover:bg-transparent";
|
|
}
|
|
}
|
|
|
|
private sizeClasses(): string {
|
|
if (this.iconPosition === "only") {
|
|
switch (this.size) {
|
|
case "xs":
|
|
return "w-6 h-6 text-xs";
|
|
case "sm":
|
|
return "w-8 h-8 text-sm";
|
|
case "md":
|
|
return "w-10 h-10 text-base";
|
|
case "lg":
|
|
return "w-12 h-12 text-lg";
|
|
}
|
|
}
|
|
switch (this.size) {
|
|
case "xs":
|
|
return "py-1 px-2 text-xs";
|
|
case "sm":
|
|
return "py-1.5 px-3 text-sm";
|
|
case "md":
|
|
return "py-3 px-4 text-base lg:text-lg";
|
|
case "lg":
|
|
return "py-4 px-6 text-lg lg:text-xl";
|
|
}
|
|
}
|
|
|
|
private widthClasses(): string {
|
|
switch (this.width) {
|
|
case "auto":
|
|
return "inline-flex items-center justify-center gap-2";
|
|
case "block":
|
|
return "flex w-full items-center justify-center gap-2";
|
|
case "blockDesktop":
|
|
return "flex w-full items-center justify-center gap-2 lg:w-1/2 lg:mx-auto";
|
|
case "fill":
|
|
return "flex w-full h-full items-center justify-center gap-2";
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const label =
|
|
this.translationKey === ""
|
|
? this.title
|
|
: translateText(this.translationKey);
|
|
const iconOnly = this.iconPosition === "only";
|
|
const classes = `${this.BASE} ${this.variantClasses()} ${this.sizeClasses()} ${this.widthClasses()}`;
|
|
|
|
return html`
|
|
<button
|
|
class=${classes}
|
|
?disabled=${this.disable}
|
|
type=${this.submit ? "submit" : "button"}
|
|
aria-label=${iconOnly ? label : nothing}
|
|
>
|
|
${this.icon && this.iconPosition !== "right" ? this.icon : nothing}
|
|
${iconOnly ? nothing : html`<span class="min-w-0">${label}</span>`}
|
|
${this.icon && this.iconPosition === "right" ? this.icon : nothing}
|
|
</button>
|
|
`;
|
|
}
|
|
}
|