@paris-ias/list 1.0.80 → 1.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +0 -4
- package/dist/runtime/components/events/SlidingItem.vue +82 -0
- package/dist/runtime/components/list/organisms/Slider.vue +188 -0
- package/dist/runtime/components/news/View.vue +1 -1
- package/dist/runtime/graphql/list/news.gql +1 -0
- package/package.json +4 -4
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card
|
|
3
|
+
v-ripple
|
|
4
|
+
class="event-sliding-item cursor-pointer"
|
|
5
|
+
elevation="2"
|
|
6
|
+
height="280"
|
|
7
|
+
@click="$router.push(localePath('/activities/events/' + item.slug[locale]))"
|
|
8
|
+
>
|
|
9
|
+
<v-card-text class="pa-4 d-flex flex-column h-100">
|
|
10
|
+
<!-- Date section -->
|
|
11
|
+
<div class="text-overline font-weight-black mb-2 text-primary">
|
|
12
|
+
{{
|
|
13
|
+
new Date(item.start).toLocaleDateString(locale, {
|
|
14
|
+
year: "numeric",
|
|
15
|
+
month: "short",
|
|
16
|
+
day: "numeric",
|
|
17
|
+
})
|
|
18
|
+
}}
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Category badge -->
|
|
22
|
+
<v-chip
|
|
23
|
+
v-if="item.category"
|
|
24
|
+
size="small"
|
|
25
|
+
variant="outlined"
|
|
26
|
+
color="primary"
|
|
27
|
+
class="mb-3 align-self-start"
|
|
28
|
+
>
|
|
29
|
+
{{ $t("list.filters.events.category." + item.category) }}
|
|
30
|
+
</v-chip>
|
|
31
|
+
|
|
32
|
+
<!-- Event title -->
|
|
33
|
+
<h3 class="text-h6 font-weight-bold mb-3 line-clamp-3">
|
|
34
|
+
{{ item.name }}
|
|
35
|
+
</h3>
|
|
36
|
+
|
|
37
|
+
<!-- Event description if available -->
|
|
38
|
+
<p v-if="item.description" class="text-body-2 text-grey-darken-2 line-clamp-4 flex-grow-1">
|
|
39
|
+
{{ item.description }}
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<!-- Location if available -->
|
|
43
|
+
<div v-if="item.location" class="mt-auto pt-2">
|
|
44
|
+
<v-icon size="small" class="mr-1">mdi-map-marker</v-icon>
|
|
45
|
+
<span class="text-caption text-grey-darken-1">{{ item.location }}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</v-card-text>
|
|
48
|
+
|
|
49
|
+
<!-- Hover overlay -->
|
|
50
|
+
<v-overlay
|
|
51
|
+
v-model="isHovered"
|
|
52
|
+
contained
|
|
53
|
+
opacity="0.1"
|
|
54
|
+
class="d-flex align-center justify-center"
|
|
55
|
+
>
|
|
56
|
+
<v-icon size="large" color="primary">mdi-eye</v-icon>
|
|
57
|
+
</v-overlay>
|
|
58
|
+
</v-card>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup>
|
|
62
|
+
import { ref } from "vue";
|
|
63
|
+
import { useLocalePath, useI18n } from "#imports";
|
|
64
|
+
const { locale } = useI18n();
|
|
65
|
+
const localePath = useLocalePath();
|
|
66
|
+
const props = defineProps({
|
|
67
|
+
item: {
|
|
68
|
+
type: Object,
|
|
69
|
+
required: true
|
|
70
|
+
},
|
|
71
|
+
index: {
|
|
72
|
+
type: Number,
|
|
73
|
+
required: true
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const { item, index } = props;
|
|
77
|
+
const isHovered = ref(false);
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<style scoped>
|
|
81
|
+
.event-sliding-item{border-radius:12px;overflow:hidden;transition:all .3s cubic-bezier(.25,.8,.25,1)}.event-sliding-item:hover{box-shadow:0 8px 25px rgba(0,0,0,.15)!important;transform:translateY(-4px)}.line-clamp-3{-webkit-line-clamp:3;line-clamp:3}.line-clamp-3,.line-clamp-4{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.line-clamp-4{-webkit-line-clamp:4;line-clamp:4}
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slider-container">
|
|
3
|
+
<!-- Header with counter and navigation -->
|
|
4
|
+
<div class="slider-header">
|
|
5
|
+
<div class="slide-counter">
|
|
6
|
+
{{ activeSlideIndex + 1 }}/{{ items.length }}
|
|
7
|
+
</div>
|
|
8
|
+
<div class="navigation-controls">
|
|
9
|
+
<button
|
|
10
|
+
:disabled="!canGoBack"
|
|
11
|
+
class="nav-button nav-prev"
|
|
12
|
+
@click="goToSlide(activeSlideIndex - 1)"
|
|
13
|
+
>
|
|
14
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
15
|
+
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
|
16
|
+
</svg>
|
|
17
|
+
</button>
|
|
18
|
+
<button
|
|
19
|
+
:disabled="!canGoForward"
|
|
20
|
+
class="nav-button nav-next"
|
|
21
|
+
@click="goToSlide(activeSlideIndex + 1)"
|
|
22
|
+
>
|
|
23
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
24
|
+
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
|
25
|
+
</svg>
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Slider container -->
|
|
31
|
+
<div ref="sliderRef" class="slider-viewport" @scroll="handleScroll">
|
|
32
|
+
<div
|
|
33
|
+
class="slider-track"
|
|
34
|
+
:style="{ transform: `translateX(${offsetX}px)` }"
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
v-for="(item, index) in items"
|
|
38
|
+
:key="index"
|
|
39
|
+
class="slide-item"
|
|
40
|
+
:class="{
|
|
41
|
+
'slide-visible': isSlideVisible(index),
|
|
42
|
+
'slide-entering': isSlideEntering(index),
|
|
43
|
+
'slide-exiting': isSlideExiting(index),
|
|
44
|
+
}"
|
|
45
|
+
:style="getSlideStyle(index)"
|
|
46
|
+
>
|
|
47
|
+
<component :is="itemComponent" :item="item" :index="index" />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup>
|
|
55
|
+
import {
|
|
56
|
+
ref,
|
|
57
|
+
computed,
|
|
58
|
+
onMounted,
|
|
59
|
+
onBeforeUnmount,
|
|
60
|
+
nextTick,
|
|
61
|
+
watch
|
|
62
|
+
} from "vue";
|
|
63
|
+
import { useRootStore } from "../../../stores/root";
|
|
64
|
+
import { capitalize } from "../../../composables/useUtils";
|
|
65
|
+
import { useNuxtApp } from "#imports";
|
|
66
|
+
import { useI18n } from "vue-i18n";
|
|
67
|
+
defineOptions({ name: "ListOrganismsSlider" });
|
|
68
|
+
const { $stores } = useNuxtApp();
|
|
69
|
+
const { locale } = useI18n();
|
|
70
|
+
const rootStore = useRootStore();
|
|
71
|
+
const props = defineProps({
|
|
72
|
+
type: {
|
|
73
|
+
type: String,
|
|
74
|
+
required: true
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const items = computed(() => $stores[props.type].items);
|
|
78
|
+
const sliderRef = ref(null);
|
|
79
|
+
const activeSlideIndex = ref(0);
|
|
80
|
+
const offsetX = ref(0);
|
|
81
|
+
const containerWidth = ref(0);
|
|
82
|
+
const slideWidth = ref(0);
|
|
83
|
+
const slidesPerView = ref(1);
|
|
84
|
+
const itemComponent = computed(() => {
|
|
85
|
+
return capitalize(props.type) + "SlidingItem";
|
|
86
|
+
});
|
|
87
|
+
const totalSlides = computed(() => items.value.length);
|
|
88
|
+
const canGoBack = computed(() => activeSlideIndex.value > 0);
|
|
89
|
+
const canGoForward = computed(() => {
|
|
90
|
+
const maxIndex = Math.max(0, totalSlides.value - slidesPerView.value);
|
|
91
|
+
return activeSlideIndex.value < maxIndex;
|
|
92
|
+
});
|
|
93
|
+
const visibleSlideIndices = computed(() => {
|
|
94
|
+
const start = activeSlideIndex.value;
|
|
95
|
+
const end = Math.min(start + slidesPerView.value, totalSlides.value);
|
|
96
|
+
return Array.from({ length: end - start }, (_, i) => start + i);
|
|
97
|
+
});
|
|
98
|
+
const calculateDimensions = () => {
|
|
99
|
+
if (!sliderRef.value) return;
|
|
100
|
+
containerWidth.value = sliderRef.value.offsetWidth;
|
|
101
|
+
const breakpoints = {
|
|
102
|
+
320: { slidesPerView: 1.2, spacing: 20 },
|
|
103
|
+
640: { slidesPerView: 2, spacing: 24 },
|
|
104
|
+
960: { slidesPerView: 2.5, spacing: 28 },
|
|
105
|
+
1280: { slidesPerView: 3, spacing: 32 },
|
|
106
|
+
1600: { slidesPerView: 3.5, spacing: 36 }
|
|
107
|
+
};
|
|
108
|
+
const currentBreakpoint = Object.entries(breakpoints).reverse().find(([width]) => window.innerWidth >= Number.parseInt(width));
|
|
109
|
+
const config = currentBreakpoint ? currentBreakpoint[1] : breakpoints[320];
|
|
110
|
+
slidesPerView.value = Math.floor(config.slidesPerView);
|
|
111
|
+
slideWidth.value = (containerWidth.value - config.spacing * (config.slidesPerView - 1)) / config.slidesPerView;
|
|
112
|
+
};
|
|
113
|
+
const isSlideVisible = (index) => {
|
|
114
|
+
return visibleSlideIndices.value.includes(index);
|
|
115
|
+
};
|
|
116
|
+
const isSlideEntering = (index) => {
|
|
117
|
+
const rightmostVisible = Math.max(...visibleSlideIndices.value);
|
|
118
|
+
return index === rightmostVisible && index > activeSlideIndex.value;
|
|
119
|
+
};
|
|
120
|
+
const isSlideExiting = (index) => {
|
|
121
|
+
const leftmostVisible = Math.min(...visibleSlideIndices.value);
|
|
122
|
+
return index === leftmostVisible && index < activeSlideIndex.value;
|
|
123
|
+
};
|
|
124
|
+
const getSlideStyle = (index) => {
|
|
125
|
+
const baseStyles = {
|
|
126
|
+
width: `${slideWidth.value}px`,
|
|
127
|
+
opacity: isSlideVisible(index) ? 1 : 0,
|
|
128
|
+
transform: `translateX(${index * (slideWidth.value + 24)}px)`,
|
|
129
|
+
transition: "all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)"
|
|
130
|
+
};
|
|
131
|
+
if (isSlideEntering(index)) {
|
|
132
|
+
baseStyles.transform += " translateX(20px)";
|
|
133
|
+
baseStyles.opacity = 0.8;
|
|
134
|
+
}
|
|
135
|
+
if (isSlideExiting(index)) {
|
|
136
|
+
baseStyles.transform += " translateX(-20px)";
|
|
137
|
+
baseStyles.opacity = 0.6;
|
|
138
|
+
}
|
|
139
|
+
return baseStyles;
|
|
140
|
+
};
|
|
141
|
+
const goToSlide = (targetIndex) => {
|
|
142
|
+
const maxIndex = Math.max(0, totalSlides.value - slidesPerView.value);
|
|
143
|
+
const newIndex = Math.max(0, Math.min(targetIndex, maxIndex));
|
|
144
|
+
if (newIndex === activeSlideIndex.value) return;
|
|
145
|
+
activeSlideIndex.value = newIndex;
|
|
146
|
+
updateSliderPosition();
|
|
147
|
+
};
|
|
148
|
+
const updateSliderPosition = () => {
|
|
149
|
+
const newOffset = -(activeSlideIndex.value * (slideWidth.value + 24));
|
|
150
|
+
const visualOffset = activeSlideIndex.value > 0 ? -40 : 0;
|
|
151
|
+
offsetX.value = newOffset + visualOffset;
|
|
152
|
+
};
|
|
153
|
+
const handleScroll = () => {
|
|
154
|
+
};
|
|
155
|
+
const handleResize = () => {
|
|
156
|
+
calculateDimensions();
|
|
157
|
+
updateSliderPosition();
|
|
158
|
+
};
|
|
159
|
+
onMounted(async () => {
|
|
160
|
+
await nextTick();
|
|
161
|
+
calculateDimensions();
|
|
162
|
+
updateSliderPosition();
|
|
163
|
+
window.addEventListener("resize", handleResize);
|
|
164
|
+
});
|
|
165
|
+
onBeforeUnmount(() => {
|
|
166
|
+
window.removeEventListener("resize", handleResize);
|
|
167
|
+
rootStore.resetState(props.type);
|
|
168
|
+
});
|
|
169
|
+
watch(
|
|
170
|
+
() => items.value,
|
|
171
|
+
() => {
|
|
172
|
+
nextTick(() => {
|
|
173
|
+
calculateDimensions();
|
|
174
|
+
updateSliderPosition();
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
{ deep: true }
|
|
178
|
+
);
|
|
179
|
+
try {
|
|
180
|
+
await rootStore.update(props.type, locale.value);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.log("error fetching update list: ", error);
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style scoped>
|
|
187
|
+
.slider-container{overflow:hidden;position:relative;width:100%}.slider-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:24px;padding:0 20px}.slide-counter{color:#666;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;font-weight:500}.navigation-controls{display:flex;gap:8px}.nav-button{align-items:center;background:#fff;border:1px solid #e0e0e0;border-radius:4px;color:#333;cursor:pointer;display:flex;height:40px;justify-content:center;transition:all .2s ease;width:40px}.nav-button:hover:not(:disabled){background:#f5f5f5;border-color:#d0d0d0}.nav-button:disabled{color:#999;cursor:not-allowed;opacity:.4}.slider-viewport{overflow:hidden;padding:0 20px;position:relative;width:100%}.slider-track{display:flex;transition:transform .4s cubic-bezier(.25,.46,.45,.94);will-change:transform}.slide-item{flex-shrink:0;margin-right:24px;transform-origin:center;will-change:transform,opacity}.slide-item:last-child{margin-right:0}.slide-entering{animation:slideInFromRight .4s cubic-bezier(.25,.46,.45,.94)}.slide-exiting{animation:slideOutToLeft .4s cubic-bezier(.25,.46,.45,.94)}@keyframes slideInFromRight{0%{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}@keyframes slideOutToLeft{0%{opacity:1;transform:translateX(0)}to{opacity:.6;transform:translateX(-40px)}}@media (max-width:640px){.slider-header{margin-bottom:20px;padding:0 16px}.slider-viewport{padding:0 16px}.slide-item{margin-right:16px}}@media (max-width:480px){.slide-counter{font-size:12px}.nav-button{height:36px;width:36px}.nav-button svg{height:20px;width:20px}}
|
|
188
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"license": "AGPL-3.0-only",
|
|
3
3
|
"main": "./dist/module.mjs",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.83",
|
|
5
5
|
"name": "@paris-ias/list",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "git+https://github.com/IEA-Paris/list.git",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@nuxt/kit": "^3.16.2",
|
|
12
|
-
"@vue/apollo-composable": "^4.2.2"
|
|
12
|
+
"@vue/apollo-composable": "^4.2.2",
|
|
13
|
+
"vuetify": "3.8.0"
|
|
13
14
|
},
|
|
14
15
|
"description": "Paris IAS List Module",
|
|
15
16
|
"devDependencies": {
|
|
@@ -38,8 +39,7 @@
|
|
|
38
39
|
"vite-plugin-graphql-loader": "^4.0.4",
|
|
39
40
|
"vitest": "^3.1.1",
|
|
40
41
|
"vue": "^3.5.13",
|
|
41
|
-
"vue-tsc": "^2.2.8"
|
|
42
|
-
"vuetify": "3.8.0"
|
|
42
|
+
"vue-tsc": "^2.2.8"
|
|
43
43
|
},
|
|
44
44
|
"exports": {
|
|
45
45
|
".": {
|