Skip to content

Commit 8cdcaac

Browse files
committed
feat(calendar): re-factor composables
1 parent b74e53f commit 8cdcaac

File tree

5 files changed

+142
-92
lines changed

5 files changed

+142
-92
lines changed

src/components/views/releases/ReleasesComponent.vue

+12-71
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,32 @@
11
<script lang="ts" setup>
2-
import { computed, ref, watch } from 'vue';
3-
4-
import type {
5-
VirtualListRef,
6-
VirtualListScrollToOptions,
7-
} from '~/models/list-scroll.model';
2+
import { watch } from 'vue';
83
94
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
105
import ListScroll from '~/components/common/list/ListScroll.vue';
116
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';
147
158
import { usePanelItem } from '~/components/views/panel/use-panel-item';
16-
import { useCalendarStore, useCalendarStoreRefs } from '~/stores/data/calendar.store';
9+
import { useReleasesStore, useReleasesStoreRefs } from '~/stores/data/releases.store';
10+
import { useCalendar, useCenterButton } from '~/utils/calendar.utils';
1711
import { useI18n } from '~/utils/i18n.utils';
1812
import { watchUserChange } from '~/utils/store.utils';
1913
2014
const i18n = useI18n('calendar');
2115
22-
const { calendar, loading, center, filteredCalendar } = useCalendarStoreRefs();
23-
const { fetchCalendar, clearState } = useCalendarStore();
16+
const { releases, loading, center } = useReleasesStoreRefs();
17+
const { fetchReleases, clearState } = useReleasesStore();
2418
25-
const list = useListScroll(filteredCalendar, 'date');
19+
const list = useListScroll(releases, 'date');
2620
27-
const centerItem = computed(() => {
28-
return list.value.find(
29-
item => item.date?.current.toLocaleDateString() === center.value.toLocaleDateString(),
30-
);
31-
});
21+
const { centerItem, centerIsToday, scrolledOut, recenterIcon, onScrollIntoOutOfView } =
22+
useCenterButton({ list, center });
3223
33-
const centerIsToday = computed(() => {
34-
return (
35-
centerItem.value?.date?.current.toLocaleDateString() ===
36-
new Date().toLocaleDateString()
37-
);
24+
const { onClick, onScrollTop, onScrollBottom, reload } = useCalendar({
25+
list,
26+
centerItem,
27+
fetchData: fetchReleases,
3828
});
3929
40-
const listRef = ref<{ list: VirtualListRef }>();
41-
42-
const scrollTo = (
43-
options?: VirtualListScrollToOptions,
44-
index = centerItem.value?.index,
45-
) => {
46-
if (index === undefined) return;
47-
if (!listRef.value?.list) return;
48-
49-
listRef.value?.list.scrollTo({
50-
top: index * 145,
51-
...options,
52-
});
53-
};
54-
55-
const reload = async () => {
56-
const promise = fetchCalendar();
57-
// watch for loading changes and recenter
58-
const unsub = watch(list, async () => scrollTo());
59-
await promise;
60-
scrollTo();
61-
unsub();
62-
};
63-
6430
watch(center, () => reload());
6531
6632
watchUserChange({
@@ -74,31 +40,6 @@ watchUserChange({
7440
},
7541
});
7642
77-
const scrolledOut = ref(false);
78-
const scrolledDown = ref(true);
79-
const onClick = () => scrollTo({ behavior: 'smooth' });
80-
const onScrollIntoOutOfView = (_scrolled: boolean, _itemRef?: HTMLDivElement) => {
81-
scrolledOut.value = _scrolled;
82-
if (!_scrolled || !_itemRef) return;
83-
scrolledDown.value = _itemRef.getBoundingClientRect().top > 0;
84-
};
85-
const recenterIcon = computed(() =>
86-
scrolledDown.value ? IconChevronDown : IconChevronUp,
87-
);
88-
89-
const onScrollTop = async () => {
90-
const first = list.value[0];
91-
await fetchCalendar('start');
92-
93-
listRef.value?.list.scrollTo({
94-
top: (list.value.findIndex(item => item.id === first.id) - 1) * 145,
95-
});
96-
};
97-
98-
const onScrollBottom = async () => {
99-
await fetchCalendar('end');
100-
};
101-
10243
const { onItemClick } = usePanelItem();
10344
</script>
10445

src/stores/data/calendar.store.ts

+19-10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const CalendarStoreConstants = {
2121
} as const;
2222

2323
type CalendarState = {
24+
date?: number;
2425
weeks: number;
2526
startCalendar: number;
2627
endCalendar: number;
@@ -31,7 +32,7 @@ export const useCalendarStore = defineStore(CalendarStoreConstants.Store, () =>
3132
const loading = ref(true);
3233
const calendar = ref<CalendarItem[]>([]);
3334

34-
const center = ref(new Date());
35+
const center = ref<Date>(new Date());
3536
const startCalendar = ref<Date>(DateUtils.weeks.previous(1, center.value));
3637
const endCalendar = ref<Date>(DateUtils.weeks.next(1, center.value));
3738

@@ -43,24 +44,27 @@ export const useCalendarStore = defineStore(CalendarStoreConstants.Store, () =>
4344
const calendarErrors = reactive<ErrorDictionary>({});
4445
ErrorService.registerDictionary('calendar', calendarErrors);
4546

46-
const clearState = (date: Date = new Date()) => {
47-
calendar.value = [];
48-
center.value = date;
49-
startCalendar.value = DateUtils.weeks.previous(1, center.value);
50-
endCalendar.value = DateUtils.weeks.next(1, center.value);
51-
clearProxy(calendarErrors);
52-
};
53-
54-
const saveState = async () =>
47+
const saveState = async (clear = false) =>
5548
storage.local.set<CalendarState>(CalendarStoreConstants.Store, {
49+
date: clear ? undefined : center.value.getTime(),
5650
weeks: weeks.value,
5751
startCalendar: startCalendar.value.getTime(),
5852
endCalendar: endCalendar.value.getTime(),
5953
});
6054

55+
const clearState = (date?: Date) => {
56+
calendar.value = [];
57+
center.value = date ?? new Date();
58+
startCalendar.value = DateUtils.weeks.previous(1, center.value);
59+
endCalendar.value = DateUtils.weeks.next(1, center.value);
60+
clearProxy(calendarErrors);
61+
saveState(!date).catch(e => logger.error('Failed to save calendar state', e));
62+
};
63+
6164
const restoreState = async () => {
6265
const restored = await storage.local.get<CalendarState>(CalendarStoreConstants.Store);
6366

67+
if (restored?.date) center.value = new Date(restored.date);
6468
if (restored?.weeks) weeks.value = restored.weeks;
6569
if (restored?.startCalendar) startCalendar.value = new Date(restored.startCalendar);
6670
if (restored?.endCalendar) endCalendar.value = new Date(restored.endCalendar);
@@ -139,6 +143,10 @@ export const useCalendarStore = defineStore(CalendarStoreConstants.Store, () =>
139143

140144
const filteredCalendar = useSearchFilter(calendar, filter, 'date');
141145

146+
const initCalendarStore = async () => {
147+
await restoreState();
148+
};
149+
142150
return {
143151
clearState,
144152
saveState,
@@ -151,6 +159,7 @@ export const useCalendarStore = defineStore(CalendarStoreConstants.Store, () =>
151159
filter,
152160
center,
153161
filteredCalendar,
162+
initCalendarStore,
154163
};
155164
});
156165

src/stores/data/releases.store.ts

+24-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TraktService } from '~/services/trakt.service';
1313
import { logger } from '~/stores/settings/log.store';
1414
import { storage } from '~/utils/browser/browser-storage.utils';
1515
import { type CalendarItem, getEmptyWeeks, getLoadingPlaceholder, spaceDate } from '~/utils/calendar.utils';
16+
import { getIntlRegion, getNavigatorRegion } from '~/utils/intl.utils';
1617
import { ErrorCount, type ErrorDictionary } from '~/utils/retry.utils';
1718
import { clearProxy } from '~/utils/vue.utils';
1819

@@ -21,6 +22,7 @@ const ReleasesStoreConstants = {
2122
} as const;
2223

2324
type ReleaseState = {
25+
date?: number;
2426
weeks: number;
2527
region?: TmdbConfigurationCounty;
2628
types: TmdbMovieReleaseTypes[];
@@ -33,7 +35,7 @@ export const useReleasesStore = defineStore(ReleasesStoreConstants.Store, () =>
3335
const loading = ref(true);
3436
const releases = ref<CalendarItem[]>([]);
3537

36-
const center = ref(new Date());
38+
const center = ref<Date>(new Date());
3739
const startCalendar = ref<Date>(DateUtils.weeks.previous(1, center.value));
3840
const endCalendar = ref<Date>(DateUtils.weeks.next(1, center.value));
3941

@@ -48,26 +50,29 @@ export const useReleasesStore = defineStore(ReleasesStoreConstants.Store, () =>
4850
const releasesErrors = reactive<ErrorDictionary>({});
4951
ErrorService.registerDictionary('releases', releasesErrors);
5052

51-
const clearState = (date: Date = new Date()) => {
52-
releases.value = [];
53-
center.value = date;
54-
startCalendar.value = DateUtils.weeks.previous(1, center.value);
55-
endCalendar.value = DateUtils.weeks.next(1, center.value);
56-
clearProxy(releasesErrors);
57-
};
58-
59-
const saveState = async () =>
53+
const saveState = async (clear = false) =>
6054
storage.local.set<ReleaseState>(ReleasesStoreConstants.Store, {
55+
date: clear ? undefined : center.value.getTime(),
6156
weeks: weeks.value,
6257
region: region.value,
6358
types: types.value,
6459
startCalendar: startCalendar.value.getTime(),
6560
endCalendar: endCalendar.value.getTime(),
6661
});
6762

63+
const clearState = (date?: Date) => {
64+
releases.value = [];
65+
center.value = date ?? new Date();
66+
startCalendar.value = DateUtils.weeks.previous(1, center.value);
67+
endCalendar.value = DateUtils.weeks.next(1, center.value);
68+
clearProxy(releasesErrors);
69+
saveState(!date).catch(e => logger.error('Failed to save calendar state', e));
70+
};
71+
6872
const restoreState = async () => {
6973
const restored = await storage.local.get<ReleaseState>(ReleasesStoreConstants.Store);
7074

75+
if (restored?.date) center.value = new Date(restored.date);
7176
if (restored?.weeks) weeks.value = restored.weeks;
7277
if (restored?.region) region.value = restored.region;
7378
if (restored?.types) types.value = Object.values(restored.types);
@@ -79,6 +84,10 @@ export const useReleasesStore = defineStore(ReleasesStoreConstants.Store, () =>
7984
regionLoading.value = true;
8085
try {
8186
regions.value = await TraktService.providers.regions();
87+
if (!region.value) {
88+
const locale = getIntlRegion() || getNavigatorRegion();
89+
if (locale) region.value = regions.value.find(r => r.iso_3166_1 === locale);
90+
}
8291
} catch (e) {
8392
logger.error('Failed to fetch regions');
8493
NotificationService.error('Failed to fetch regions', e);
@@ -154,6 +163,10 @@ export const useReleasesStore = defineStore(ReleasesStoreConstants.Store, () =>
154163
}
155164
};
156165

166+
const initReleasesStore = async () => {
167+
await restoreState();
168+
};
169+
157170
return {
158171
clearState,
159172
saveState,
@@ -181,6 +194,7 @@ export const useReleasesStore = defineStore(ReleasesStoreConstants.Store, () =>
181194
saveState().catch(error => logger.error('Failed to save release types state', error));
182195
},
183196
}),
197+
initReleasesStore,
184198
};
185199
});
186200

src/utils/calendar.utils.ts

+80-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { DateUtils } from '@dvcol/common-utils/common/date';
2+
import { computed, ref, type Ref, watch } from 'vue';
23

34
import type { TraktCalendarMovie, TraktCalendarShow } from '@dvcol/trakt-http-client/models';
45

5-
import { type ListScrollItem, type ListScrollItemTag, ListScrollItemType } from '~/models/list-scroll.model';
6+
import IconChevronDown from '~/components/icons/IconChevronDown.vue';
7+
import IconChevronUp from '~/components/icons/IconChevronUp.vue';
8+
import {
9+
type ListScrollItem,
10+
type ListScrollItemTag,
11+
ListScrollItemType,
12+
type VirtualListRef,
13+
type VirtualListScrollToOptions,
14+
} from '~/models/list-scroll.model';
615

716
export type CalendarItem = (TraktCalendarShow | TraktCalendarMovie | Record<never, never>) & {
817
id: ListScrollItem['id'];
@@ -77,3 +86,73 @@ export const spaceDate = (data: CalendarItem[], startDate: Date, endDate: Date):
7786
// if no data in response fill with placeholders
7887
return spacedData;
7988
};
89+
90+
export const useCenterButton = ({ center, list }: { center: Ref<Date>; list: Ref<ListScrollItem[]> }) => {
91+
const centerItem = computed(() => {
92+
return list.value.find(item => item.date?.current.toLocaleDateString() === center.value.toLocaleDateString());
93+
});
94+
95+
const centerIsToday = computed(() => {
96+
return centerItem.value?.date?.current.toLocaleDateString() === new Date().toLocaleDateString();
97+
});
98+
99+
const scrolledOut = ref(false);
100+
const scrolledDown = ref(true);
101+
const onScrollIntoOutOfView = (_scrolled: boolean, _itemRef?: HTMLDivElement) => {
102+
scrolledOut.value = _scrolled;
103+
if (!_scrolled || !_itemRef) return;
104+
scrolledDown.value = _itemRef.getBoundingClientRect().top > 0;
105+
};
106+
const recenterIcon = computed(() => (scrolledDown.value ? IconChevronDown : IconChevronUp));
107+
108+
return { centerItem, centerIsToday, scrolledOut, onScrollIntoOutOfView, recenterIcon };
109+
};
110+
111+
export const useCalendar = ({
112+
list,
113+
centerItem,
114+
fetchData,
115+
}: {
116+
list: Ref<ListScrollItem[]>;
117+
centerItem: Ref<ListScrollItem | undefined>;
118+
fetchData: (mode?: 'start' | 'end' | 'reload') => Promise<unknown>;
119+
}) => {
120+
const listRef = ref<{ list: VirtualListRef }>();
121+
const scrollTo = (options?: VirtualListScrollToOptions, index = centerItem.value?.index) => {
122+
if (index === undefined) return;
123+
if (!listRef.value?.list) return;
124+
125+
listRef.value?.list.scrollTo({
126+
top: index * 145,
127+
...options,
128+
});
129+
};
130+
131+
const reload = async () => {
132+
const promise = fetchData();
133+
// watch for loading changes and recenter
134+
const unsub = watch(list, async () => scrollTo());
135+
console.info('Reloading calendar', {
136+
list: list.value.map(item => JSON.parse(JSON.stringify(item))),
137+
});
138+
await promise;
139+
scrollTo();
140+
unsub();
141+
};
142+
143+
const onClick = () => scrollTo({ behavior: 'smooth' });
144+
145+
const onScrollTop = async () => {
146+
const first = list.value[0];
147+
await fetchData('start');
148+
149+
listRef.value?.list.scrollTo({
150+
top: (list.value.findIndex(item => item.id === first.id) - 1) * 145,
151+
});
152+
};
153+
154+
const onScrollBottom = async () => {
155+
await fetchData('end');
156+
};
157+
return { onClick, onScrollTop, onScrollBottom, reload };
158+
};

src/utils/intl.utils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const getIntl = () => Intl.DateTimeFormat().resolvedOptions();
2+
export const getIntlLocale = () => getIntl().locale;
3+
export const getIntlLanguage = () => getIntlLocale().split('-').shift();
4+
export const getIntlRegion = () => getIntlLocale().split('-').pop();
5+
6+
export const getNavigatorLanguage = () => navigator?.language?.split('-').shift();
7+
export const getNavigatorRegion = () => navigator?.language?.split('-').pop();

0 commit comments

Comments
 (0)