feat(tip): improve gift layout and background
This commit is contained in:
@@ -209,6 +209,9 @@ describe("TipScreen checkout", () => {
|
|||||||
.querySelector("main")
|
.querySelector("main")
|
||||||
?.getAttribute("style"),
|
?.getAttribute("style"),
|
||||||
).toContain('--tip-cover-image: url("/images/cover/maya.png")');
|
).toContain('--tip-cover-image: url("/images/cover/maya.png")');
|
||||||
|
expect(
|
||||||
|
container.querySelectorAll('main > div[aria-hidden="true"]'),
|
||||||
|
).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@@ -14,42 +14,87 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 88% 8%, rgba(255, 116, 151, 0.14), transparent 28%),
|
radial-gradient(
|
||||||
linear-gradient(180deg, #fff2e8 0%, #fff9f4 58%, #ffffff 100%);
|
circle at 18% 12%,
|
||||||
|
rgba(255, 181, 105, 0.34),
|
||||||
|
transparent 32%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 86% 24%,
|
||||||
|
rgba(255, 85, 139, 0.2),
|
||||||
|
transparent 30%
|
||||||
|
),
|
||||||
|
linear-gradient(180deg, #fff2e8 0%, #fff8f2 46%, #ffffff 100%);
|
||||||
color: #25191b;
|
color: #25191b;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bgImage {
|
.bgImage,
|
||||||
|
.bgGlowOne,
|
||||||
|
.bgGlowTwo {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgImage {
|
||||||
inset: 0 0 auto;
|
inset: 0 0 auto;
|
||||||
height: clamp(230px, 42vh, 330px);
|
height: clamp(230px, 42vh, 330px);
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(255, 242, 232, 0.18) 0%,
|
rgba(255, 242, 232, 0.3) 0%,
|
||||||
rgba(255, 246, 239, 0.5) 62%,
|
rgba(255, 242, 232, 0.64) 64%,
|
||||||
#fff9f4 100%
|
#fff2e8 96%
|
||||||
),
|
),
|
||||||
var(--tip-cover-image) center 20% / cover no-repeat;
|
var(--tip-cover-image) center 18% / cover no-repeat;
|
||||||
filter: saturate(0.9);
|
filter: saturate(0.95) blur(0.2px);
|
||||||
opacity: 0.48;
|
opacity: 0.36;
|
||||||
pointer-events: none;
|
}
|
||||||
|
|
||||||
|
.bgGlowOne,
|
||||||
|
.bgGlowTwo {
|
||||||
|
border-radius: 999px;
|
||||||
|
filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgGlowOne {
|
||||||
|
top: 192px;
|
||||||
|
right: -86px;
|
||||||
|
width: 190px;
|
||||||
|
height: 190px;
|
||||||
|
background: rgba(255, 92, 142, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgGlowTwo {
|
||||||
|
bottom: 108px;
|
||||||
|
left: -98px;
|
||||||
|
width: 210px;
|
||||||
|
height: 210px;
|
||||||
|
background: rgba(255, 181, 104, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: grid;
|
display: flex;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
grid-template-columns: 42px minmax(0, 1fr) 42px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
}
|
||||||
|
|
||||||
|
.contentFlow {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerIdentity {
|
.headerIdentity {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -82,12 +127,6 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerBalance {
|
|
||||||
display: block;
|
|
||||||
width: 42px;
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supportPrompt {
|
.supportPrompt {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -119,7 +158,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1 1 auto;
|
flex: 0 1 auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
@@ -128,7 +167,7 @@
|
|||||||
.productCard {
|
.productCard {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
grid-template-columns: clamp(98px, 28vw, 118px) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -143,9 +182,10 @@
|
|||||||
|
|
||||||
.coffeeStage {
|
.coffeeStage {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: clamp(136px, 40vw, 176px);
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: #c59b7d;
|
background: #c59b7d;
|
||||||
@@ -341,6 +381,7 @@
|
|||||||
|
|
||||||
.productSkeleton {
|
.productSkeleton {
|
||||||
min-height: 174px;
|
min-height: 174px;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeletonImage,
|
.skeletonImage,
|
||||||
@@ -356,8 +397,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.skeletonImage {
|
.skeletonImage {
|
||||||
width: 100%;
|
width: clamp(136px, 40vw, 176px);
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
justify-self: center;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +420,6 @@
|
|||||||
|
|
||||||
.checkoutSlot {
|
.checkoutSlot {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-top: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkoutButton {
|
.checkoutButton {
|
||||||
@@ -428,8 +469,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
grid-template-columns: 40px minmax(0, 1fr) 40px;
|
min-height: 40px;
|
||||||
gap: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerIdentity {
|
.headerIdentity {
|
||||||
@@ -442,7 +482,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.productCard {
|
.productCard {
|
||||||
grid-template-columns: 82px minmax(0, 1fr);
|
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
}
|
}
|
||||||
@@ -497,10 +536,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.productCard {
|
.productCard {
|
||||||
grid-template-columns: 92px minmax(0, 1fr);
|
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coffeeStage,
|
||||||
|
.skeletonImage {
|
||||||
|
width: 128px;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
.tierList {
|
.tierList {
|
||||||
max-height: 148px;
|
max-height: 148px;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|||||||
+90
-84
@@ -210,6 +210,8 @@ export function TipScreen({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.bgImage} aria-hidden="true" />
|
<div className={styles.bgImage} aria-hidden="true" />
|
||||||
|
<div className={styles.bgGlowOne} aria-hidden="true" />
|
||||||
|
<div className={styles.bgGlowTwo} aria-hidden="true" />
|
||||||
|
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<BackButton
|
<BackButton
|
||||||
@@ -218,6 +220,9 @@ export function TipScreen({
|
|||||||
ariaLabel="Back to character home"
|
ariaLabel="Back to character home"
|
||||||
analyticsKey="tip.back_to_splash"
|
analyticsKey="tip.back_to_splash"
|
||||||
/>
|
/>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.contentFlow}>
|
||||||
<div className={styles.headerIdentity}>
|
<div className={styles.headerIdentity}>
|
||||||
<div className={styles.headerAvatar}>
|
<div className={styles.headerAvatar}>
|
||||||
<CharacterAvatar
|
<CharacterAvatar
|
||||||
@@ -232,97 +237,98 @@ export function TipScreen({
|
|||||||
{character.copy.tipTitle}
|
{character.copy.tipTitle}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.headerBalance} aria-hidden="true" />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className={`${styles.supportPrompt} ${
|
className={`${styles.supportPrompt} ${
|
||||||
supportPrompt.isReady ? styles.supportPromptReady : ""
|
supportPrompt.isReady ? styles.supportPromptReady : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{supportPrompt.prompt}
|
{supportPrompt.prompt}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={styles.purchaseFlow}>
|
<div className={styles.purchaseFlow}>
|
||||||
{payment.isLoadingPlans ? (
|
{payment.isLoadingPlans ? (
|
||||||
<section
|
|
||||||
className={`${styles.productCard} ${styles.productSkeleton}`}
|
|
||||||
aria-label="Loading gifts"
|
|
||||||
aria-busy="true"
|
|
||||||
>
|
|
||||||
<div className={styles.skeletonImage} />
|
|
||||||
<div className={styles.skeletonList}>
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : selectedProduct ? (
|
|
||||||
<>
|
|
||||||
<section
|
<section
|
||||||
className={styles.productCard}
|
className={`${styles.productCard} ${styles.productSkeleton}`}
|
||||||
aria-label="Gift products"
|
aria-label="Loading gifts"
|
||||||
|
aria-busy="true"
|
||||||
>
|
>
|
||||||
<div className={styles.coffeeStage}>
|
<div className={styles.skeletonImage} />
|
||||||
<TipProductImage
|
<div className={styles.skeletonList}>
|
||||||
key={selectedProduct.planId}
|
<span />
|
||||||
sources={getGiftImageSources(
|
<span />
|
||||||
selectedProduct,
|
<span />
|
||||||
selectedCategory,
|
</div>
|
||||||
)}
|
</section>
|
||||||
alt={selectedProduct.planName}
|
) : selectedProduct ? (
|
||||||
className={styles.coffeeImage}
|
<>
|
||||||
priority
|
<section
|
||||||
|
className={styles.productCard}
|
||||||
|
aria-label="Gift products"
|
||||||
|
>
|
||||||
|
<div className={styles.coffeeStage}>
|
||||||
|
<TipProductImage
|
||||||
|
key={selectedProduct.planId}
|
||||||
|
sources={getGiftImageSources(
|
||||||
|
selectedProduct,
|
||||||
|
selectedCategory,
|
||||||
|
)}
|
||||||
|
alt={selectedProduct.planName}
|
||||||
|
className={styles.coffeeImage}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TipGiftProductSelector
|
||||||
|
disabled={isSelectionDisabled}
|
||||||
|
products={visibleProducts}
|
||||||
|
onChange={handleProductChange}
|
||||||
|
selectedPlanId={payment.selectedPlanId}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<PaymentMethodSelector
|
||||||
|
config={paymentMethodConfig}
|
||||||
|
value={payment.payChannel}
|
||||||
|
density="compact"
|
||||||
|
disabled={isPaymentBusy}
|
||||||
|
className={styles.paymentMethodSlot}
|
||||||
|
analyticsKey="tip.payment_method"
|
||||||
|
onChange={handlePaymentMethodChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.checkoutSlot}>
|
||||||
|
<TipCheckoutButton
|
||||||
|
giftCategory={payment.selectedGiftCategory}
|
||||||
|
giftPlanId={payment.selectedPlanId || null}
|
||||||
|
disabled={!canCreateOrder}
|
||||||
|
onOrder={handleOrder}
|
||||||
|
returnPath={returnPath}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<TipGiftProductSelector
|
) : showCatalogError || showEmptyCatalog ? (
|
||||||
disabled={isSelectionDisabled}
|
<section className={styles.catalogStatus} role="status">
|
||||||
products={visibleProducts}
|
<p>
|
||||||
onChange={handleProductChange}
|
{showCatalogError
|
||||||
selectedPlanId={payment.selectedPlanId}
|
? "We could not load gifts for this character."
|
||||||
/>
|
: "This character does not have any gifts available yet."}
|
||||||
|
</p>
|
||||||
|
{showCatalogError ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
paymentDispatch({
|
||||||
|
type: "PaymentCatalogRetryRequested",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
) : null}
|
||||||
<PaymentMethodSelector
|
</div>
|
||||||
config={paymentMethodConfig}
|
|
||||||
value={payment.payChannel}
|
|
||||||
density="compact"
|
|
||||||
disabled={isPaymentBusy}
|
|
||||||
className={styles.paymentMethodSlot}
|
|
||||||
analyticsKey="tip.payment_method"
|
|
||||||
onChange={handlePaymentMethodChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.checkoutSlot}>
|
|
||||||
<TipCheckoutButton
|
|
||||||
giftCategory={payment.selectedGiftCategory}
|
|
||||||
giftPlanId={payment.selectedPlanId || null}
|
|
||||||
disabled={!canCreateOrder}
|
|
||||||
onOrder={handleOrder}
|
|
||||||
returnPath={returnPath}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : showCatalogError || showEmptyCatalog ? (
|
|
||||||
<section className={styles.catalogStatus} role="status">
|
|
||||||
<p>
|
|
||||||
{showCatalogError
|
|
||||||
? "We could not load gifts for this character."
|
|
||||||
: "This character does not have any gifts available yet."}
|
|
||||||
</p>
|
|
||||||
{showCatalogError ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
paymentDispatch({ type: "PaymentCatalogRetryRequested" })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
|
|||||||
Reference in New Issue
Block a user