Rework project rewards into smaller components

This commit is contained in:
2025-09-15 17:11:41 +02:00
parent 429c2c0c81
commit ca5d6837d1
4 changed files with 302 additions and 25 deletions
+169
View File
@@ -0,0 +1,169 @@
<script lang="ts">
import { Modal } from "flowbite-svelte";
import type { Project, ProjectReward } from "../openapi/client";
import { t } from "../i18n/store";
import { formatCurrency, getUnit } from "../utils/currencies";
import UnitIcon from "../svgs/UnitIcon.svelte";
import UserIcon from "../svgs/UserIcon.svelte";
import { extractId } from "../utils/extractId";
import { cart } from "../stores/cart";
import { renderMarkdown } from "../utils/renderMarkdown";
import { onMount } from "svelte";
let {
open = $bindable(false),
reward = $bindable(),
project,
}: {
open: boolean;
reward: ProjectReward;
project: Project;
} = $props();
let rawInput = $state("");
let customAmount = $state(0);
let link = $state(`/calculadora-fiscal`);
async function updateAmount(action: "close" | "checkout") {
const numericAmount = customAmount;
if (
isNaN(numericAmount) ||
numericAmount * getUnit(reward.money?.currency) < (reward.money?.amount ?? 0)
) {
alert($t("rewards.error-invalid-amount"));
return;
}
const target = Number(extractId(project.accounting));
cart.addItem({
title: reward.title,
amount: numericAmount * getUnit(reward.money?.currency),
quantity: 1,
image: "",
project: Number(extractId(reward.project)),
target,
claimed: reward.unitsTotal! - reward.unitsAvailable!,
currency: reward.money?.currency,
});
if (action === "checkout") {
window.location.href = "/checkout";
} else {
open = false;
}
}
onMount(() => {
rawInput = formatCurrency(reward.money.amount, reward.money.currency);
customAmount = +formatCurrency(reward.money.amount, reward.money.currency, { asLocaleString: false });
});
</script>
<Modal
bind:open
closeBtnClass="top-7 end-7 bg-transparent text-[#462949] hover:bg-transparent hover:text-[#462949] hover:scale-110 transition-transform duration-200 transform focus:ring-0 shadow-none dark:text-[#462949] dark:hover:text-[#462949] dark:hover:bg-transparent"
class="fixed top-1/2 left-1/2 w-full max-w-[900px] -translate-x-1/2 -translate-y-1/2 rounded-3xl bg-white p-6 shadow-lg backdrop:bg-[#878282B2] backdrop:backdrop-blur-[5px]"
headerClass="py-2"
>
<div class="flex flex-col gap-6">
<div class="text-tertiary flex w-full flex-col gap-6 text-left text-[40px] font-semibold">
<h3>
{@html $t(
"rewards.by-amount-or-more",
{
amount: `${formatCurrency(reward.money.amount, reward.money.currency)}`,
},
{ allowHTML: true },
)}
</h3>
<h3>{$t("rewards.thanks-message")}</h3>
</div>
<div class="flex flex-row gap-4">
<div class=" flex min-w-0 flex-1 flex-col gap-2">
<p class="text-gray-700">
{#await renderMarkdown(reward.description!) then content}
{@html content}
{/await}
</p>
<p class="mb-8">
{@html $t(
"rewards.reward-iva-message",
{
link: `<a class="font-bold" href="${window.location.origin}${link}" target="_blank">${window.location.origin}${link}</a>`,
},
{ allowHTML: true },
)}
</p>
<div>
<input
type="text"
class="focus-ring-2 focus:ring-tertiary w-full rounded border border-gray-300 p-4"
bind:value={rawInput}
onfocus={() => {
rawInput = customAmount.toString();
}}
onblur={() => {
const currency = reward?.money?.currency!;
const unit = getUnit(currency);
const parsed = parseFloat(
rawInput.replace(/[^\d.,]/g, "").replace(",", "."),
);
customAmount = isNaN(parsed) ? 0 : parsed;
rawInput =
customAmount > 0
? formatCurrency(customAmount * unit, currency)
: "";
}}
placeholder={$t("rewards.donation-free.placeholder")}
/>
</div>
</div>
</div>
<div class="flex w-full gap-2">
<div class="text-tertiary flex items-center justify-between gap-2 text-sm font-bold">
<UserIcon />
<span>
{@html $t(
"rewards.donators",
{ donators: reward.unitsClaimed! },
{ allowHTML: true },
)}
</span>
</div>
{#if reward.isFinite}
<div
class="text-tertiary flex items-center justify-between gap-2 text-sm font-bold"
>
<UnitIcon />
<span>
{@html $t(
"rewards.units-available",
{ units: reward.unitsAvailable! },
{ allowHTML: true },
)}
</span>
</div>
{/if}
</div>
<div class="flex flex-row gap-4">
<button
type="button"
onclick={() => updateAmount("close")}
class="text-tertiary border-tertiary inline-block w-full cursor-pointer rounded-3xl border bg-[#FFF] px-6 py-4 font-bold transition"
>
{$t("rewards.reward-donate-close")}
</button>
<button
type="button"
onclick={() => updateAmount("checkout")}
class="text-tertiary bg-primary bg inline-block w-full cursor-pointer rounded-3xl px-6 py-4 font-bold transition"
>
{$t("rewards.donation-free.btn")}
</button>
</div>
</div>
</Modal>
+50
View File
@@ -0,0 +1,50 @@
<script lang="ts">
import { t } from "../i18n/store";
import type { Project, ProjectReward } from "../openapi/client";
import { formatCurrency } from "../utils/currencies";
import { renderMarkdown } from "../utils/renderMarkdown";
import RewardModal from "./RewardModal.svelte";
let {
reward = $bindable(),
project,
}: {
reward: ProjectReward;
project: Project;
} = $props();
let openModal = $state(false);
</script>
<li
class="flex basis-1/3 flex-col items-center justify-between gap-8 rounded-4xl border border-[#F3F3EF] bg-[#FFF] p-6 shadow-[0px_1px_3px_0px_#0000001A]"
class:opacity-50={reward.isFinite && (reward.unitsAvailable ?? 0) === 0}
class:cursor-not-allowed={reward.isFinite && (reward.unitsAvailable ?? 0) === 0}
>
<div class="flex flex-col gap-4">
<h3 class="text-tertiary line-clamp-2 w-full text-left text-2xl font-semibold">
{reward.title}
</h3>
{#if reward.description}
<div class="line-clamp-7 text-sm whitespace-pre-line text-gray-800">
{#await renderMarkdown(reward.description) then description}
{@html description}
{/await}
</div>
{/if}
</div>
<button
type="button"
onclick={() => (openModal = true)}
disabled={reward.isFinite && (reward.unitsAvailable ?? 0) === 0}
class:cursor-not-allowed={reward.isFinite && (reward.unitsAvailable ?? 0) === 0}
class:cursor-pointer={!reward.isFinite || (reward.unitsAvailable ?? 0) > 0}
class="text-tertiary inline-block w-full rounded-3xl bg-[#E6E5F7] px-6 py-4 font-bold transition"
>
{$t("reward.donate")}
{formatCurrency(reward.money.amount, reward.money.currency)}
</button>
</li>
<RewardModal {reward} {project} bind:open={openModal} />
+50 -25
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import { onMount } from "svelte";
import type {
Project,
Accounting,
ApiAccountingBalancePointsGetCollectionData,
AccountingBalance,
ProjectSupport,
import {
type Project,
type Accounting,
type ApiAccountingBalancePointsGetCollectionData,
type AccountingBalance,
apiProjectsIdOrSlugGet,
} from "../../openapi/client/index";
import Tags from "../Tags.svelte";
import { getTerritoryTag } from "../../utils/getTerritoryTag";
@@ -13,28 +13,36 @@
import LanguagesDropdown from "../LanguagesDropdown.svelte";
import Sharebutton from "./Sharebutton.svelte";
import { languagesList } from "../../i18n/locales";
import Tabs from "./Tabs.svelte";
import Card from "./Card.svelte";
import Player from "../Player/Player.svelte";
import Rewards from "../Rewards.svelte";
import Banner from "./Banner.svelte";
import { t } from "../../i18n/store";
import { locale, t } from "../../i18n/store";
import ArrowRightIcon from "../../svgs/ArrowRightIcon.svelte";
import RememberIcon from "../../svgs/RememberIcon.svelte";
import { getDefaultLanguage } from "../../utils/consts";
import type { Locale } from "../../i18n/locales";
import TopRewards from "./TopRewards.svelte";
export let project: Project;
export let accounting: Accounting;
export let accountingBalance: AccountingBalance;
export let ownerName: string;
export let balancePoints: ApiAccountingBalancePointsGetCollectionData;
export let totalSupports: number = 0;
let {
lang = $bindable(),
project,
accounting,
accountingBalance,
ownerName,
balancePoints,
}: {
lang: string;
project: Project;
accounting: Accounting;
accountingBalance: AccountingBalance;
ownerName: string;
balancePoints: ApiAccountingBalancePointsGetCollectionData;
} = $props();
let poster = { src: project.video?.thumbnail || "", alt: "Miniatura del video" };
const limit = 3;
let showFull = false;
const languages = project?.locales as (keyof typeof languagesList)[];
let showFull = $state(false);
const tags = {
categoryTag: project.category,
@@ -42,9 +50,22 @@
};
let paragraphRef: HTMLParagraphElement;
let showToggle = false;
let showToggle = $state(false);
const countdownEnd = project.calendar?.optimum ? new Date(project.calendar.optimum) : undefined;
async function getProjectData(code?: string) {
lang = code ? code : getDefaultLanguage();
locale.set(code as Locale);
const { data } = await apiProjectsIdOrSlugGet({
path: { idOrSlug: project?.id!.toString() },
headers: { "Accept-Language": lang },
});
project = data!;
}
onMount(() => {
const twoLinesHeight = parseFloat(getComputedStyle(paragraphRef).lineHeight) * 2;
if (paragraphRef.scrollHeight > twoLinesHeight + 1) {
@@ -62,7 +83,7 @@
<span class="text-tertiary font-bold underline"> {ownerName}</span>
</h3>
<h1 class="text-3xl font-bold text-[#575757] lg:text-4xl">
{project.title} <span>{ownerName}</span>
{project.title}
</h1>
</div>
@@ -80,7 +101,7 @@
<button
type="button"
class="mt-2 text-sm font-medium text-blue-600 hover:underline"
on:click={() => (showFull = !showFull)}
onclick={() => (showFull = !showFull)}
>
{showFull ? "Ver menos" : "Ver más"}
</button>
@@ -90,7 +111,11 @@
<div class="flex w-full flex-col gap-4 lg:w-[30%] lg:justify-between">
<div class="flex justify-end">
<LanguagesDropdown {languages} />
<LanguagesDropdown
{lang}
languages={project.locales!}
select={(lang: string) => getProjectData(lang)}
/>
</div>
<div class="hidden lg:block">
@@ -138,13 +163,13 @@
<ArrowRightIcon />{$t("reward.showAll")}
</button>
</div>
<Rewards {project} {limit} />
<TopRewards bind:lang {project} />
<button
class="text-tertiary flex cursor-pointer items-center gap-4 rounded-3xl bg-[#E6E5F7] px-6 py-4 font-bold transition lg:hidden justify-center"
class="text-tertiary flex cursor-pointer items-center justify-center gap-4 rounded-3xl bg-[#E6E5F7] px-6 py-4 font-bold transition lg:hidden"
>
<ArrowRightIcon />{$t("reward.showAll")}
</button>
</div>
<Banner {ownerName} />
</section>
<Tabs {project} {accounting} {accountingBalance} />
<Tabs bind:project {accounting} {accountingBalance} />
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts">
import {
apiProjectRewardsGetCollection,
type Project,
type ProjectReward,
} from "../../openapi/client";
import TopReward from "../TopReward.svelte";
let {
lang = $bindable(),
project,
}: {
lang: string;
project: Project;
} = $props();
let rewards: ProjectReward[] = $state([]);
$effect(() => {
apiProjectRewardsGetCollection({
query: { project: project.id!.toString(), itemsPerPage: 3 },
headers: { "Accept-Language": lang },
}).then((data) => {
rewards = data.data!;
});
});
</script>
<ul class="flex flex-col gap-6 lg:flex-row">
{#each rewards as reward}
<TopReward {reward} {project} />
{/each}
</ul>