refactor(subscription): migrate payment components to tailwind

This commit is contained in:
2026-07-13 14:12:57 +08:00
parent 34c6f5523c
commit 2ebf5b9e97
5 changed files with 93 additions and 185 deletions
@@ -2,6 +2,8 @@ import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { SubscriptionCtaButton } from "../subscription-cta-button"; import { SubscriptionCtaButton } from "../subscription-cta-button";
import { SubscriptionPaymentMethod } from "../subscription-payment-method";
import { SubscriptionPaymentSuccessDialog } from "../subscription-payment-success-dialog";
describe("subscription Tailwind components", () => { describe("subscription Tailwind components", () => {
it("renders SubscriptionCtaButton with loading state", () => { it("renders SubscriptionCtaButton with loading state", () => {
@@ -15,4 +17,55 @@ describe("subscription Tailwind components", () => {
expect(html).toContain("disabled"); expect(html).toContain("disabled");
expect(html).toContain("Pay now"); expect(html).toContain("Pay now");
}); });
it("renders SubscriptionPaymentSuccessDialog with topup content", () => {
expect(
renderToStaticMarkup(
<SubscriptionPaymentSuccessDialog
open={false}
subscriptionType="vip"
onClose={() => undefined}
/>,
),
).toBe("");
const html = renderToStaticMarkup(
<SubscriptionPaymentSuccessDialog
open
subscriptionType="topup"
onClose={() => undefined}
/>,
);
expect(html).toContain('role="alertdialog"');
expect(html).toContain("z-80");
expect(html).toContain("Payment successful");
expect(html).toContain("Your coins have been added");
expect(html).toContain("bg-[linear-gradient(135deg,#ff67e0_0%,#f657a0_100%)]");
expect(html).toContain("OK");
});
it("renders SubscriptionPaymentMethod with default ordering and selection", () => {
const html = renderToStaticMarkup(
<SubscriptionPaymentMethod
value="ezpay"
defaultChannel="ezpay"
caption="GCash by default"
onChange={() => undefined}
/>,
);
expect(html).toContain('aria-labelledby="payment-method-title"');
expect(html).toContain("Payment Method");
expect(html).toContain("GCash by default");
expect(html.indexOf('aria-label="GCash"')).toBeLessThan(
html.indexOf('aria-label="Stripe"'),
);
expect(html).toContain('aria-pressed="true"');
expect(html).toContain("border-[#d92f7f]");
expect(html).toContain("bg-[#fff4f9]");
expect(html).toContain("0_0_0_3px_rgba(217,47,127,0.18)");
expect(html).toContain("-translate-y-0.5");
expect(html).toContain("/images/subscription/gcash-logo.svg");
});
}); });
@@ -1,87 +0,0 @@
.root {
display: flex;
flex-direction: column;
gap: clamp(8px, 1.852vw, 10px);
}
.header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--spacing-sm);
padding: 0 var(--spacing-xs);
}
.title {
margin: 0;
font-family: var(--font-athelas), var(--font-system);
font-size: var(--responsive-card-title, var(--font-size-lg));
font-weight: 700;
color: var(--color-auth-text-primary);
line-height: 1.2;
}
.caption {
color: var(--color-text-secondary);
font-size: var(--responsive-caption, var(--font-size-sm));
line-height: 1.2;
white-space: nowrap;
}
.options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--spacing-sm);
}
.option {
display: flex;
min-height: clamp(54px, 11.481vw, 62px);
align-items: center;
justify-content: center;
padding: clamp(9px, 1.852vw, 10px) clamp(12px, 2.593vw, 14px);
border: 1px solid rgba(246, 87, 160, 0.18);
border-radius: var(--responsive-card-radius-sm, 18px);
background: #ffffff;
box-shadow: 0 5px 7px 0 rgba(247, 89, 168, 0.08);
color: #3c3b3b;
cursor: pointer;
font: inherit;
text-align: center;
transition: border-color 0.15s ease, box-shadow 0.15s ease,
transform 0.05s ease;
}
.option:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.option:not(:disabled):active {
transform: scale(0.98);
}
.option:disabled {
cursor: not-allowed;
opacity: 0.72;
}
.selected {
border-color: #f657a0;
border-width: 2px;
box-shadow: 0 6px 12px 0 rgba(247, 89, 168, 0.14);
}
.optionTitle {
color: #1e1e1e;
font-size: var(--responsive-body, 16px);
font-weight: 700;
line-height: 1.1;
}
.optionLogo {
display: block;
width: min(100%, 92px);
max-height: 34px;
object-fit: contain;
}
@@ -4,8 +4,6 @@ import Image from "next/image";
import type { PayChannel } from "@/data/dto/payment"; import type { PayChannel } from "@/data/dto/payment";
import styles from "./subscription-payment-method.module.css";
const PAYMENT_METHODS: Array<{ const PAYMENT_METHODS: Array<{
channel: PayChannel; channel: PayChannel;
title: string; title: string;
@@ -45,19 +43,30 @@ export function SubscriptionPaymentMethod({
: PAYMENT_METHODS; : PAYMENT_METHODS;
return ( return (
<section className={styles.root} aria-labelledby="payment-method-title"> <section
<div className={styles.header}> className="flex flex-col gap-[clamp(8px,1.852vw,10px)]"
<h2 id="payment-method-title" className={styles.title}> aria-labelledby="payment-method-title"
>
<div className="flex items-baseline justify-between gap-sm px-xs">
<h2
id="payment-method-title"
className="m-0 text-(length:--responsive-card-title,var(--font-size-lg)) font-bold leading-[1.2] text-auth-text-primary"
style={{ fontFamily: "var(--font-athelas), var(--font-system)" }}
>
Payment Method Payment Method
</h2> </h2>
<span className={styles.caption}>{caption}</span> <span className="whitespace-nowrap text-(length:--responsive-caption,var(--font-size-sm)) leading-[1.2] text-text-secondary">
{caption}
</span>
</div> </div>
<div className={styles.options}> <div className="grid grid-cols-2 gap-sm">
{paymentMethods.map((method) => { {paymentMethods.map((method) => {
const selected = method.channel === value; const selected = method.channel === value;
const classes = [ const classes = [
styles.option, "flex min-h-[clamp(54px,11.481vw,62px)] cursor-pointer items-center justify-center rounded-(--responsive-card-radius-sm,18px) border border-[rgba(246,87,160,0.18)] bg-white px-[clamp(12px,2.593vw,14px)] py-[clamp(9px,1.852vw,10px)] text-center font-[inherit] text-[#3c3b3b] shadow-[0_5px_7px_0_rgba(247,89,168,0.08)] transition-[border-color,box-shadow,transform] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--color-accent) active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-72",
selected ? styles.selected : "", selected
? "border-2 border-[#d92f7f] bg-[#fff4f9] shadow-[0_8px_18px_rgba(217,47,127,0.24),0_0_0_3px_rgba(217,47,127,0.18)] -translate-y-0.5"
: "",
] ]
.filter(Boolean) .filter(Boolean)
.join(" "); .join(" ");
@@ -79,10 +88,12 @@ export function SubscriptionPaymentMethod({
aria-hidden="true" aria-hidden="true"
width={122} width={122}
height={29} height={29}
className={styles.optionLogo} className="block max-h-[34px] w-[min(100%,92px)] object-contain"
/> />
) : ( ) : (
<span className={styles.optionTitle}>{method.title}</span> <span className="text-(length:--responsive-body,16px) font-bold leading-[1.1] text-[#1e1e1e]">
{method.title}
</span>
)} )}
</button> </button>
); );
@@ -1,79 +0,0 @@
.overlay {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: center;
justify-content: center;
padding:
calc(var(--dialog-safe-margin, 20px) + var(--app-safe-top, 0px))
calc(var(--dialog-safe-margin, 20px) + var(--app-safe-right, 0px))
calc(var(--dialog-safe-margin, 20px) + var(--app-safe-bottom, 0px))
calc(var(--dialog-safe-margin, 20px) + var(--app-safe-left, 0px));
background: rgba(0, 0, 0, 0.45);
}
.dialog {
width: 100%;
max-width: var(--dialog-max-width, 380px);
border-radius: var(--responsive-card-radius, 32px);
background: var(--color-page-background, #ffffff);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16);
padding:
var(--responsive-card-padding-lg, 28px)
var(--responsive-card-padding, 20px)
var(--responsive-card-padding, 20px);
text-align: center;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(52px, 10.741vw, 58px);
height: clamp(52px, 10.741vw, 58px);
margin-bottom: var(--page-section-gap, 16px);
border-radius: 999px;
background: linear-gradient(135deg, #ff67e0 0%, #f657a0 100%);
color: #ffffff;
font-size: clamp(28px, 5.926vw, 32px);
font-weight: 700;
line-height: 1;
box-shadow: 0 8px 18px rgba(247, 89, 168, 0.28);
}
.title {
margin: 0 0 clamp(7px, 1.481vw, 8px);
color: var(--color-text-foreground, #171717);
font-size: var(--responsive-page-title, 22px);
font-weight: 700;
line-height: 1.2;
}
.content {
margin: 0;
color: #393939;
font-size: var(--responsive-body, 14px);
line-height: 1.5;
}
.button {
width: 100%;
min-height: var(--responsive-control-height, 48px);
margin-top: var(--page-section-gap-lg, 22px);
border: 0;
border-radius: 999px;
background:
linear-gradient(269deg, #ff67e0 0%, rgba(254, 104, 224, 0.5) 20%, rgba(252, 105, 223, 0.79) 66%, #f96ade 100%),
linear-gradient(#f657a0, #f657a0);
background-blend-mode: normal, normal;
box-shadow: 0 5px 7px rgba(247, 89, 168, 0.31);
color: #ffffff;
cursor: pointer;
font-size: var(--responsive-body, 16px);
font-weight: 700;
}
.button:active {
transform: translateY(1px);
}
@@ -1,7 +1,5 @@
"use client"; "use client";
import styles from "./subscription-payment-success-dialog.module.css";
export interface SubscriptionPaymentSuccessDialogProps { export interface SubscriptionPaymentSuccessDialogProps {
open: boolean; open: boolean;
subscriptionType: "vip" | "topup"; subscriptionType: "vip" | "topup";
@@ -22,20 +20,32 @@ export function SubscriptionPaymentSuccessDialog({
return ( return (
<div <div
className={styles.overlay} className="fixed inset-0 z-80 flex items-center justify-center bg-[rgba(0,0,0,0.45)] pb-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-bottom,0px))] pl-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-left,0px))] pr-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-right,0px))] pt-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-top,0px))]"
role="alertdialog" role="alertdialog"
aria-modal="true" aria-modal="true"
aria-labelledby="subscription-payment-success-title" aria-labelledby="subscription-payment-success-title"
> >
<div className={styles.dialog}> <div className="w-full max-w-(--dialog-max-width,380px) rounded-(--responsive-card-radius,32px) bg-(--color-page-background,#ffffff) px-(--responsive-card-padding,20px) pb-(--responsive-card-padding,20px) pt-(--responsive-card-padding-lg,28px) text-center shadow-[0_18px_40px_rgba(0,0,0,0.16)]">
<div className={styles.badge} aria-hidden="true"> <div
className="mb-(--page-section-gap,16px) inline-flex size-[clamp(52px,10.741vw,58px)] items-center justify-center rounded-full bg-[linear-gradient(135deg,#ff67e0_0%,#f657a0_100%)] text-[clamp(28px,5.926vw,32px)] font-bold leading-none text-white shadow-[0_8px_18px_rgba(247,89,168,0.28)]"
aria-hidden="true"
>
</div> </div>
<h2 id="subscription-payment-success-title" className={styles.title}> <h2
id="subscription-payment-success-title"
className="m-0 mb-[clamp(7px,1.481vw,8px)] text-(length:--responsive-page-title,22px) font-bold leading-[1.2] text-(--color-text-foreground,#171717)"
>
Payment successful Payment successful
</h2> </h2>
<p className={styles.content}>{content}</p> <p className="m-0 text-(length:--responsive-body,14px) leading-[1.5] text-[#393939]">
<button type="button" className={styles.button} onClick={onClose}> {content}
</p>
<button
type="button"
className="mt-(--page-section-gap-lg,22px) min-h-(--responsive-control-height,48px) w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(269deg,#ff67e0_0%,rgba(254,104,224,0.5)_20%,rgba(252,105,223,0.79)_66%,#f96ade_100%),linear-gradient(#f657a0,#f657a0)] text-(length:--responsive-body,16px) font-bold text-white shadow-[0_5px_7px_rgba(247,89,168,0.31)] active:translate-y-px"
onClick={onClose}
>
OK OK
</button> </button>
</div> </div>