Skip to content

Commit 6ba27f8

Browse files
committed
web/DonateOptionsCard: add scroll buttons to the options container
cuz users without touchpads couldn't scroll it without tabbing
1 parent 5a4be48 commit 6ba27f8

File tree

1 file changed

+159
-16
lines changed

1 file changed

+159
-16
lines changed

web/src/components/donate/DonateOptionsCard.svelte

+159-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script lang="ts">
2+
import settings from "$lib/state/settings";
3+
24
import { donate } from "$lib/env";
5+
import { device } from "$lib/device";
36
import { t } from "$lib/i18n/translations";
47
58
import DonateCardContainer from "$components/donate/DonateCardContainer.svelte";
@@ -11,7 +14,6 @@
1114
import IconPizza from "@tabler/icons-svelte/IconPizza.svelte";
1215
import IconToolsKitchen2 from "@tabler/icons-svelte/IconToolsKitchen2.svelte";
1316
import IconPaperBag from "@tabler/icons-svelte/IconPaperBag.svelte";
14-
import IconArrowRight from "@tabler/icons-svelte/IconArrowRight.svelte";
1517
import IconSoup from "@tabler/icons-svelte/IconSoup.svelte";
1618
import IconFridge from "@tabler/icons-svelte/IconFridge.svelte";
1719
import IconArmchair from "@tabler/icons-svelte/IconArmchair.svelte";
@@ -20,7 +22,11 @@
2022
import IconPhoto from "@tabler/icons-svelte/IconPhoto.svelte";
2123
import IconWorldWww from "@tabler/icons-svelte/IconWorldWww.svelte";
2224
import IconBath from "@tabler/icons-svelte/IconBath.svelte";
23-
import OuterLink from "$components/misc/OuterLink.svelte";
25+
26+
import IconArrowLeft from "@tabler/icons-svelte/IconArrowLeft.svelte";
27+
import IconArrowRight from "@tabler/icons-svelte/IconArrowRight.svelte";
28+
29+
let donateList: HTMLElement;
2430
2531
let customInput: HTMLInputElement;
2632
let customInputValue: number | null;
@@ -39,7 +45,7 @@
3945
4900: IconApple,
4046
7398: IconDeviceLaptop,
4147
8629: IconPhoto,
42-
9433: IconBath
48+
9433: IconBath,
4349
};
4450
4551
type Processor = "stripe" | "liberapay";
@@ -66,7 +72,30 @@
6672
}
6773
6874
const amount = Math.floor(Number(customInputValue) * 100);
69-
return window.open(donationMethods[processor](amount), '_blank');
75+
return window.open(donationMethods[processor](amount), "_blank");
76+
};
77+
78+
const scrollBehavior = $settings.appearance.reduceMotion
79+
? "instant"
80+
: "smooth";
81+
82+
$: showLeftScroll = false;
83+
$: showRightScroll = true;
84+
85+
const scroll = (direction: "left" | "right") => {
86+
const currentPos = donateList.scrollLeft;
87+
const newPos = direction === "left" ? currentPos - 150 : currentPos + 150;
88+
const maxPos = donateList.scrollWidth - donateList.getBoundingClientRect().width;
89+
90+
donateList.scroll({
91+
left: newPos,
92+
behavior: scrollBehavior,
93+
});
94+
95+
setTimeout(() => {
96+
showLeftScroll = newPos > 0;
97+
showRightScroll = newPos < maxPos && newPos !== maxPos;
98+
}, 150)
7099
};
71100
</script>
72101

@@ -84,10 +113,11 @@
84113
<div class="donation-type-text">
85114
<div class="donate-card-title">{$t("donate.card.once")}</div>
86115
<div class="donate-card-subtitle">
87-
{$t("donate.card.processor", { value: 'stripe' })}
116+
{$t("donate.card.processor", { value: "stripe" })}
88117
</div>
89118
</div>
90119
</button>
120+
91121
<button
92122
id="donation-type-monthly"
93123
class="donation-type"
@@ -100,27 +130,65 @@
100130
<div class="donation-type-text">
101131
<div class="donate-card-title">{$t("donate.card.monthly")}</div>
102132
<div class="donate-card-subtitle">
103-
{$t("donate.card.processor", { value: 'liberapay' })}
133+
{$t("donate.card.processor", { value: "liberapay" })}
104134
</div>
105135
</div>
106136
</button>
107137
</div>
108-
<div id="donation-options">
109-
{#each Object.entries(PRESET_DONATION_AMOUNTS) as [ amount, component ]}
110-
<DonationOption
111-
price={+amount}
112-
desc={$t(`donate.card.option.${amount}`)}
113-
href={donationMethods[processor](+amount * 100)}
114-
>
115-
<svelte:component this={component} />
116-
</DonationOption>
117-
{/each}
138+
139+
<div
140+
id="donation-options-container"
141+
aria-hidden="true"
142+
class:mask-both={!device.is.mobile && showLeftScroll && showRightScroll}
143+
class:mask-left={!device.is.mobile && showLeftScroll && !showRightScroll}
144+
class:mask-right={!device.is.mobile && showRightScroll && !showLeftScroll}
145+
>
146+
{#if !device.is.mobile}
147+
<div id="donation-options-scroll">
148+
<button
149+
class="scroll-button left"
150+
class:hidden={!showLeftScroll}
151+
on:click={() => {
152+
scroll("left");
153+
}}
154+
>
155+
<IconArrowLeft />
156+
</button>
157+
<button
158+
class="scroll-button right"
159+
class:hidden={!showRightScroll}
160+
on:click={() => {
161+
scroll("right");
162+
}}
163+
>
164+
<IconArrowRight />
165+
</button>
166+
</div>
167+
{/if}
168+
169+
<div
170+
id="donation-options"
171+
bind:this={donateList}
172+
on:scroll={(e) => {}}
173+
>
174+
{#each Object.entries(PRESET_DONATION_AMOUNTS) as [amount, component]}
175+
<DonationOption
176+
price={+amount}
177+
desc={$t(`donate.card.option.${amount}`)}
178+
href={donationMethods[processor](+amount * 100)}
179+
>
180+
<svelte:component this={component} />
181+
</DonationOption>
182+
{/each}
183+
</div>
118184
</div>
185+
119186
<div id="donation-custom">
120187
<div id="input-container" class:focused={customFocused}>
121188
{#if customInputValue || customInput?.validity.badInput}
122189
<span id="input-dollar-sign">$</span>
123190
{/if}
191+
124192
<input
125193
id="donation-custom-input"
126194
type="number"
@@ -137,6 +205,7 @@
137205
on:keydown={(e) => e.key === "Enter" && sendCustom()}
138206
/>
139207
</div>
208+
140209
<button
141210
id="donation-custom-submit"
142211
on:click={sendCustom}
@@ -146,6 +215,7 @@
146215
<IconArrowRight />
147216
</button>
148217
</div>
218+
149219
<div class="donate-card-subtitle processor-mobile">
150220
{$t("donate.card.processor", { value: processor })}
151221
</div>
@@ -288,6 +358,79 @@
288358
text-align: center;
289359
}
290360
361+
#donation-options-container {
362+
display: flex;
363+
flex-direction: column;
364+
gap: calc(var(--donate-card-main-padding) / 2);
365+
position: relative;
366+
}
367+
368+
#donation-options-scroll {
369+
display: flex;
370+
flex-direction: row;
371+
justify-content: space-between;
372+
align-items: center;
373+
position: absolute;
374+
pointer-events: none;
375+
width: 100%;
376+
height: 100%;
377+
z-index: 3;
378+
overflow: hidden;
379+
opacity: 0;
380+
transition: opacity 0.2s;
381+
}
382+
383+
.scroll-button {
384+
pointer-events: all;
385+
color: white;
386+
padding: 0 16px;
387+
background-color: transparent;
388+
height: 100%;
389+
transition: opacity 0.2s;
390+
}
391+
392+
#donation-options-container:hover #donation-options-scroll {
393+
opacity: 1;
394+
}
395+
396+
.scroll-button.hidden {
397+
opacity: 0;
398+
visibility: hidden;
399+
}
400+
401+
#donation-options-container.mask-both:hover #donation-options {
402+
mask-image: linear-gradient(
403+
90deg,
404+
rgba(0, 0, 0, 0) 0%,
405+
rgba(0, 0, 0, 1) 20%,
406+
rgba(0, 0, 0, 1) 50%,
407+
rgba(0, 0, 0, 1) 80%,
408+
rgba(0, 0, 0, 0) 100%
409+
);
410+
}
411+
412+
#donation-options-container.mask-left:hover #donation-options {
413+
mask-image: linear-gradient(
414+
90deg,
415+
rgba(0, 0, 0, 0) 0%,
416+
rgba(0, 0, 0, 1) 20%,
417+
rgba(0, 0, 0, 1) 50%,
418+
rgba(0, 0, 0, 1) 96%,
419+
rgba(0, 0, 0, 0) 100%
420+
);
421+
}
422+
423+
#donation-options-container.mask-right:hover #donation-options {
424+
mask-image: linear-gradient(
425+
90deg,
426+
rgba(0, 0, 0, 0) 0%,
427+
rgba(0, 0, 0, 1) 4%,
428+
rgba(0, 0, 0, 1) 50%,
429+
rgba(0, 0, 0, 1) 80%,
430+
rgba(0, 0, 0, 0) 100%
431+
);
432+
}
433+
291434
@media screen and (max-width: 550px) {
292435
:global(#donation-box) {
293436
min-width: unset;

0 commit comments

Comments
 (0)