feat: show carousel with project´s update
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user