Skip to content

Commit 09f82f8

Browse files
committed
feat(calendar): adds recenter button
1 parent 0f4e970 commit 09f82f8

13 files changed

+178
-28
lines changed

src/components/common/buttons/FloatingButton.vue

+13-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { NFlex, NFloatButton, NIcon } from 'naive-ui';
33
44
import type { Component, PropType, Transition } from 'vue';
55
6-
import IconTopArrow from '~/components/icons/IconTopArrow.vue';
6+
import IconChevronUp from '~/components/icons/IconChevronUp.vue';
77
88
defineProps({
99
show: {
@@ -13,7 +13,11 @@ defineProps({
1313
icon: {
1414
type: Object as PropType<Component>,
1515
required: false,
16-
default: IconTopArrow,
16+
default: IconChevronUp,
17+
},
18+
width: {
19+
type: String,
20+
required: false,
1721
},
1822
});
1923
@@ -27,7 +31,12 @@ const emit = defineEmits<{
2731
<NFloatButton v-if="show" class="button" width="fit-content" @click="emit('onClick')">
2832
<NFlex size="small" align="center" justify="space-evenly">
2933
<NIcon :component="icon" />
30-
<span class="text">
34+
<span
35+
class="text"
36+
:style="{
37+
'--floating-button-width': width,
38+
}"
39+
>
3140
<slot />
3241
</span>
3342
</NFlex>
@@ -60,7 +69,7 @@ const emit = defineEmits<{
6069
}
6170
6271
&:hover .text {
63-
width: 4.5rem;
72+
width: var(--floating-button-width, 4.5rem);
6473
margin-left: 0.5rem;
6574
}
6675

src/components/common/list/ListItem.vue

+53-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
<script setup lang="ts">
22
import { NFlex, NImage, NSkeleton, NTime, NTimelineItem } from 'naive-ui';
33
4-
import { computed, defineProps, type PropType, ref, toRefs, watch } from 'vue';
4+
import {
5+
computed,
6+
defineProps,
7+
onBeforeUnmount,
8+
onMounted,
9+
type PropType,
10+
ref,
11+
toRefs,
12+
watch,
13+
} from 'vue';
514
615
import type { ListScrollItem } from '~/components/common/list/ListScroll.model';
716
@@ -54,13 +63,34 @@ const props = defineProps({
5463
type: Boolean,
5564
required: false,
5665
},
66+
scrollIntoView: {
67+
type: Boolean,
68+
required: false,
69+
},
5770
});
5871
5972
const emit = defineEmits<{
6073
(e: 'onHover', event: { index: number; item: ListScrollItem; hover: boolean }): void;
74+
(
75+
e: 'onScrollIntoView',
76+
event: { item: ListScrollItem; index: number; ref?: HTMLDivElement },
77+
): void;
78+
(
79+
e: 'onScrollOutOfView',
80+
event: { item: ListScrollItem; index: number; ref?: HTMLDivElement },
81+
): void;
6182
}>();
6283
63-
const { item, index, noHeader, nextHasHeader, poster, episode, hideDate } = toRefs(props);
84+
const {
85+
item,
86+
index,
87+
noHeader,
88+
nextHasHeader,
89+
poster,
90+
episode,
91+
hideDate,
92+
scrollIntoView,
93+
} = toRefs(props);
6494
6595
const onHover = (_hover: boolean) => {
6696
emit('onHover', { index: index?.value, item: item?.value, hover: _hover });
@@ -105,10 +135,31 @@ const getPosters = (_item: ListScrollItem) => {
105135
};
106136
107137
watch(item, getPosters, { immediate: true, flush: 'post' });
138+
139+
const itemRef = ref<HTMLElement & InstanceType<typeof NTimelineItem>>();
140+
141+
onMounted(() => {
142+
if (!scrollIntoView.value) return;
143+
emit('onScrollIntoView', {
144+
item: item?.value,
145+
index: index.value,
146+
ref: itemRef.value?.$el,
147+
});
148+
});
149+
150+
onBeforeUnmount(() => {
151+
if (!scrollIntoView.value) return;
152+
emit('onScrollOutOfView', {
153+
item: item?.value,
154+
index: index.value,
155+
ref: itemRef.value?.$el,
156+
});
157+
});
108158
</script>
109159

110160
<template>
111161
<NTimelineItem
162+
ref="itemRef"
112163
:key="item.id"
113164
class="timeline-item"
114165
:class="{

src/components/common/list/ListScroll.model.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type VirtualListProps = {
1616
paddingTop?: string | number;
1717
paddingBottom?: string | number;
1818
};
19+
export type VirtualListScrollToOptions = Parameters<VirtualListInst['scrollTo']>[0];
1920

2021
export type OnScroll = (listRef: Ref<VirtualListRef | undefined>) => void;
2122
export type OnUpdated = (listRef: Ref<VirtualListRef | undefined>) => void;

src/components/common/list/ListScroll.vue

+15
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ const props = defineProps({
4747
type: Boolean,
4848
required: false,
4949
},
50+
scrollIntoView: {
51+
type: Array as PropType<ListScrollItem['id'][]>,
52+
required: false,
53+
},
5054
scrollThreshold: {
5155
type: Number,
5256
required: false,
@@ -68,6 +72,14 @@ const emits = defineEmits<{
6872
pageSize: number;
6973
},
7074
): void;
75+
(
76+
e: 'onScrollIntoView',
77+
event: { item: ListScrollItem; index: number; ref?: HTMLDivElement },
78+
): void;
79+
(
80+
e: 'onScrollOutOfView',
81+
event: { item: ListScrollItem; index: number; ref?: HTMLDivElement },
82+
): void;
7183
}>();
7284
7385
defineExpose({
@@ -160,7 +172,10 @@ const onLoadMore = (payload: { page: number; pageCount: number; pageSize: number
160172
:hide-date="hideDate"
161173
:episode="episode"
162174
:hover="hoverDate === item.date?.current?.toDateString()"
175+
:scroll-into-view="scrollIntoView?.includes(item.id)"
163176
@on-hover="onHover"
177+
@on-scroll-into-view="(...args) => $emit('onScrollIntoView', ...args)"
178+
@on-scroll-out-of-view="(...args) => $emit('onScrollOutOfView', ...args)"
164179
>
165180
<slot :item="item" :index="item.index" :loading="item.loading" />
166181
</ListItem>

src/components/common/navbar/NavbarPageSizeSelect.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { NIcon, NSelect, NTooltip, type SelectOption } from 'naive-ui';
33
44
import { computed, defineProps, onMounted, ref, toRefs, watch } from 'vue';
55
6-
import IconChevron from '~/components/icons/IconChevron.vue';
6+
import IconChevron from '~/components/icons/IconChevronDownSmall.vue';
77
import IconPage from '~/components/icons/IconPage.vue';
88
import IconPageDouble from '~/components/icons/IconPageDouble.vue';
99
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
3+
<g transform="rotate(-90 12 12)">
4+
<path
5+
fill="none"
6+
stroke="currentColor"
7+
stroke-dasharray="10"
8+
stroke-dashoffset="10"
9+
stroke-linecap="round"
10+
stroke-width="2"
11+
d="M8 12L15 5M8 12L15 19"
12+
>
13+
<animate
14+
fill="freeze"
15+
attributeName="stroke-dashoffset"
16+
dur="0.225s"
17+
values="10;0"
18+
/>
19+
</path>
20+
</g>
21+
</svg>
22+
</template>
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
3+
<g transform="rotate(-90 12 12) translate(24 0) scale(-1 1)">
4+
<path
5+
fill="none"
6+
stroke="currentColor"
7+
stroke-dasharray="10"
8+
stroke-dashoffset="10"
9+
stroke-linecap="round"
10+
stroke-width="2"
11+
d="M8 12L15 5M8 12L15 19"
12+
>
13+
<animate
14+
fill="freeze"
15+
attributeName="stroke-dashoffset"
16+
dur="0.225s"
17+
values="10;0"
18+
/>
19+
</path>
20+
</g>
21+
</svg>
22+
</template>

src/components/views/calendar/CalendarComponent.vue

+45-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
<script lang="ts" setup>
2-
import { computed, watch } from 'vue';
2+
import { computed, ref, watch } from 'vue';
3+
4+
import type {
5+
VirtualListRef,
6+
VirtualListScrollToOptions,
7+
} from '~/components/common/list/ListScroll.model';
38
49
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
5-
import { useBackToTop } from '~/components/common/buttons/use-back-to-top';
610
import ListScroll from '~/components/common/list/ListScroll.vue';
711
import { useListScroll } from '~/components/common/list/use-list-scroll';
12+
import IconChevronDown from '~/components/icons/IconChevronDown.vue';
13+
import IconChevronUp from '~/components/icons/IconChevronUp.vue';
14+
815
import { useCalendarStore, useCalendarStoreRefs } from '~/stores/data/calendar.store';
916
import { useI18n } from '~/utils';
1017
import { watchUserChange } from '~/utils/store.utils';
@@ -16,28 +23,29 @@ const { fetchCalendar, clearState } = useCalendarStore();
1623
1724
const list = useListScroll(calendar, 'date');
1825
19-
const { scrolled, listRef, onClick } = useBackToTop();
20-
2126
const today = computed(() => {
2227
return list.value.find(
2328
item => item.date?.current.toLocaleDateString() === new Date().toLocaleDateString(),
2429
);
2530
});
2631
27-
watchUserChange({
28-
mounted: () => {
29-
const unsub = watch(today, async () => {
30-
if (!today.value) return;
31-
32-
const _listRef = listRef.value?.list;
33-
if (!_listRef) return;
32+
const listRef = ref<{ list: VirtualListRef }>();
3433
35-
await _listRef.$nextTick();
34+
const scrollToToday = (options?: VirtualListScrollToOptions) => {
35+
if (!today.value) return;
36+
if (!listRef.value?.list) return;
3637
37-
_listRef.scrollTo({
38-
top: today.value.index * 145,
39-
});
38+
listRef.value?.list.scrollTo({
39+
top: today.value.index * 145,
40+
...options,
41+
});
42+
};
4043
44+
watchUserChange({
45+
mounted: () => {
46+
const unsub = watch(today, async () => {
47+
await listRef.value?.list.$nextTick();
48+
scrollToToday();
4149
unsub();
4250
});
4351
},
@@ -47,6 +55,18 @@ watchUserChange({
4755
if (active) fetchCalendar();
4856
},
4957
});
58+
59+
const scrolledOut = ref(false);
60+
const scrolledDown = ref(true);
61+
const onClick = () => scrollToToday({ behavior: 'smooth' });
62+
const onScrollIntoOutOfView = (_scrolled: boolean, _itemRef?: HTMLDivElement) => {
63+
scrolledOut.value = _scrolled;
64+
if (!_scrolled || !_itemRef) return;
65+
scrolledDown.value = _itemRef.getBoundingClientRect().top > 0;
66+
};
67+
const recenterIcon = computed(() =>
68+
scrolledDown.value ? IconChevronDown : IconChevronUp,
69+
);
5070
</script>
5171

5272
<template>
@@ -57,15 +77,21 @@ watchUserChange({
5777
:loading="loading"
5878
:scroll-threshold="300"
5979
episode
60-
@on-scroll="scrolled = true"
61-
@on-scroll-top="scrolled = false"
80+
:scroll-into-view="today?.id ? [today?.id] : []"
81+
@on-scroll-into-view="e => onScrollIntoOutOfView(false, e.ref)"
82+
@on-scroll-out-of-view="e => onScrollIntoOutOfView(true, e.ref)"
6283
>
6384
<template #default>
6485
<!-- TODO buttons here-->
6586
</template>
6687
</ListScroll>
67-
<FloatingButton :show="scrolled" @on-click="onClick">
68-
{{ i18n('back_to_top', 'common', 'button') }}
88+
<FloatingButton
89+
:show="scrolledOut"
90+
width="2.5rem"
91+
:icon="recenterIcon"
92+
@on-click="onClick"
93+
>
94+
{{ i18n('recenter', 'common', 'button') }}
6995
</FloatingButton>
7096
</div>
7197
</template>

src/components/views/history/HistoryNavbar.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { computed, defineProps, ref } from 'vue';
55
66
import NavbarPageSizeSelect from '~/components/common/navbar/NavbarPageSizeSelect.vue';
77
import IconCalendar from '~/components/icons/IconCalendar.vue';
8-
import IconChevron from '~/components/icons/IconChevron.vue';
8+
import IconChevron from '~/components/icons/IconChevronDownSmall.vue';
99
import IconLoop from '~/components/icons/IconLoop.vue';
1010
1111
import { useHistoryStore, useHistoryStoreRefs } from '~/stores/data/history.store';

src/i18n/en/common/button.json

+4
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@
22
"common__button__back_to_top": {
33
"message": "Back to top",
44
"description": "Button to scroll to the top of the history."
5+
},
6+
"common__button__recenter": {
7+
"message": "Today",
8+
"description": "Button to recenter the timeline on today."
59
}
610
}

src/stores/data/calendar.store.ts

-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ export const useCalendarStore = defineStore('data.calendar', () => {
168168
} else {
169169
calendar.value = [...calendar.value.filter(c => c.type !== 'placeholder'), ...spacedData];
170170
}
171-
console.info('Fetched Calendar', calendar.value);
172171
} catch (e) {
173172
console.error('Failed to fetch history');
174173
calendar.value = calendar.value.filter(c => c.type !== 'placeholder');

src/stores/data/history.store.ts

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const useHistoryStore = defineStore('data.history', () => {
8383
};
8484

8585
const setHistoryRange = async ({ start, end }: { start?: Date; end?: Date } = {}) => {
86+
if (start === historyStart.value && end === historyEnd.value) return;
8687
historyStart.value = start;
8788
historyEnd.value = end;
8889
const result = fetchHistory({ start, end });

0 commit comments

Comments
 (0)