Rework project rewards into smaller components
This commit is contained in:
@@ -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>
|
||||
@@ -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} />
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user