Merge branch 'feat/admin-newsletter-design' into feat/unified-admin-pages-layout

This commit is contained in:
2026-08-17 12:52:12 +02:00
30 changed files with 772 additions and 386 deletions
+9
View File
@@ -0,0 +1,9 @@
<script lang="ts">
import { clearForUser } from "../../stores/checkoutsStore";
let { userId }: { userId: number } = $props();
$effect(() => {
clearForUser(userId);
});
</script>
@@ -0,0 +1,85 @@
<script lang="ts">
import CommentModal from "./CommentModal.svelte";
import { t } from "../../i18n/store";
import {
apiProjectSupportsIdPatch,
type Project,
type ProjectSupport,
} from "../../openapi/client";
import { formatCurrency } from "../../utils/currencies";
import EditIcon from "../icons/actions/Edit.svelte";
import AnnotationIcon from "../icons/status/AnnotationIcon.svelte";
import Button from "../library/buttons/Button.svelte";
import Checkbox from "../library/inputs/Checkbox.svelte";
interface Props {
project: Project;
support: ProjectSupport;
onUpdate?: (support: ProjectSupport) => void;
}
let { project, support, onUpdate }: Props = $props();
let open = $state(false);
let isAnonymous = $state(false);
$effect(() => {
apiProjectSupportsIdPatch({
baseUrl: "/api/relay",
path: { id: String(support.id!) },
headers: {
"Content-Type": "application/merge-patch+json",
},
body: {
anonymous: isAnonymous,
},
});
});
</script>
<article class="border-grey w-full items-center rounded-4xl border bg-white p-6 shadow/10">
<div class="grid w-full grid-cols-2 gap-6">
<div class="border-grey overflow-hidden rounded-3xl border">
<img
src={project.video?.thumbnail}
alt={project.title}
class="h-full w-full object-cover"
/>
</div>
<div class="flex flex-col justify-between">
<div class="flex flex-col gap-1">
<span class="text-base/6 font-bold text-black"
>{$t("pages.checkout.verify.approved.formReview.donated")}</span
>
<h3 class="text-secondary text-2xl/8 font-bold">
{formatCurrency(support.money)}
</h3>
</div>
<Button onclick={() => (open = true)} size="md" kind="secondary">
{#if support?.message}
<EditIcon class="size-3.75" />
{$t("pages.checkout.verify.approved.formReview.commentBtn.editComment")}
{:else}
<AnnotationIcon class="size-3.75" />
{$t("pages.checkout.verify.approved.formReview.commentBtn.leaveComment")}
{/if}
</Button>
</div>
</div>
</article>
<Checkbox
bind:checked={isAnonymous}
label={$t("pages.checkout.verify.approved.formReview.anonymous")}
class="gap-2"
/>
<CommentModal
bind:open
bind:isAnonymous
{support}
onSubmit={(message, anonymous) => {
onUpdate?.({
...support,
message,
anonymous,
});
}}
/>
@@ -0,0 +1,75 @@
<script lang="ts">
import { Modal } from "flowbite-svelte";
import { t } from "../../i18n/store";
import { apiProjectSupportsIdPatch, type ProjectSupport } from "../../openapi/client";
import Button from "../library/buttons/Button.svelte";
import TextArea from "../library/inputs/TextArea.svelte";
interface Props {
open: boolean;
isAnonymous: boolean;
support: ProjectSupport;
onSubmit?: (message: string, anonymous: boolean) => void;
}
let {
open = $bindable(false),
isAnonymous = $bindable(false),
support,
onSubmit,
}: Props = $props();
let message = $state("");
async function handleSubmit(event: SubmitEvent) {
event.preventDefault();
const trimmed = message.trim();
if (!trimmed || trimmed === support?.message) return;
try {
await apiProjectSupportsIdPatch({
baseUrl: "/api/relay",
path: { id: String(support.id!) },
headers: {
"Content-Type": "application/merge-patch+json",
},
body: {
anonymous: isAnonymous,
message: trimmed,
},
});
onSubmit?.(trimmed, isAnonymous);
open = false;
} catch (e) {
console.error("PATCH FAILED", e);
}
}
$effect(() => {
if (open) {
message = support?.message ?? "";
}
});
</script>
<Modal
bind:open
title={$t("pages.checkout.verify.approved.formReview.commentModal.title")}
closeBtnClass="top-7 end-7 bg-transparent text-secondary hover:bg-transparent hover:text-secondary hover:scale-110 transition-transform duration-200 transform focus:ring-0 shadow-none dark:text-secondary dark:hover:text-secondary dark:hover:bg-transparent"
class="fixed top-1/2 left-1/2 w-full max-w-225 -translate-x-1/2 -translate-y-1/2 rounded-3xl bg-white p-6 shadow-lg backdrop:bg-[#878282B2] backdrop:backdrop-blur-[5px]"
headerClass="self-start md:px-0 p-0 text-2xl font-bold text-content text-ellipsis border-none"
bodyClass="p-0 md:p-0 md:pt-1 border-none"
>
<form onsubmit={handleSubmit} class="flex flex-col gap-6">
<TextArea
labelText={$t("pages.checkout.verify.approved.formReview.commentModal.label")}
bind:value={message}
/>
<Button type="submit" class="w-full self-center md:w-fit md:self-end">
{$t("common.save")}
</Button>
</form>
</Modal>
+74 -57
View File
@@ -1,80 +1,97 @@
<script lang="ts">
import CommentCard from "./CommentCard.svelte";
import { t } from "../../i18n/store";
import { type Project, type ProjectSupport } from "../../openapi/client";
import { client } from "../../openapi/client/client.gen";
import { apiProjectsIdOrSlugGetUrl } from "../../openapi/client/paths.gen";
import RadioButton from "../library/inputs/RadioButton.svelte";
import Title from "../library/typography/Title.svelte";
interface Props {
paymentMethod?: string;
projects: Project[];
projectSupports: ProjectSupport[];
}
let { paymentMethod = undefined }: Props = $props();
let { paymentMethod = undefined, projects, projectSupports }: Props = $props();
let messageType = $state("anonymous"); // 'anonymous' o 'public'
let type = $state("organization");
let type = $state<"organization" | "individual">("organization");
let feedbackItems = $derived(() =>
projects.map((project) => {
const iri = client.buildUrl({
url: apiProjectsIdOrSlugGetUrl,
path: { idOrSlug: project.id },
});
const support = projectSupports.find((ps) => ps.project === iri)!;
return { project, support };
}),
);
</script>
<div>
<div class=" sm:w-full sm:max-w-md">
<form id="feedback" method="POST" class="flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center gap-2">
<Title level={2} variant="subsection">
{$t("pages.checkout.verify.approved.formGoal.title")}
</Title>
</div>
<div class="md:w-full md:max-w-167">
<form id="feedback" method="POST" class="flex flex-col gap-4">
<div class="flex w-full max-w-md flex-col gap-4">
<div class="flex flex-row items-center gap-2">
<Title level={2} variant="subsection">
{$t("pages.checkout.verify.approved.formGoal.title")}
</Title>
</div>
<fieldset class="flex flex-col gap-6">
<RadioButton name="type" value="organization" bind:group={type}>
</div>
<fieldset class="flex w-full max-w-md flex-col gap-6">
<RadioButton name="type" value="organization" bind:group={type}>
<div class="text-content text-base/6 font-normal">
{$t("pages.checkout.verify.approved.formGoal.options.2")}
<span class="capitalize">{paymentMethod}</span>
</RadioButton>
<RadioButton name="type" value="individual" bind:group={type}>
{$t("pages.checkout.verify.approved.formGoal.options.1")}
</RadioButton>
</fieldset>
<div class="mt-6 flex flex-col gap-4">
<div class="flex flex-row items-center gap-2">
<Title level={2} variant="subsection">
{$t("pages.checkout.verify.approved.formReview.title")}
</Title>
</div>
</RadioButton>
<RadioButton name="type" value="individual" bind:group={type}>
<div class="text-content flex flex-col text-base/6 font-normal">
{$t("pages.checkout.verify.approved.formGoal.options.1")}
<a href="/wallet" class="text-secondary font-bold underline"
>{$t("common.learnMore")}</a
>
</div>
</RadioButton>
</fieldset>
<div class="mt-6 flex w-full max-w-167 flex-col gap-6">
<div class="flex w-full max-w-md flex-col gap-4">
<Title level={2} variant="subsection">
{$t("pages.checkout.verify.approved.formReview.title")}
</Title>
<p class="text-content">
{$t("pages.checkout.verify.approved.formReview.description")}
</p>
</div>
<fieldset class="flex flex-col gap-6">
<RadioButton
name="messageType"
value="anonymous"
bind:group={messageType}
label="Dejar un mensaje de forma anónima"
/>
<RadioButton
name="messageType"
value="public"
bind:group={messageType}
label="Dejar un mensaje de forma pública"
/>
</fieldset>
<div class="flex w-full flex-col gap-2">
<label for="review-message" class="text-sm font-medium text-gray-700">
Mensaje al impulsor (opcional):
</label>
<textarea
id="review-message"
class="border-secondary focus:ring-secondary w-full appearance-none rounded-md border bg-white p-3 text-base text-gray-700 placeholder-gray-400 focus:ring-1 focus:outline-none"
name="review"
placeholder="Escribe tu mensaje de apoyo al impulsor..."
rows="4"></textarea>
<div class="mb-10 flex flex-col gap-7">
{#each feedbackItems() as { project, support }, index}
<div class="flex flex-col gap-3">
<h1 class="text-lg font-bold text-black">
{$t("pages.checkout.verify.approved.formReview.crowdfundingProject")}
<a
href={`/project/${project.slug}`}
class="text-secondary underline underline-offset-1"
>{project.title}</a
>
</h1>
<CommentCard
{project}
{support}
onUpdate={(updatedSupport) => {
projectSupports = projectSupports.map((s) =>
s.id === updatedSupport.id ? updatedSupport : s,
);
}}
/>
</div>
{/each}
</div>
<!-- TODO: Add Btn "Continuar" -->
</form>
<div id="feedback-error-content" class="mt-4 text-center text-red-500"></div>
</div>
</div>
</form>
<div id="feedback-error-content" class="mt-4 text-center text-red-500"></div>
</div>
+3 -3
View File
@@ -1,8 +1,8 @@
---
import type { ApiGatewaysIdGetResponse } from "../../openapi/client/index";
import CreditCardIcon from "../icons/payment/CreditCardIcon.astro";
import PaypalIcon from "../icons/payment/PaypalIcon.astro";
import StripeIcon from "../icons/payment/StripeIcon.astro";
import CreditCardIcon from "../icons/payment/CreditCardIcon.svelte";
import PaypalIcon from "../icons/payment/PaypalIcon.svelte";
import StripeIcon from "../icons/payment/StripeIcon.svelte";
import { formatCurrency } from "../../utils/currencies";
const { t } = Astro.locals;
@@ -9,7 +9,7 @@
type ProjectProjectCreationDto,
} from "../../openapi/client";
import { client } from "../../openapi/client/client.gen";
import { apiCategoriesIdGetUrl } from "../../openapi/client/paths.gen";
import { apiCategoriesIdOrSlugGetUrl } from "../../openapi/client/paths.gen";
import {
validateCreateForm,
validateField,
@@ -120,7 +120,7 @@
function handleCategoryChange(selected: Category[]) {
const categoryIris = selected.map((s) => {
return client.buildUrl({ url: apiCategoriesIdGetUrl, path: { id: s.id } });
return client.buildUrl({ url: apiCategoriesIdOrSlugGetUrl, path: { idOrSlug: s.id } });
});
handleFieldChange("categories", categoryIris);
@@ -3,5 +3,6 @@
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 2C2.44772 2 2 2.44772 2 3V6H24V3C24 2.44772 23.5523 2 23 2H3ZM26 3C26 1.34315 24.6569 0 23 0H3C1.34315 0 0 1.34315 0 3V19C0 20.6569 1.34315 22 3 22H23C24.6569 22 26 20.6569 26 19V3ZM24 10V19C24 19.5523 23.5523 20 23 20H3C2.44772 20 2 19.5523 2 19V10H24ZM4 17C4 16.4477 4.44772 16 5 16H13C13.5523 16 14 16.4477 14 17C14 17.5523 13.5523 18 13 18H5C4.44772 18 4 17.5523 4 17Z"
fill="currentColor"></path>
fill="currentColor"
></path>
</svg>

Before

Width:  |  Height:  |  Size: 593 B

After

Width:  |  Height:  |  Size: 598 B

@@ -1,11 +1,14 @@
<svg width="26" height="32" viewBox="0 0 26 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.4128 6.39921C10.1964 6.39939 9.98724 6.4779 9.82278 6.62064C9.65833 6.76337 9.54938 6.96098 9.51554 7.17794L8.02472 16.7779C8.09422 16.3296 8.47473 15.9992 8.92195 15.9992H13.2906C17.6873 15.9992 21.4181 12.7428 22.0998 8.32982C22.1505 8.00036 22.1793 7.6678 22.1858 7.3344C21.0685 6.73957 19.7559 6.39921 18.3177 6.39921H10.4128Z"
fill="#001C64"></path>
fill="#001C64"
></path>
<path
d="M22.1858 7.3344C22.1793 7.6678 22.1505 8.00036 22.0998 8.32982C21.4181 12.7428 17.6873 15.9992 13.2906 15.9992H8.92195C8.47473 15.9992 8.09422 16.3296 8.02472 16.7779L5.79524 31.134C5.77848 31.2409 5.78476 31.3502 5.81367 31.4544C5.84257 31.5586 5.89341 31.6552 5.96266 31.7376C6.03191 31.8199 6.11792 31.886 6.21477 31.9313C6.31162 31.9766 6.417 32.0001 6.52364 32H11.2654C11.4818 31.9998 11.691 31.9213 11.8554 31.7786C12.0199 31.6358 12.1288 31.4382 12.1626 31.2213L13.4116 23.1784C13.4455 22.9613 13.5545 22.7636 13.7191 22.6209C13.8837 22.4781 14.0931 22.3997 14.3096 22.3997H17.1014C21.4981 22.3997 25.2288 19.1433 25.9106 14.7303C26.3946 11.5983 24.841 8.74788 22.1858 7.3344Z"
fill="#0070E0"></path>
fill="#0070E0"
></path>
<path
d="M5.02581 0C4.57894 0 4.19808 0.330026 4.12857 0.77802L0.407595 24.7338C0.337037 25.1885 0.683167 25.5998 1.1367 25.5998H6.65461L8.02472 16.7779L9.51554 7.17794C9.54938 6.96098 9.65833 6.76337 9.82278 6.62064C9.98724 6.4779 10.1964 6.39939 10.4128 6.39921H18.3177C19.7559 6.39921 21.0685 6.73957 22.1858 7.3344C22.2623 3.31671 18.997 0 14.5076 0H5.02581Z"
fill="#003087"></path>
fill="#003087"
></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -4,33 +4,39 @@
fill-rule="evenodd"
clip-rule="evenodd"
d="M76.9965 16.5291C76.9965 11.062 74.3483 6.74805 69.2869 6.74805C64.2042 6.74805 61.1289 11.062 61.1289 16.4864C61.1289 22.9146 64.7594 26.1608 69.9703 26.1608C72.5117 26.1608 74.4338 25.5841 75.886 24.7726V20.5014C74.4338 21.2275 72.768 21.676 70.6537 21.676C68.5822 21.676 66.7456 20.9499 66.5106 18.4298H76.9538C76.9538 18.1522 76.9965 17.0417 76.9965 16.5291ZM66.4466 14.5003C66.4466 12.0871 67.9201 11.0833 69.2656 11.0833C70.5683 11.0833 71.9565 12.0871 71.9565 14.5003H66.4466Z"
fill="#635BFF"></path>
fill="#635BFF"
></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M52.8844 6.74805C50.7915 6.74805 49.4461 7.73043 48.6986 8.41382L48.421 7.08974H43.7227V31.991L49.0617 30.8591L49.083 24.8153C49.8519 25.3706 50.9837 26.1608 52.8631 26.1608C56.6858 26.1608 60.1669 23.0855 60.1669 16.3156C60.1455 10.1223 56.6217 6.74805 52.8844 6.74805ZM51.6031 21.4624C50.3431 21.4624 49.5956 21.0139 49.083 20.4587L49.0617 12.5356C49.6169 11.9162 50.3858 11.4891 51.6031 11.4891C53.5465 11.4891 54.8919 13.6674 54.8919 16.4651C54.8919 19.3268 53.5678 21.4624 51.6031 21.4624Z"
fill="#635BFF"></path>
fill="#635BFF"
></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M36.375 5.48852L41.7354 4.33529V0L36.375 1.13187V5.48852Z"
fill="#635BFF"></path>
fill="#635BFF"
></path>
<path d="M41.7354 7.11133H36.375V25.7979H41.7354V7.11133Z" fill="#635BFF"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M30.6304 8.69173L30.2887 7.11137H25.6758V25.798H31.0148V13.1338C32.2748 11.4894 34.4104 11.7884 35.0725 12.0233V7.11137C34.3891 6.8551 31.8904 6.38526 30.6304 8.69173Z"
fill="#635BFF"></path>
fill="#635BFF"
></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M19.9549 2.47754L14.744 3.58806L14.7227 20.6943C14.7227 23.855 17.0932 26.1828 20.2539 26.1828C22.0051 26.1828 23.2865 25.8625 23.9912 25.4781V21.1428C23.3078 21.4204 19.9335 22.4028 19.9335 19.2421V11.6607H23.9912V7.11182H19.9335L19.9549 2.47754Z"
fill="#635BFF"></path>
fill="#635BFF"
></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.51638 12.5356C5.51638 11.7027 6.19978 11.3823 7.33165 11.3823C8.95471 11.3823 11.0049 11.8735 12.628 12.7491V7.73043C10.8554 7.02568 9.10421 6.74805 7.33165 6.74805C2.99636 6.74805 0.113281 9.0118 0.113281 12.7918C0.113281 18.6861 8.22861 17.7464 8.22861 20.2878C8.22861 21.2702 7.37436 21.5905 6.17842 21.5905C4.40586 21.5905 2.14211 20.8644 0.348198 19.8821V24.9648C2.33432 25.8191 4.34179 26.1821 6.17842 26.1821C10.6205 26.1821 13.6744 23.9824 13.6744 20.1597C13.6531 13.7956 5.51638 14.9274 5.51638 12.5356Z"
fill="#635BFF"></path>
fill="#635BFF"
></path>
</g>
<defs>
<clipPath id="clip0_957_1465">

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,25 @@
<script lang="ts">
import { twMerge, type ClassNameValue } from "tailwind-merge";
interface Props {
class?: ClassNameValue;
iconColor?: string;
}
let { class: classes, iconColor }: Props = $props();
let variantColor = $derived(iconColor ? `${iconColor}` : "#462949");
</script>
<svg
class={twMerge("size-4", classes)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
><path
stroke={variantColor}
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4.5 7h2m-2-2h5m-8-2.5v10l2.5-2h7.5a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1"
/></svg
>
@@ -16,7 +16,7 @@
let { group = $bindable(), label, class: classes, children, ...rest }: Props = $props();
</script>
<label for={id} class="flex cursor-pointer items-center gap-2">
<label for={id} class="flex cursor-pointer items-start gap-2">
<div class="w-6">
<input
{id}
+2 -2
View File
@@ -14,7 +14,7 @@
vars={{ owner: `<a href="#" class="font-bold text-secondary">${ownerName}</a>` }}
/>
<a href="/src/pages/[...locale]/about/certificate" class="text-secondary font-bold"
>{$t("pages.project.view.banner.certificate.learnMore")}
>{$t("common.learnMore")}
</a>
</div>
</div>
@@ -24,7 +24,7 @@
<div class="text-content">
{$t("pages.project.view.banner.certificate.description")}
<a href="/src/pages/[...locale]/about/certificate" class="text-secondary font-bold"
>{$t("pages.project.view.banner.certificate.learnMore")}
>{$t("common.learnMore")}
</a>
</div>
</div>
+3 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { locale } from "../../i18n/store";
import { apiCategoriesIdGet, type Category } from "../../openapi/client";
import { apiCategoriesIdOrSlugGet, type Category } from "../../openapi/client";
import { extractId } from "../../utils/extractId";
import BookmarkIcon from "../icons/actions/Bookmark.svelte";
import Tag from "../library/tags/Tag.svelte";
@@ -12,9 +12,9 @@
let { iri }: Props = $props();
async function getCategory(iri: string): Promise<Category> {
const { data: category } = await apiCategoriesIdGet({
const { data: category } = await apiCategoriesIdOrSlugGet({
headers: { "Accept-Language": $locale },
path: { id: extractId(iri)! },
path: { idOrSlug: extractId(iri)! },
});
return category!;
+23 -4
View File
@@ -8,6 +8,7 @@
import { t } from "../../i18n/store";
import { renderMarkdown } from "../../utils/renderMarkdown";
import ArrowSliderIcon from "../icons/navigation/ArrowSliderIcon.svelte";
import Title from "../library/typography/Title.svelte";
import type { Project, Accounting } from "../../openapi/client/index";
@@ -15,11 +16,11 @@
lang = $bindable(),
project = $bindable(),
accounting,
} = $props<{
}: {
lang: string;
project: Project;
accounting: Accounting;
}>();
} = $props();
let activeTab = $state("project");
let tabsContainer: HTMLDivElement;
@@ -27,7 +28,7 @@
let canScrollRight = $state(true);
const tabs = [
{ id: "project", label: $t("pages.project.view.tabs.project") },
{ id: "project", label: $t("pages.project.view.tabs.project.title") },
{ id: "rewards", label: $t("pages.project.view.tabs.rewards") },
{ id: "budget", label: $t("pages.project.view.tabs.budget.title") },
{ id: "updates", label: $t("pages.project.view.tabs.updates.title") },
@@ -131,7 +132,25 @@
class="marked-content flex w-full max-w-4xl flex-col gap-6 overflow-hidden"
style="overflow-wrap: break-word; word-wrap: break-word; word-break: break-word;"
>
{#await renderMarkdown(project.description) then content}
{#await renderMarkdown(project.descBrief!) then content}
{@html content}
{/await}
<Title level={2} variant="section" class="color-secondary font-bold">
{$t("pages.project.view.tabs.project.descAbout")}
</Title>
{#await renderMarkdown(project.descAbout!) then content}
{@html content}
{/await}
<Title level={2} variant="section" class="color-secondary font-bold">
{$t("pages.project.view.tabs.project.descGoal")}
</Title>
{#await renderMarkdown(project.descGoal!) then content}
{@html content}
{/await}
<Title level={2} variant="section" class="color-secondary font-bold">
{$t("pages.project.view.tabs.project.descTeam")}
</Title>
{#await renderMarkdown(project.descTeam!) then content}
{@html content}
{/await}
</div>
@@ -42,9 +42,9 @@
$currentDraft?.wizardForm.campaignInfo ?? {
images: [],
video: undefined,
objectives: emptyRichText(),
legacy: emptyRichText(),
targetAudience: emptyRichText(),
brief: emptyRichText(),
about: emptyRichText(),
goal: emptyRichText(),
team: emptyRichText(),
communicationStrategy: emptyRichText(),
},
@@ -106,21 +106,21 @@
});
}
function handleObjectivesChange(doc: JSONContent) {
function handleBriefChange(doc: JSONContent) {
updateCampaignInfo({
objectives: doc,
brief: doc,
});
}
function handleLegacyChange(doc: JSONContent) {
function handleAboutChange(doc: JSONContent) {
updateCampaignInfo({
legacy: doc,
about: doc,
});
}
function handleTargetAudienceChange(doc: JSONContent) {
function handleGoalChange(doc: JSONContent) {
updateCampaignInfo({
targetAudience: doc,
goal: doc,
});
}
@@ -208,8 +208,8 @@
<RichTextEditor
id="objectives"
value={campaignInfo.objectives}
onChange={handleObjectivesChange}
value={campaignInfo.brief}
onChange={handleBriefChange}
placeholder={$t("common.textPlaceholder")}
ariaDescribedBy="objectives-help"
/>
@@ -229,8 +229,8 @@
<RichTextEditor
id="legacy"
value={campaignInfo.legacy}
onChange={handleLegacyChange}
value={campaignInfo.about}
onChange={handleAboutChange}
placeholder={$t("common.textPlaceholder")}
ariaDescribedBy="legacy-help"
/>
@@ -250,8 +250,8 @@
<RichTextEditor
id="target-audience"
value={campaignInfo.targetAudience}
onChange={handleTargetAudienceChange}
value={campaignInfo.goal}
onChange={handleGoalChange}
placeholder={$t("common.textPlaceholder")}
ariaDescribedBy="target-help"
/>
@@ -19,7 +19,7 @@
import { locale, t } from "../../../i18n/store";
import { apiCategoriesGetCollection } from "../../../openapi/client";
import { client } from "../../../openapi/client/client.gen";
import { apiCategoriesIdGetUrl } from "../../../openapi/client/paths.gen";
import { apiCategoriesIdOrSlugGetUrl } from "../../../openapi/client/paths.gen";
import {
currentDraft,
navigateToStep,
@@ -103,7 +103,7 @@
*/
function handleCategoryChange(selected: Category[]) {
const categoryIris = selected.map((s) => {
return client.buildUrl({ url: apiCategoriesIdGetUrl, path: { id: s.id } });
return client.buildUrl({ url: apiCategoriesIdOrSlugGetUrl, path: { idOrSlug: s.id } });
});
updateProject({ categories: categoryIris });
+4 -4
View File
@@ -7,7 +7,7 @@ Implements active/inactive pill states matching Figma design
import { locale, t } from "../../i18n/store";
import {
apiCategoriesGetCollection,
apiCategoriesIdGet,
apiCategoriesIdOrSlugGet,
type Category,
} from "../../openapi/client";
import { extractId } from "../../utils/extractId";
@@ -42,9 +42,9 @@ Implements active/inactive pill states matching Figma design
}
async function getCategory(iri: string): Promise<Category> {
const { data: category } = await apiCategoriesIdGet({
const { data: category } = await apiCategoriesIdOrSlugGet({
headers: { "Accept-Language": $locale },
path: { id: extractId(iri) || iri },
path: { idOrSlug: extractId(iri) || iri },
});
return category!;
@@ -61,7 +61,7 @@ Implements active/inactive pill states matching Figma design
{#await categories then categories}
<CategorySelect
bind:selected
selectedIds={selected.map((s) => s.id)}
selectedIds={selected.map((s) => s.id!)}
options={categories}
onchange={(selected) => onCategoryChange?.(selected.map((o) => `${o.id}`))}
/>
+23 -7
View File
@@ -174,6 +174,7 @@
"delete": "Borrar",
"continue": "Continuar",
"back": "Volver",
"learnMore": "Saber más",
"cancel": "Cancelar",
"publish": "Publicar",
"preview": "Vista previa",
@@ -458,19 +459,31 @@
"remainingBalance": "Saldo que te quedará"
},
"verify": {
"title": "Estado del pago",
"approved": {
"title": "Tu medio de pago se ha procesado con un éxito apabullante",
"description": "¡Bien! Estás a un paso de terminar y de hacer el mundo un poco mejor.",
"formGoal": {
"title": "Si la campaña no logra su objetivo",
"options": {
"1": "Ingresar en mi monedero de Goteo para poder destinarlo a otros proyectos. Saber más",
"1": "Ingresar en mi monedero de Goteo para poder destinarlo a otros proyectos.",
"2": "Devolverlo a mi medio de pago"
}
},
"formReview": {
"title": "Deja un mensaje en los proyectos a los que has apoyado",
"description": "Tu donación se verá reflejada en los proyectos que has apoyado. Puedes dejar un mensaje o permanecer en el anonimato. "
"description": "Tu donación se verá reflejada en los proyectos que has apoyado. Puedes dejar un mensaje o permanecer en el anonimato.",
"crowdfundingProject": "Proyecto de crowdfunding:",
"donated": "Has donado",
"commentBtn": {
"leaveComment": "Dejar un comentario",
"editComment": "Editar comentario"
},
"commentModal": {
"title": "Deja un comentario",
"label": "Escribe aquí tu comentario"
},
"anonymous": "Quiero que mi comentario sea anónimo"
}
},
"error": {
@@ -794,7 +807,12 @@
"view": {
"owner": "Campaña de crowdfunding impulsada por {{ owner }}",
"tabs": {
"project": "Información de campaña",
"project": {
"title": "Información de campaña",
"descAbout": "Descripción del proyecto. Características, fortalezas y diferencias.",
"descGoal": "Motivación y a quién va dirigido el proyecto.",
"descTeam": "Experiencia previa y equipo."
},
"rewards": "Recompensas",
"budget": {
"title": "Presupuesto",
@@ -867,12 +885,10 @@
},
"banner": {
"donator": {
"description": "Goteo pertenece a la Fundación Platoniq. Al colaborar en una campaña tu dinero será custodiado por Platoniq y donado a {{owner}} en caso de llegar a los objetivos de éxito marcados.",
"learnMore": "Saber más."
"description": "Goteo pertenece a la Fundación Platoniq. Al colaborar en una campaña tu dinero será custodiado por Platoniq y donado a {{owner}} en caso de llegar a los objetivos de éxito marcados."
},
"certificate": {
"description": "Al colaborar con un proyecto impulsado en Goteo puedes solicitar un certificado de donación. Así, te deduces hasta un 80% de lo que hayas aportado en tu próxima declaración de la renta.",
"learnMore": "Saber más."
"description": "Al colaborar con un proyecto impulsado en Goteo puedes solicitar un certificado de donación. Así, te deduces hasta un 80% de lo que hayas aportado en tu próxima declaración de la renta."
}
},
"langNotAvailable": "El idioma seleccionado \"{{lang}}\" no está disponible para este proyecto. Mostrando contenido en el idioma disponible.",
+2 -2
View File
@@ -16,9 +16,9 @@ export const apiCategoriesGetCollectionUrl = '/v4/categories';
export const apiCategoriesPostUrl = '/v4/categories';
export const apiCategoriesIdDeleteUrl = '/v4/categories/{id}';
export const apiCategoriesIdOrSlugGetUrl = '/v4/categories/{idOrSlug}';
export const apiCategoriesIdGetUrl = '/v4/categories/{id}';
export const apiCategoriesIdDeleteUrl = '/v4/categories/{id}';
export const apiCategoriesIdPatchUrl = '/v4/categories/{id}';
File diff suppressed because one or more lines are too long
+136 -65
View File
@@ -248,7 +248,11 @@ export type Category = {
/**
* This value will identify this Category in relationships with other resources.
*/
id: string;
id?: string;
/**
* A unique, non white space, string identifier for this Category.
*/
readonly slug?: string;
/**
* A human-readable self-descriptive string of what this Category is about.
*/
@@ -278,7 +282,11 @@ export type CategoryJsonld = {
/**
* This value will identify this Category in relationships with other resources.
*/
id: string;
id?: string;
/**
* A unique, non white space, string identifier for this Category.
*/
readonly slug?: string;
/**
* A human-readable self-descriptive string of what this Category is about.
*/
@@ -1530,9 +1538,21 @@ export type Project = {
*/
territory: Territory;
/**
* Free-form rich text description for the Project.
* Rich-text (markdown) introduction to the project.
*/
description: string;
descBrief?: string;
/**
* Rich-text (markdown) description on the main features of the project.
*/
descAbout?: string;
/**
* Rich-text (markdown) about why this project is important.
*/
descGoal?: string;
/**
* Rich-text (markdown) about team and previous experience.
*/
descTeam?: string;
/**
* Extracted embedding data from the Project's video.
*/
@@ -1639,9 +1659,21 @@ export type ProjectProjectUpdationDto = {
*/
territory?: Territory;
/**
* Free-form rich text description for the Project.
* Rich-text (markdown allowed) introduction to the project.
*/
description?: string;
descBrief?: string;
/**
* Rich-text (markdown allowed) description on the main features of the project.
*/
descAbout?: string;
/**
* Rich-text (markdown allowed) about why this project is important.
*/
descGoal?: string;
/**
* Rich-text (markdown allowed) about team and previous experience.
*/
descTeam?: string;
/**
* On `minimum`, Project will campaign until the minimum deadline.\
* On `optimum`, Project will campaing until the minimum deadline,
@@ -1713,9 +1745,21 @@ export type ProjectJsonld = {
*/
territory: TerritoryJsonld;
/**
* Free-form rich text description for the Project.
* Rich-text (markdown) introduction to the project.
*/
description: string;
descBrief?: string;
/**
* Rich-text (markdown) description on the main features of the project.
*/
descAbout?: string;
/**
* Rich-text (markdown) about why this project is important.
*/
descGoal?: string;
/**
* Rich-text (markdown) about team and previous experience.
*/
descTeam?: string;
/**
* Extracted embedding data from the Project's video.
*/
@@ -2487,6 +2531,9 @@ export type User = {
* A unique, non white space, byte-safe string identifier for this User.
*/
handle: string;
/**
* The User's given email address. Only available to themselves and platform administrators.
*/
email: string;
/**
* Has this User confirmed their email address?
@@ -2507,7 +2554,8 @@ export type User = {
readonly displayName?: string;
/**
* For `individual` User types: personal data about the User themselves.\
* For `organization` User types: data for the organization representative or person managing the User.
* For `organization` User types: data for the organization representative or person managing the User.\
* Only available to themselves and platform administrators.
*/
readonly person?: string;
/**
@@ -2597,6 +2645,9 @@ export type UserJsonld = {
* A unique, non white space, byte-safe string identifier for this User.
*/
handle: string;
/**
* The User's given email address. Only available to themselves and platform administrators.
*/
email: string;
/**
* Has this User confirmed their email address?
@@ -2617,7 +2668,8 @@ export type UserJsonld = {
readonly displayName?: string;
/**
* For `individual` User types: personal data about the User themselves.\
* For `organization` User types: data for the organization representative or person managing the User.
* For `organization` User types: data for the organization representative or person managing the User.\
* Only available to themselves and platform administrators.
*/
readonly person?: string;
/**
@@ -2950,6 +3002,9 @@ export type ApiCategoriesGetCollectionData = {
* The number of items per page
*/
itemsPerPage?: number;
slug?: string;
'slug[]'?: Array<string>;
name?: string;
};
url: '/v4/categories';
};
@@ -2999,6 +3054,36 @@ export type ApiCategoriesPostResponses = {
export type ApiCategoriesPostResponse = ApiCategoriesPostResponses[keyof ApiCategoriesPostResponses];
export type ApiCategoriesIdOrSlugGetData = {
body?: never;
path: {
/**
* Category identifier or slug
*/
idOrSlug: string;
};
query?: never;
url: '/v4/categories/{idOrSlug}';
};
export type ApiCategoriesIdOrSlugGetErrors = {
/**
* Not found
*/
404: ErrorJsonld;
};
export type ApiCategoriesIdOrSlugGetError = ApiCategoriesIdOrSlugGetErrors[keyof ApiCategoriesIdOrSlugGetErrors];
export type ApiCategoriesIdOrSlugGetResponses = {
/**
* Category resource
*/
200: Category;
};
export type ApiCategoriesIdOrSlugGetResponse = ApiCategoriesIdOrSlugGetResponses[keyof ApiCategoriesIdOrSlugGetResponses];
export type ApiCategoriesIdDeleteData = {
body?: never;
path: {
@@ -3033,36 +3118,6 @@ export type ApiCategoriesIdDeleteResponses = {
export type ApiCategoriesIdDeleteResponse = ApiCategoriesIdDeleteResponses[keyof ApiCategoriesIdDeleteResponses];
export type ApiCategoriesIdGetData = {
body?: never;
path: {
/**
* Category identifier
*/
id: string;
};
query?: never;
url: '/v4/categories/{id}';
};
export type ApiCategoriesIdGetErrors = {
/**
* Not found
*/
404: ErrorJsonld;
};
export type ApiCategoriesIdGetError = ApiCategoriesIdGetErrors[keyof ApiCategoriesIdGetErrors];
export type ApiCategoriesIdGetResponses = {
/**
* Category resource
*/
200: Category;
};
export type ApiCategoriesIdGetResponse = ApiCategoriesIdGetResponses[keyof ApiCategoriesIdGetResponses];
export type ApiCategoriesIdPatchData = {
/**
* The updated Category resource
@@ -3177,10 +3232,14 @@ export type ApiGatewayChargesGetCollectionData = {
itemsPerPage?: number;
'checkout.gateway'?: string;
'checkout.gateway[]'?: Array<string>;
'checkout.origin'?: string;
'checkout.origin[]'?: Array<string>;
'checkout.trackings.value'?: string;
'checkout.trackings.value[]'?: Array<string>;
type?: string;
'type[]'?: Array<string>;
title?: string;
'title[]'?: Array<string>;
target?: string;
'target[]'?: Array<string>;
'money.currency'?: string;
@@ -3223,10 +3282,14 @@ export type ApiGatewayChargestotalsGetCollectionData = {
query?: {
'checkout.gateway'?: string;
'checkout.gateway[]'?: Array<string>;
'checkout.origin'?: string;
'checkout.origin[]'?: Array<string>;
'checkout.trackings.value'?: string;
'checkout.trackings.value[]'?: Array<string>;
type?: string;
'type[]'?: Array<string>;
title?: string;
'title[]'?: Array<string>;
target?: string;
'target[]'?: Array<string>;
'money.currency'?: string;
@@ -3346,6 +3409,14 @@ export type ApiGatewayCheckoutsGetCollectionData = {
* The number of items per page
*/
itemsPerPage?: number;
gateway?: string;
'gateway[]'?: Array<string>;
origin?: string;
'origin[]'?: Array<string>;
status?: string;
'status[]'?: Array<string>;
'trackings.value'?: string;
'trackings.value[]'?: Array<string>;
};
url: '/v4/gateway_checkouts';
};
@@ -4268,7 +4339,7 @@ export type ApiProjectsGetCollectionData = {
'territory.subLvl1[]'?: Array<string>;
'territory.subLvl2'?: string;
'territory.subLvl2[]'?: Array<string>;
description?: string;
descBrief?: string;
status?: string;
'status[]'?: Array<string>;
'calendar.release[before]'?: string;
@@ -5554,6 +5625,10 @@ export type ApiUsersGetCollectionData = {
* The number of items per page
*/
itemsPerPage?: number;
/**
* Searches on `email`, `person.firstName`, `person.lastName`, `organization.legalName`, `organization.businessName`
*/
q?: string;
handle?: string;
'order[handle]'?: 'asc' | 'desc';
'order[dateCreated]'?: 'asc' | 'desc';
@@ -5571,6 +5646,22 @@ export type ApiUsersGetCollectionData = {
'territory.subLvl2[]'?: Array<string>;
roles?: string;
'roles[]'?: Array<string>;
/**
* Secured by `is_granted("ROLE_ADMIN")`
*/
'person.taxId'?: string;
/**
* Secured by `is_granted("ROLE_ADMIN")`
*/
'person.taxId[]'?: Array<string>;
/**
* Secured by `is_granted("ROLE_ADMIN")`
*/
'organization.taxId'?: string;
/**
* Secured by `is_granted("ROLE_ADMIN")`
*/
'organization.taxId[]'?: Array<string>;
active?: boolean;
'dateCreated[before]'?: string;
'dateCreated[strictly_before]'?: string;
@@ -5599,12 +5690,7 @@ export type ApiUsersPostData = {
*/
body: UserUserSignupDto;
path?: never;
query?: {
/**
* Only available to admin users
*/
email?: string;
};
query?: never;
url: '/v4/users';
};
@@ -5638,12 +5724,7 @@ export type ApiUsersIdOrHandleGetData = {
*/
idOrHandle: string;
};
query?: {
/**
* Only available to admin users
*/
email?: string;
};
query?: never;
url: '/v4/users/{idOrHandle}';
};
@@ -5673,12 +5754,7 @@ export type ApiUsersIdDeleteData = {
*/
id: string;
};
query?: {
/**
* Only available to admin users
*/
email?: string;
};
query?: never;
url: '/v4/users/{id}';
};
@@ -5711,12 +5787,7 @@ export type ApiUsersIdPatchData = {
*/
id: string;
};
query?: {
/**
* Only available to admin users
*/
email?: string;
};
query?: never;
url: '/v4/users/{id}';
};
@@ -0,0 +1,63 @@
---
import Summary from "../../../components/checkout/Summary.svelte";
import PaymentMethod from "../../../components/checkout/PaymentMethod.astro";
import Card from "../../../components/library/cards/Card.svelte";
const { t } = Astro.locals;
const { accounting, paymentGateways, selectedGateway, postPaymentUrl } = Astro.props;
---
<div class="wrapper max-md:px-0">
<div class="flex flex-col gap-8 pt-6 lg:flex-row">
<div class="flex w-full flex-col gap-10 px-4 lg:w-1/2 lg:px-0">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-10">
<div class="flex flex-row items-center justify-between gap-6">
<div class="flex w-2/3 flex-col gap-4">
<h2 class="text-[2.5rem]/12 font-bold tracking-[0%] text-black">
{t("pages.checkout.verify.error.title")}
</h2>
<p class="text-content">
{t("pages.checkout.verify.error.description")}
</p>
</div>
<img
src="/images/checkout/payment-error.svg"
alt=""
width="221"
height="214"
/>
</div>
<PaymentMethod
{accounting}
{paymentGateways}
{selectedGateway}
hasError={true}
/>
</div>
</div>
</div>
<Card
class="lg: sticky -bottom-6 flex h-fit w-full flex-col items-start gap-6 rounded-3xl border-black pr-4 pb-10 pl-4 lg:top-(--sticky-top) lg:bottom-auto lg:w-1/2 lg:gap-6 lg:rounded-[40px] lg:p-6"
>
<Summary client:load hasError={true} />
<div class="w-full lg:block" id="checkout-details">
<div class="flex flex-col">
<p class="text-content text-sm">
{t("pages.checkout.summary.deduct")}
</p>
</div>
</div>
<div class="flex flex-col items-end">
<a
href={postPaymentUrl}
class="bg-primary text-secondary w-fit cursor-pointer rounded-3xl px-6 py-4 font-bold"
>
{t("common.continue")}
</a>
</div>
</Card>
</div>
</div>
@@ -0,0 +1,66 @@
---
import FeedbackForm from "../../../components/checkout/FeedbackForm.svelte";
import Summary from "../../../components/checkout/Summary.svelte";
import Card from "../../../components/library/cards/Card.svelte";
import ClearCart from "../../../components/cart/ClearCart.svelte";
const { t, session } = Astro.locals;
const { total, selectedGateway, donatedProjects, projectSupports, postPaymentUrl } = Astro.props;
---
<div class="wrapper max-md:px-0">
<div class="flex flex-col gap-8 pt-6 lg:flex-row">
<div class="flex w-full flex-col gap-10 px-4 lg:w-1/2 lg:px-0">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-9">
<div class="flex flex-row items-center justify-between gap-6">
<div class="flex w-2/3 flex-col gap-4">
<h2 class="text-[2.5rem]/12 font-bold tracking-[0%] text-black">
{t("pages.checkout.verify.approved.title")}
</h2>
<p class="text-content">
{t("pages.checkout.verify.approved.description")}
</p>
</div>
<img
src="/images/checkout/payment-approved.svg"
alt=""
width="215"
height="190"
/>
</div>
<div>
<FeedbackForm
client:load
paymentMethod={selectedGateway}
projects={donatedProjects}
projectSupports={projectSupports}
/>
</div>
</div>
</div>
</div>
<Card
class="lg: sticky -bottom-6 flex h-fit w-full flex-col items-start gap-6 rounded-3xl border-black pr-4 pb-10 pl-4 lg:top-(--sticky-top) lg:bottom-auto lg:w-1/2 lg:gap-6 lg:rounded-[40px] lg:p-6"
>
<Summary client:load {total} />
<ClearCart client:load userId={session!.user.id!} />
<div class="w-full lg:block" id="checkout-details">
<div class="flex flex-col">
<p class="text-content text-sm">
{t("pages.checkout.summary.deduct")}
</p>
</div>
</div>
<div class="flex flex-col items-end">
<a
href={postPaymentUrl}
class="bg-primary text-secondary w-fit cursor-pointer rounded-3xl px-6 py-4 font-bold"
>
{t("common.continue")}
</a>
</div>
</Card>
</div>
</div>
+104 -154
View File
@@ -1,194 +1,144 @@
---
import Layout from "../../../layouts/Layout.astro";
import App from "../../../layouts/App.svelte";
import { apiAccountingsIdGet, apiGatewayCheckoutsIdGet } from "../../../openapi/client/index.ts";
import FeedbackForm from "../../../components/checkout/FeedbackForm.svelte";
import Summary from "../../../components/checkout/Summary.svelte";
import PaymentMethod from "../../../components/checkout/PaymentMethod.astro";
import { apiGatewaysGetCollection } from "../../../openapi/client/index";
import Card from "../../../components/library/cards/Card.svelte";
import { extractId } from "../../../utils/extractId.ts";
import VerificationError from "./_VerificationError.astro";
import VerificationSuccess from "./_VerificationSuccess.astro";
import {
apiAccountingsIdGet,
apiGatewayCheckoutsIdGet,
apiGatewaysGetCollection,
apiProjectSupportsGetCollection,
apiProjectsGetCollection,
} from "../../../openapi/client";
import { apiProjectsIdOrSlugGetUrl } from "../../../openapi/client/paths.gen";
import { client } from "../../../openapi/client/client.gen";
import { extractId } from "../../../utils/extractId";
import { sumMoney } from "../../../utils/money";
import { goto } from "../../../utils/navigation";
import type { Money } from "../../../openapi/client";
import type { Money, MoneyOutput, Project, ProjectSupport } from "../../../openapi/client";
const { t, lang, session } = Astro.locals;
const { data: paymentGateways } = await apiGatewaysGetCollection();
if (!session) {
return goto("/login", { query: { callback: Astro.url.pathname + Astro.url.search } });
return goto("/login", {
query: {
callback: Astro.url.pathname + Astro.url.search,
},
});
}
const { token, user } = session;
const checkoutId = Astro.url.searchParams.get("checkoutId");
const { data: accounting } = await apiAccountingsIdGet({
path: { id: extractId(user.accounting)! },
headers: {
Authorization: `${token.token_type} ${token.access_token}`,
},
});
const postPaymentUrl = checkoutId
? `/checkout/post-payment?checkoutId=${checkoutId}`
: "/checkout/post-payment";
let error = null;
let total: Money | undefined = undefined;
let selectedGateway: string | undefined = undefined;
const [{ data: paymentGateways }, { data: accounting }] = await Promise.all([
apiGatewaysGetCollection(),
apiAccountingsIdGet({
path: { id: extractId(user.accounting)! },
headers: token.asHttpHeaders,
}),
]);
let error: string | undefined;
let total: Money | undefined;
let selectedGateway: string | undefined;
let donatedProjects: Project[] = [];
let projectSupports: ProjectSupport[] = [];
try {
const url = new URL(Astro.request.url);
const checkoutId = url.searchParams.get("checkoutId");
if (!checkoutId) {
throw new Error("Checkout ID not found");
}
const response = await apiGatewayCheckoutsIdGet({
const { data: checkout } = await apiGatewayCheckoutsIdGet({
path: { id: checkoutId },
headers: {
Authorization: `${token.token_type} ${token.access_token}`,
},
headers: token.asHttpHeaders,
});
if (!response.data) {
throw new Error("Data not found");
if (!checkout) {
throw new Error("Checkout data not found");
}
selectedGateway = response.data.gateway.split("/").pop();
if (response.data.status !== "charged") {
if (checkout.status !== "charged") {
throw new Error("Payment not completed");
}
total = response.data?.charges?.length
? sumMoney(response.data.charges.filter((c) => c.money).map((c) => c.money! as Money))
: undefined;
selectedGateway = checkout.gateway.split("/").at(-1);
const charges = checkout.charges ?? [];
const money = charges
.map((charge) => charge.money)
.filter((money): money is MoneyOutput => Boolean(money));
total = money.length > 0 ? sumMoney(money) : undefined;
const accountingTargets = [
...new Set(
charges
.map((charge) => charge.target)
.filter((target): target is string => Boolean(target)),
),
];
const { data: projects = [] } = await apiProjectsGetCollection({
query: {
"accounting[]": accountingTargets,
},
});
donatedProjects = projects;
if (projects.length > 0) {
const projectUrls = projects.map((project) =>
client.buildUrl({
url: apiProjectsIdOrSlugGetUrl,
path: { idOrSlug: project.id },
}),
);
const { data: supports = [] } = await apiProjectSupportsGetCollection({
query: {
origin: user.accounting,
"project[]": projectUrls,
},
headers: token.asHttpHeaders,
});
projectSupports = supports;
}
} catch (err) {
console.error("Error trying to get payment status", err);
console.error("Error trying to verify payment", err);
error = err instanceof Error ? err.message : "Unknown error";
}
const checkoutId = new URL(Astro.request.url).searchParams.get("checkoutId");
const postPaymentUrl = checkoutId
? `/checkout/post-payment?checkoutId=${checkoutId}`
: "/checkout/post-payment";
---
<Layout title="Estado del pago">
<Layout title={t("pages.checkout.verify.title")}>
<App client:load locale={lang} session={session}>
<div class="wrapper pt-6 max-md:px-0">
<div class="wrapper pt-6 md:px-0">
{
error ? (
<div class="wrapper max-md:px-0">
<div class="flex flex-col gap-8 pt-6 lg:flex-row">
<div class="flex w-full flex-col gap-10 px-4 lg:w-1/2 lg:px-0">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-10">
<div class="flex flex-row items-center justify-between gap-6">
<div class="flex w-2/3 flex-col gap-4">
<h2 class="text-[2.5rem]/12 font-bold tracking-[0%] text-black">
{t("pages.checkout.verify.error.title")}
</h2>
<p class="text-content">
{t("pages.checkout.verify.error.description")}
</p>
</div>
<img
src="/images/checkout/payment-error.svg"
alt=""
width="221"
height="214"
/>
</div>
<PaymentMethod
{paymentGateways}
{accounting}
{selectedGateway}
hasError={!!error}
/>
</div>
</div>
</div>
<Card class="lg: sticky -bottom-6 flex h-fit w-full flex-col items-start gap-6 rounded-3xl border-black pr-4 pb-10 pl-4 lg:top-(--sticky-top) lg:bottom-auto lg:w-1/2 lg:gap-6 lg:rounded-[40px] lg:p-6">
<Summary client:load hasError={!!error} />
<div class="w-full lg:block" id="checkout-details">
<div class="flex flex-col">
<p class="text-content text-sm">
{t("pages.checkout.summary.deduct")}
</p>
</div>
</div>
<div class="flex flex-col items-end">
<a
href={postPaymentUrl}
class="bg-primary text-secondary w-fit cursor-pointer rounded-3xl px-6 py-4 font-bold"
>
{t("common.continue")}
</a>
</div>
</Card>
</div>
</div>
<VerificationError
accounting={accounting}
paymentGateways={paymentGateways}
selectedGateway={selectedGateway}
postPaymentUrl={postPaymentUrl}
/>
) : (
<>
<div class="wrapper max-md:px-0">
<div class="flex flex-col gap-8 pt-6 lg:flex-row">
<div class="flex w-full flex-col gap-10 px-4 lg:w-1/2 lg:px-0">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-9">
<div class="flex flex-row items-center justify-between gap-6">
<div class="flex w-2/3 flex-col gap-4">
<h2 class="text-[2.5rem]/12 font-bold tracking-[0%] text-black">
{t("pages.checkout.verify.approved.title")}
</h2>
<p class="text-content">
{t(
"pages.checkout.verify.approved.description",
)}
</p>
</div>
<img
src="/images/checkout/payment-approved.svg"
alt=""
width="215"
height="190"
/>
</div>
<div>
<FeedbackForm
client:load
paymentMethod={selectedGateway}
/>
</div>
</div>
</div>
</div>
<Card class="lg: sticky -bottom-6 flex h-fit w-full flex-col items-start gap-6 rounded-3xl border-black pr-4 pb-10 pl-4 lg:top-(--sticky-top) lg:bottom-auto lg:w-1/2 lg:gap-6 lg:rounded-[40px] lg:p-6">
<Summary client:load hasError={!!error} {total} />
<div class="w-full lg:block" id="checkout-details">
<div class="flex flex-col">
<p class="text-content text-sm">
{t("pages.checkout.summary.deduct")}
</p>
</div>
</div>
<div class="flex flex-col items-end">
<a
href={postPaymentUrl}
class="bg-primary text-secondary w-fit cursor-pointer rounded-3xl px-6 py-4 font-bold"
>
{t("common.continue")}
</a>
</div>
</Card>
</div>
</div>
<div id="checkout-clear" data-user-id={user.id} />
<script>
import {clearForUser} from "../../../stores/checkoutsStore";
clearForUser( document.getElementById("checkout-clear")?.dataset.userId,
);
</script>
</>
<VerificationSuccess
total={total}
selectedGateway={selectedGateway}
donatedProjects={donatedProjects}
projectSupports={projectSupports}
postPaymentUrl={postPaymentUrl}
/>
)
}
</div>
+5 -1
View File
@@ -14,6 +14,8 @@ const SAFE_RESPONSE_HEADERS = [
"vary",
];
const UNSAFE_REQUEST_HEADERS = ["host", "content-length", "connection", "transfer-encoding"];
/**
* API Relay Endpoint
*
@@ -36,7 +38,9 @@ export const ALL: APIRoute = async ({ request, cookies, params }) => {
const url = `${import.meta.env.PUBLIC_API_URL}/${params.path}${query}`;
const reqHeaders = new Headers(request.headers);
reqHeaders.delete("host");
for (const header of UNSAFE_REQUEST_HEADERS) {
reqHeaders.delete(header);
}
const session = await getSession(cookies);
if (session && session.token.asHttpHeaders) {
+3 -3
View File
@@ -137,7 +137,7 @@ export function validateCampaignInfo(wizard: Wizard): ValidationErrors {
}
// Objectives validation
const objectivesPlainText = richTextToPlainText(data.objectives).trim();
const objectivesPlainText = richTextToPlainText(data.brief).trim();
if (objectivesPlainText.length === 0) {
errors.objectives = "pages.project.edit.rewards.validation_info.reward.objectives.required";
} else if (objectivesPlainText.length < 50) {
@@ -149,7 +149,7 @@ export function validateCampaignInfo(wizard: Wizard): ValidationErrors {
}
// Legacy validation
const legacyPlainText = richTextToPlainText(data.legacy).trim();
const legacyPlainText = richTextToPlainText(data.about).trim();
if (legacyPlainText.length === 0) {
errors.legacy = "pages.project.edit.rewards.validation_info.reward.legacy.required";
} else if (legacyPlainText.length < 50) {
@@ -159,7 +159,7 @@ export function validateCampaignInfo(wizard: Wizard): ValidationErrors {
}
// Target audience validation
const targetPlainText = richTextToPlainText(data.targetAudience).trim();
const targetPlainText = richTextToPlainText(data.goal).trim();
if (targetPlainText.length === 0) {
errors.targetAudience = "pages.project.edit.rewards.validation_info.reward.target.required";
} else if (targetPlainText.length < 30) {
+9 -9
View File
@@ -46,9 +46,9 @@ export interface WizardCampaignInfo {
video: string | undefined;
// Rich text content (stored as Tiptap JSON)
objectives: JSONContent;
legacy: JSONContent;
targetAudience: JSONContent;
brief: JSONContent;
about: JSONContent;
goal: JSONContent;
team: JSONContent;
communicationStrategy: JSONContent;
}
@@ -144,9 +144,9 @@ export const wizard = derived(
campaignInfo: {
images: [],
video: "",
objectives: emptyRichText(),
legacy: emptyRichText(),
targetAudience: emptyRichText(),
brief: emptyRichText(),
about: emptyRichText(),
goal: emptyRichText(),
team: emptyRichText(),
communicationStrategy: emptyRichText(),
},
@@ -292,9 +292,9 @@ export async function createDraft(
campaignInfo: {
images: resources?.images ?? [],
video: "",
objectives: emptyRichText(),
legacy: emptyRichText(),
targetAudience: emptyRichText(),
brief: emptyRichText(),
about: emptyRichText(),
goal: emptyRichText(),
team: emptyRichText(),
communicationStrategy: emptyRichText(),
},
-17
View File
@@ -3,23 +3,6 @@
color: var(--color-content);
}
.marked-content h1 {
padding-bottom: var(--text-lg);
color: var(--color-secondary);
font-size: var(--text-3xl);
font-weight: var(--font-weight-black);
line-height: var(--text-xs--line-height);
}
.marked-content h2 {
padding-top: 40px;
padding-bottom: var(--text-xs);
color: var(--color-secondary);
font-size: var(--text-xl);
font-weight: var(--font-weight-black);
line-height: var(--text-xs--line-height);
}
.marked-content img {
margin: auto;
margin-top: var(--text-xl);
+3 -3
View File
@@ -18,9 +18,9 @@ function normalizeRichText(draft: Draft): Draft {
...draft.wizardForm,
campaignInfo: {
...campaignInfo,
objectives: toRichText(campaignInfo.objectives),
legacy: toRichText(campaignInfo.legacy),
targetAudience: toRichText(campaignInfo.targetAudience),
brief: toRichText(campaignInfo.brief),
about: toRichText(campaignInfo.about),
goal: toRichText(campaignInfo.goal),
team: toRichText(campaignInfo.team),
communicationStrategy: toRichText(campaignInfo.communicationStrategy),
},
+5 -12
View File
@@ -10,7 +10,7 @@ import {
updateCollaboration,
updateReward,
} from "./projectSubmissionApi";
import { isRichTextEmpty, richTextToHtml } from "./richText";
import { richTextToMarkdown } from "./richText";
import { validateDraftToPublish } from "../stores/drafts/draftValidation";
import type { Session } from "../auth/types";
@@ -145,16 +145,6 @@ export async function publishDraft(draft: Draft, session: Session, projectId: st
},
};
const description = [
wizard.campaignInfo.objectives,
wizard.campaignInfo.legacy,
wizard.campaignInfo.targetAudience,
wizard.campaignInfo.team,
]
.filter((doc) => !isRichTextEmpty(doc))
.map(richTextToHtml)
.join("\n\n");
const firstImage = wizard.campaignInfo.images[0];
const result = await patchProject(
@@ -164,7 +154,10 @@ export async function publishDraft(draft: Draft, session: Session, projectId: st
subtitle: draft.createProject.subtitle,
categories: draft.createProject.categories,
video: wizard.campaignInfo.video,
description,
descBrief: richTextToMarkdown(wizard.campaignInfo.brief),
descAbout: richTextToMarkdown(wizard.campaignInfo.about),
descGoal: richTextToMarkdown(wizard.campaignInfo.goal),
descTeam: richTextToMarkdown(wizard.campaignInfo.team),
deadline: wizard.configuration.projectDeadline,
territory: draft.createProject.territory,
cover: firstImage?.url ?? "",