feat: show carousel with project´s update

This commit is contained in:
Jean Piere Rodriguez
2025-06-16 05:25:16 -05:00
parent 21829a2047
commit e84caafec3
6 changed files with 176 additions and 57 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

+51 -43
View File
@@ -1,14 +1,17 @@
<script lang="ts">
import ArrowSliderIcon from "../svgs/ArrowSliderIcon.svelte";
import { onMount } from "svelte";
import { onMount, tick } from "svelte";
import { writable } from "svelte/store";
export let itemsPerGroup: number = 1;
export let gap: number = 16;
export let showDots: boolean = true;
export let highlightActive: boolean = false;
export let dimNext: boolean = false;
let container: HTMLDivElement;
let totalItems = 0;
let activeGroupValue = 0;
const activeGroup = writable(0);
let totalGroups = 0;
@@ -29,8 +32,7 @@
function handleMove(x: number, event: Event) {
if (!isDragging) return;
event.preventDefault();
const currentX = x - container.offsetLeft;
const walk = (currentX - startX) * 1.5;
const walk = (x - container.offsetLeft - startX) * 1.5;
container.scrollLeft = scrollLeft - walk;
}
@@ -39,33 +41,17 @@
container.classList.remove("dragging");
}
function handleMouseDown(e: MouseEvent) {
handleStart(e.pageX);
}
function handleMouseMove(e: MouseEvent) {
handleMove(e.pageX, e);
}
function handleTouchStart(e: TouchEvent) {
handleStart(e.touches[0].pageX);
}
function handleTouchMove(e: TouchEvent) {
handleMove(e.touches[0].pageX, e);
}
function scrollToGroup(groupIndex: number) {
function scrollToGroup(index: number) {
const children = Array.from(container.children) as HTMLElement[];
const firstItemIndex = groupIndex * itemsPerGroup;
const firstItem = children[firstItemIndex];
if (!firstItem) return;
const first = children[index * itemsPerGroup];
if (!first) return;
container.scrollTo({
left: firstItem.offsetLeft - container.offsetLeft,
left: first.offsetLeft - container.offsetLeft,
behavior: "smooth",
});
activeGroup.set(groupIndex);
activeGroup.set(index);
}
function scroll(direction: "left" | "right") {
@@ -86,11 +72,9 @@
for (let i = 0; i < totalGroups; i++) {
const firstIndex = i * itemsPerGroup;
const firstItem = children[firstIndex];
if (!firstItem) continue;
const itemLeft = firstItem.getBoundingClientRect().left;
if (itemLeft >= containerLeft - 1) {
activeGroup.set(i);
break;
@@ -103,7 +87,6 @@
const paddingLeft = parseFloat(styles.paddingLeft);
const paddingRight = parseFloat(styles.paddingRight);
const availableWidth = container.clientWidth - paddingLeft - paddingRight;
const childWidth = (availableWidth - gap * (itemsPerGroup - 1)) / itemsPerGroup;
for (const child of Array.from(container.children) as HTMLElement[]) {
@@ -113,49 +96,72 @@
}
}
onMount(() => {
totalItems = container.children.length;
totalGroups = Math.ceil(totalItems / itemsPerGroup);
function updateChildrenClasses() {
const children = Array.from(container.children) as HTMLElement[];
children.forEach((child, i) => {
child.classList.remove("carousel-primary", "carousel-next");
if (highlightActive && i === activeGroupValue) {
child.classList.add("carousel-primary");
}
if (dimNext && i === activeGroupValue + 1) {
child.classList.add("carousel-next");
}
});
}
onMount(async () => {
await tick();
updateChildrenWidth();
const resizeObserver = new ResizeObserver(() => {
updateChildrenWidth();
});
resizeObserver.observe(container);
const unsubscribe = activeGroup.subscribe((index) => {
activeGroupValue = index;
isAtStart = index === 0;
isAtEnd = index === totalGroups - 1;
updateChildrenClasses();
});
const mutationObserver = new MutationObserver(() => {
totalItems = container.children.length;
totalGroups = Math.ceil(totalItems / itemsPerGroup);
if (totalItems > 0) {
activeGroup.set(0);
mutationObserver.disconnect();
}
});
mutationObserver.observe(container, { childList: true });
return () => {
resizeObserver.disconnect();
mutationObserver.disconnect();
unsubscribe();
};
});
</script>
<div class="relative w-full overflow-hidden">
<div class="relative w-full">
<button
on:click={() => scroll("left")}
class="absolute top-1/2 left-2 z-10 h-10 w-10 -translate-y-1/2 rounded-full bg-[#e6e5f7] p-2 shadow disabled:cursor-not-allowed disabled:opacity-50"
class="absolute top-1/2 -left-4 z-10 h-10 w-10 -translate-y-1/2 cursor-pointer rounded-full bg-[#e6e5f7] p-2 shadow-md disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Scroll left"
disabled={isAtStart}
>
<ArrowSliderIcon direction={"left"} />
<ArrowSliderIcon direction="left" />
</button>
<div
bind:this={container}
class="no-select hide-scrollbar flex cursor-grab touch-none snap-x snap-mandatory gap-[16px] overflow-x-auto px-8 py-8"
on:mousedown={handleMouseDown}
on:mousemove={handleMouseMove}
class="no-select hide-scrollbar flex cursor-grab touch-none snap-x snap-mandatory gap-[16px] overflow-hidden overflow-x-auto"
on:mousedown={(e) => handleStart(e.pageX)}
on:mousemove={(e) => handleMove(e.pageX, e)}
on:mouseup={endDrag}
on:mouseleave={endDrag}
on:touchstart={handleTouchStart}
on:touchmove={handleTouchMove}
on:touchstart={(e) => handleStart(e.touches[0].pageX)}
on:touchmove={(e) => handleMove(e.touches[0].pageX, e)}
on:touchend={endDrag}
on:scroll={handleScroll}
>
@@ -164,21 +170,21 @@
<button
on:click={() => scroll("right")}
class="absolute top-1/2 right-2 z-10 h-10 w-10 -translate-y-1/2 rounded-full bg-[#e6e5f7] p-2 shadow disabled:cursor-not-allowed disabled:opacity-50"
class="absolute top-1/2 -right-4 z-10 h-10 w-10 -translate-y-1/2 cursor-pointer rounded-full bg-[#e6e5f7] p-2 shadow-md disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Scroll right"
disabled={isAtEnd}
>
<ArrowSliderIcon />
</button>
{#if showDots}
{#if showDots && totalGroups > 1}
<div class="mt-4 flex justify-center gap-2">
{#each Array(totalGroups) as _, i}
<button
on:click={() => scrollToGroup(i)}
class="h-2 w-2 rounded-full transition-all"
class:bg-indigo-500={i === $activeGroup}
class:bg-gray-300={i !== $activeGroup}
class:bg-indigo-500={i === activeGroupValue}
class:bg-gray-300={i !== activeGroupValue}
/>
{/each}
</div>
@@ -191,6 +197,7 @@
-webkit-user-select: none;
-ms-user-select: none;
}
.dragging {
cursor: grabbing;
}
@@ -199,6 +206,7 @@
scrollbar-width: none;
-ms-overflow-style: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
+99 -3
View File
@@ -1,20 +1,116 @@
<script lang="ts">
import { onMount } from "svelte";
import { t } from "../../i18n/store";
import ActiveFilterIcon from "../../svgs/ActiveFilterIcon.svelte";
import { Modal } from "flowbite-svelte";
import type { Project, ProjectUpdate } from "../../openapi/client/index";
import { apiProjectUpdatesGetCollection } from "../../openapi/client/index";
import Slider from "./Slider.svelte";
import Carousel from "../Carousel.svelte";
const { project } = $props<{ project: Project }>();
let projectsUpdates: ProjectUpdate[] = $state([]);
let openModal = $state(false);
let selectedProject: ProjectUpdate | null = $state(null);
function formatDate(date: string): string {
const options: Intl.DateTimeFormatOptions = {
day: "2-digit",
month: "short",
year: "numeric",
};
return new Date(date).toLocaleDateString(undefined, options);
}
function cleanCloseButton() {
const closeBtn = document.querySelector('button[aria-label="Close"]');
if (!closeBtn) return;
closeBtn.removeAttribute("aria-label");
closeBtn.classList.remove("sr-only");
closeBtn.querySelectorAll("span").forEach((el) => {
if (el.textContent?.trim() === "Close") {
el.remove();
}
});
}
onMount(async () => {
const { data } = await apiProjectUpdatesGetCollection({
query: { project: `/v4/projects/${project.id}` },
});
console.log("DATA:", data);
projectsUpdates = data || [];
});
$effect(() => {
if (openModal) cleanCloseButton();
if (!openModal) selectedProject = null;
});
</script>
<Slider slides={projectsUpdates} />
<div class="flex flex-col gap-10">
<h2 class="text-tertiary line-clamp-2 flex max-w-2xl text-[40px] leading-tight font-bold">
{$t("project.tabs.updates.content.title")}
</h2>
<Carousel gap={16} showDots={true} itemsPerGroup={2}>
{#if projectsUpdates.length === 0}
<div
class="flex h-[140px] w-full items-center justify-center rounded bg-indigo-100 font-bold"
>
{$t("project.tabs.updates.content.empty")}
</div>
{/if}
{#each projectsUpdates as project}
<div
class="flex w-full flex-col justify-between gap-6 rounded-4xl bg-white p-6 font-bold"
>
<div class="flex flex-col gap-4">
<div class="text-tertiary flex flex-row items-center gap-2">
{formatDate(project.date ?? "")}
<ActiveFilterIcon />
</div>
<img
src="/imgs/placeholder-project-update.jpg"
alt="Imagen de project update"
class="no-select rounded-3xl"
draggable="false"
/>
</div>
<div class="flex flex-col gap-4">
<h2 class="text-tertiary text-lg font-semibold">{project.title}</h2>
<div class="flex flex-col gap-2">
<p class="text-secondary text-sm">{project.subtitle}</p>
<p class="line-clamp-2 text-sm text-[#575757]">{project.body}</p>
</div>
</div>
<div class="flex w-full items-center justify-end">
<button
class="text-tertiary border-tertiary flex cursor-pointer items-start truncate rounded-3xl border px-6 py-4 whitespace-nowrap"
onclick={() => {
selectedProject = project;
openModal = true;
}}
>
{$t("project.tabs.updates.content.btn.read-more")}
</button>
</div>
</div>
{/each}
</Carousel>
<Modal
bind:open={openModal}
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="!left-1/2 max-w-[800px] p-4 backdrop:bg-[#878282B2] backdrop:backdrop-blur-[5px]"
title={selectedProject?.title}
headerClass="py-2"
>
{#if selectedProject}
<div class="flex flex-col gap-4">
<p class="text-sm text-[#575757]">{selectedProject.body}</p>
</div>
{/if}
</Modal>
</div>
@@ -10,7 +10,6 @@
import { getTerritoryTag } from "../../utils/getTerritoryTag";
import Countdown from "../Countdown.svelte";
import LanguagesDropdown from "../LanguagesDropdown.svelte";
// import TestSlider from "../TestSlider.svelte";
import { languagesList } from "../../i18n/locales";
import Tabs from "./Tabs.svelte";
@@ -115,7 +114,6 @@
<Rewards {project} {limit} />
<Banner {ownerName} />
<!-- <TestSlider /> -->
<Slider {slides} />
<Tabs {project} />
</section>
+16 -8
View File
@@ -1,5 +1,4 @@
<script lang="ts">
// import TestSlider from "../TestSlider.svelte";
import { onMount } from "svelte";
import { t } from "../../i18n/store";
import { renderMarkdown } from "../../utils/renderMarkdown";
@@ -13,7 +12,7 @@
{ id: "rewards", label: $t("project.tabs.rewards") },
{ id: "project", label: $t("project.tabs.project") },
{ id: "budget", label: $t("project.tabs.budget") },
{ id: "updates", label: $t("project.tabs.updates") },
{ id: "updates", label: $t("project.tabs.updates.title") },
{ id: "community", label: $t("project.tabs.community") },
];
@@ -46,15 +45,20 @@
<div class="flex w-full justify-center bg-[#E6E5F7] py-20">
{#if activeTab === "rewards"}
<div id="tab-rewards" role="tabpanel" aria-labelledby="tab-button-rewards" class="w-full">
<ProjectUpdate {project} />
<div
id="tab-rewards"
role="tabpanel"
aria-labelledby="tab-button-rewards"
class="w-full px-4"
>
Contenido de rewards
</div>
{:else if activeTab === "project"}
<div
id="tab-project"
role="tabpanel"
aria-labelledby="tab-button-project"
class="flex max-w-4xl flex-col items-center gap-4 px-4"
class="flex max-w-4xl flex-col items-center gap-4 px-8"
>
{@html contentDescription}
</div>
@@ -63,9 +67,13 @@
Contenido de necesidades del presupuesto
</div>
{:else if activeTab === "updates"}
<div id="tab-updates" role="tabpanel" aria-labelledby="tab-button-updates" class="w-full">
<!-- <TestSlider /> -->
<!-- <ProjectUpdate {project} active /> -->
<div
id="tab-updates"
role="tabpanel"
aria-labelledby="tab-button-updates"
class="w-full px-10"
>
<ProjectUpdate {project} />
</div>
{:else if activeTab === "community"}
<div
+10 -1
View File
@@ -39,7 +39,16 @@
"project": "Información de campaña",
"budget": "Presupuesto",
"rewards": "Recompensas",
"updates": "Actualizaciones",
"updates": {
"title": "Actualizaciones",
"content": {
"title": "No pierdas detalle de los avances de esta campaña",
"empty": "No existen más actualizaciones previstas.",
"btn": {
"read-more": "Seguir leyendo"
}
}
},
"community": "Comunidad"
},
"actions": {