| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | <template> | 
					
						
							| 
									
										
										
										
											2023-08-23 12:30:59 -05:00
										 |  |  |   <div style="height: 100%;"> | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |     <v-row class="my-0 mx-7"> | 
					
						
							|  |  |  |       <v-spacer /> | 
					
						
							|  |  |  |       <v-col class="text-right"> | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  |         <!-- Filters --> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |         <v-menu | 
					
						
							|  |  |  |           offset-y | 
					
						
							|  |  |  |           bottom | 
					
						
							|  |  |  |           start | 
					
						
							|  |  |  |           nudge-bottom="3" | 
					
						
							|  |  |  |           :close-on-content-click="false" | 
					
						
							|  |  |  |         > | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |           <template #activator="{ props: activatorProps }"> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |             <v-badge | 
					
						
							|  |  |  |               :content="filterBadgeCount" | 
					
						
							|  |  |  |               :model-value="filterBadgeCount > 0" | 
					
						
							|  |  |  |               bordered | 
					
						
							|  |  |  |             > | 
					
						
							|  |  |  |               <v-btn | 
					
						
							|  |  |  |                 class="rounded-circle" | 
					
						
							|  |  |  |                 size="small" | 
					
						
							|  |  |  |                 color="info" | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |                 v-bind="activatorProps" | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |                 icon | 
					
						
							|  |  |  |               > | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  |                 <v-icon> {{ $globals.icons.filter }} </v-icon> | 
					
						
							|  |  |  |               </v-btn> | 
					
						
							|  |  |  |             </v-badge> | 
					
						
							|  |  |  |           </template> | 
					
						
							|  |  |  |           <v-card> | 
					
						
							|  |  |  |             <v-list> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |               <v-list-item | 
					
						
							|  |  |  |                 :prepend-icon="preferences.orderDirection === 'asc' ? $globals.icons.sortCalendarDescending : $globals.icons.sortCalendarAscending" | 
					
						
							|  |  |  |                 :title="preferences.orderDirection === 'asc' ? $t('general.sort-descending') : $t('general.sort-ascending')" | 
					
						
							|  |  |  |                 @click="reverseSort" | 
					
						
							|  |  |  |               /> | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  |               <v-divider /> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |               <v-list-item | 
					
						
							|  |  |  |                 v-for="option, idx in eventTypeFilterState" | 
					
						
							|  |  |  |                 :key="idx" | 
					
						
							|  |  |  |               > | 
					
						
							|  |  |  |                 <v-checkbox | 
					
						
							|  |  |  |                   :model-value="option.checked" | 
					
						
							|  |  |  |                   color="primary" | 
					
						
							|  |  |  |                   readonly | 
					
						
							|  |  |  |                   @click="toggleEventTypeOption(option.value)" | 
					
						
							|  |  |  |                 > | 
					
						
							|  |  |  |                   <template #label> | 
					
						
							|  |  |  |                     <v-icon start> | 
					
						
							|  |  |  |                       {{ option.icon }} | 
					
						
							|  |  |  |                     </v-icon> | 
					
						
							|  |  |  |                     {{ option.label }} | 
					
						
							|  |  |  |                   </template> | 
					
						
							|  |  |  |                 </v-checkbox> | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  |               </v-list-item> | 
					
						
							|  |  |  |             </v-list> | 
					
						
							|  |  |  |           </v-card> | 
					
						
							|  |  |  |         </v-menu> | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |       </v-col> | 
					
						
							|  |  |  |     </v-row> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     <v-divider class="mx-2" /> | 
					
						
							| 
									
										
										
										
											2023-08-21 20:41:18 +02:00
										 |  |  |     <div | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |       v-if="timelineEvents.length" | 
					
						
							|  |  |  |       id="timeline-container" | 
					
						
							|  |  |  |       height="fit-content" | 
					
						
							|  |  |  |       width="100%" | 
					
						
							|  |  |  |       class="px-1" | 
					
						
							| 
									
										
										
										
											2023-08-23 12:30:59 -05:00
										 |  |  |       :style="maxHeight ? `max-height: ${maxHeight}; overflow-y: auto;` : ''" | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |     > | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       <v-timeline | 
					
						
							| 
									
										
										
										
											2025-06-30 17:25:32 +02:00
										 |  |  |         :density="$vuetify.display.smAndDown ? ($vuetify.display.xs ? 'compact' : 'comfortable') : undefined" | 
					
						
							|  |  |  |         justify="center" | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |         class="timeline" | 
					
						
							|  |  |  |       > | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |         <RecipeTimelineItem | 
					
						
							|  |  |  |           v-for="(event, index) in timelineEvents" | 
					
						
							|  |  |  |           :key="event.id" | 
					
						
							|  |  |  |           :event="event" | 
					
						
							|  |  |  |           :recipe="recipes.get(event.recipeId)" | 
					
						
							|  |  |  |           :show-recipe-cards="showRecipeCards" | 
					
						
							| 
									
										
										
										
											2025-06-30 17:25:32 +02:00
										 |  |  |           :width="$vuetify.display.smAndDown ? '100%' : undefined" | 
					
						
							| 
									
										
										
										
											2025-09-13 15:36:18 -05:00
										 |  |  |           @update="updateTimelineEvent(index, $event)" | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |           @delete="deleteTimelineEvent(index)" | 
					
						
							|  |  |  |         /> | 
					
						
							|  |  |  |       </v-timeline> | 
					
						
							| 
									
										
										
										
											2023-08-21 20:41:18 +02:00
										 |  |  |     </div> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     <v-card | 
					
						
							|  |  |  |       v-else-if="!loading" | 
					
						
							|  |  |  |       class="mt-2" | 
					
						
							|  |  |  |     > | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |       <v-card-title class="justify-center pa-9"> | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  |         {{ $t("recipe.timeline-no-events-found-try-adjusting-filters") }} | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |       </v-card-title> | 
					
						
							|  |  |  |     </v-card> | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     <div | 
					
						
							|  |  |  |       v-if="loading" | 
					
						
							|  |  |  |       class="mb-3 text-center" | 
					
						
							|  |  |  |     > | 
					
						
							|  |  |  |       <AppLoader | 
					
						
							|  |  |  |         :loading="loading" | 
					
						
							|  |  |  |         :waiting-text="$t('general.loading-events')" | 
					
						
							|  |  |  |       /> | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |     </div> | 
					
						
							|  |  |  |   </div> | 
					
						
							|  |  |  | </template> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  | <script setup lang="ts"> | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | import { useThrottleFn, whenever } from "@vueuse/core"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import RecipeTimelineItem from "./RecipeTimelineItem.vue"; | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | import { useTimelinePreferences } from "~/composables/use-users/preferences"; | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  | import { useTimelineEventTypes } from "~/composables/recipes/use-recipe-timeline-events"; | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | import { useAsyncKey } from "~/composables/use-utils"; | 
					
						
							|  |  |  | import { alert } from "~/composables/use-toast"; | 
					
						
							|  |  |  | import { useUserApi } from "~/composables/api"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import type { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate, TimelineEventType } from "~/lib/api/types/recipe"; | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  | interface Props { | 
					
						
							|  |  |  |   modelValue?: boolean; | 
					
						
							|  |  |  |   queryFilter: string; | 
					
						
							|  |  |  |   maxHeight?: number | string; | 
					
						
							|  |  |  |   showRecipeCards?: boolean; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const props = withDefaults(defineProps<Props>(), { | 
					
						
							|  |  |  |   modelValue: false, | 
					
						
							|  |  |  |   maxHeight: undefined, | 
					
						
							|  |  |  |   showRecipeCards: false, | 
					
						
							|  |  |  | }); | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  | const api = useUserApi(); | 
					
						
							|  |  |  | const i18n = useI18n(); | 
					
						
							|  |  |  | const preferences = useTimelinePreferences(); | 
					
						
							|  |  |  | const { eventTypeOptions } = useTimelineEventTypes(); | 
					
						
							|  |  |  | const loading = ref(true); | 
					
						
							|  |  |  | const ready = ref(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const page = ref(1); | 
					
						
							|  |  |  | const perPage = 32; | 
					
						
							|  |  |  | const hasMore = ref(true); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const timelineEvents = ref([] as RecipeTimelineEventOut[]); | 
					
						
							|  |  |  | const recipes = new Map<string, Recipe>(); | 
					
						
							|  |  |  | const filterBadgeCount = computed(() => eventTypeOptions.value.length - preferences.value.types.length); | 
					
						
							|  |  |  | const eventTypeFilterState = computed(() => { | 
					
						
							|  |  |  |   return eventTypeOptions.value.map((option) => { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       ...option, | 
					
						
							|  |  |  |       checked: preferences.value.types.includes(option.value), | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |     }; | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |   }); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | const screenBuffer = 4; | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  | whenever( | 
					
						
							|  |  |  |   () => props.modelValue, | 
					
						
							|  |  |  |   () => { | 
					
						
							|  |  |  |     initializeTimelineEvents(); | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  | ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Preferences
 | 
					
						
							|  |  |  | function reverseSort() { | 
					
						
							|  |  |  |   if (loading.value) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   preferences.value.orderDirection = preferences.value.orderDirection === "asc" ? "desc" : "asc"; | 
					
						
							|  |  |  |   initializeTimelineEvents(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function toggleEventTypeOption(option: TimelineEventType) { | 
					
						
							|  |  |  |   if (loading.value) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const index = preferences.value.types.indexOf(option); | 
					
						
							|  |  |  |   if (index === -1) { | 
					
						
							|  |  |  |     preferences.value.types.push(option); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   else { | 
					
						
							|  |  |  |     preferences.value.types.splice(index, 1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   initializeTimelineEvents(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Timeline Actions
 | 
					
						
							| 
									
										
										
										
											2025-09-13 15:36:18 -05:00
										 |  |  | async function updateTimelineEvent(index: number, event: RecipeTimelineEventUpdate) { | 
					
						
							|  |  |  |   const eventId = timelineEvents.value[index].id; | 
					
						
							|  |  |  |   const { response } = await api.recipes.updateTimelineEvent(eventId, event); | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |   if (response?.status !== 200) { | 
					
						
							|  |  |  |     alert.error(i18n.t("events.something-went-wrong") as string); | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-13 15:36:18 -05:00
										 |  |  |   // Update the local event data to reflect the changes in the UI
 | 
					
						
							|  |  |  |   timelineEvents.value[index] = response.data; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |   alert.success(i18n.t("events.event-updated") as string); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function deleteTimelineEvent(index: number) { | 
					
						
							|  |  |  |   const { response } = await api.recipes.deleteTimelineEvent(timelineEvents.value[index].id); | 
					
						
							|  |  |  |   if (response?.status !== 200) { | 
					
						
							|  |  |  |     alert.error(i18n.t("events.something-went-wrong") as string); | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   timelineEvents.value.splice(index, 1); | 
					
						
							|  |  |  |   alert.success(i18n.t("events.event-deleted") as string); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function getRecipes(recipeIds: string[]): Promise<Recipe[]> { | 
					
						
							|  |  |  |   const qf = "id IN [" + recipeIds.map(id => `"${id}"`).join(", ") + "]"; | 
					
						
							|  |  |  |   const { data } = await api.recipes.getAll(1, -1, { queryFilter: qf }); | 
					
						
							|  |  |  |   return data?.items || []; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function updateRecipes(events: RecipeTimelineEventOut[]) { | 
					
						
							|  |  |  |   const recipeIds: string[] = []; | 
					
						
							|  |  |  |   events.forEach((event) => { | 
					
						
							|  |  |  |     if (recipeIds.includes(event.recipeId) || recipes.has(event.recipeId)) { | 
					
						
							|  |  |  |       return; | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |     recipeIds.push(event.recipeId); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |   const results = await getRecipes(recipeIds); | 
					
						
							|  |  |  |   results.forEach((result) => { | 
					
						
							|  |  |  |     if (!result?.id) { | 
					
						
							|  |  |  |       return; | 
					
						
							| 
									
										
										
										
											2024-03-12 10:20:48 -05:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |     recipes.set(result.id, result); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function scrollTimelineEvents() { | 
					
						
							|  |  |  |   const orderBy = "timestamp"; | 
					
						
							|  |  |  |   const orderDirection = preferences.value.orderDirection === "asc" ? "asc" : "desc"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const eventTypeValue = `["${preferences.value.types.join("\", \"")}"]`; | 
					
						
							|  |  |  |   const queryFilter = `(${props.queryFilter}) AND eventType IN ${eventTypeValue}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const response = await api.recipes.getAllTimelineEvents(page.value, perPage, { orderBy, orderDirection, queryFilter }); | 
					
						
							|  |  |  |   page.value += 1; | 
					
						
							|  |  |  |   if (!response?.data) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const events = response.data.items; | 
					
						
							|  |  |  |   if (events.length < perPage) { | 
					
						
							|  |  |  |     hasMore.value = false; | 
					
						
							|  |  |  |     if (!events.length) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // fetch recipes
 | 
					
						
							|  |  |  |   if (props.showRecipeCards) { | 
					
						
							|  |  |  |     await updateRecipes(events); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // this is set last so Vue knows to re-render
 | 
					
						
							|  |  |  |   timelineEvents.value.push(...events); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function initializeTimelineEvents() { | 
					
						
							|  |  |  |   loading.value = true; | 
					
						
							|  |  |  |   ready.value = false; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   page.value = 1; | 
					
						
							|  |  |  |   hasMore.value = true; | 
					
						
							|  |  |  |   timelineEvents.value = []; | 
					
						
							|  |  |  |   await scrollTimelineEvents(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   ready.value = true; | 
					
						
							|  |  |  |   loading.value = false; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const infiniteScroll = useThrottleFn(() => { | 
					
						
							|  |  |  |   useAsyncData(useAsyncKey(), async () => { | 
					
						
							|  |  |  |     if (!hasMore.value || loading.value) { | 
					
						
							|  |  |  |       return; | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |     loading.value = true; | 
					
						
							|  |  |  |     await scrollTimelineEvents(); | 
					
						
							|  |  |  |     loading.value = false; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }, 500); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // preload events
 | 
					
						
							|  |  |  | initializeTimelineEvents(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | onMounted( | 
					
						
							|  |  |  |   () => { | 
					
						
							|  |  |  |     document.onscroll = () => { | 
					
						
							|  |  |  |       // if the inner element is scrollable, let its scroll event handle the infiniteScroll
 | 
					
						
							|  |  |  |       const timelineContainerElement = document.getElementById("timeline-container"); | 
					
						
							|  |  |  |       if (timelineContainerElement) { | 
					
						
							|  |  |  |         const { clientHeight, scrollHeight } = timelineContainerElement; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // if scrollHeight == clientHeight, the element is not scrollable, so we need to look at the global position
 | 
					
						
							|  |  |  |         // if scrollHeight > clientHeight, it is scrollable and we don't need to do anything here
 | 
					
						
							|  |  |  |         if (scrollHeight > clientHeight) { | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  |       const bottomOfWindow = document.documentElement.scrollTop + window.innerHeight >= document.documentElement.offsetHeight - (window.innerHeight * screenBuffer); | 
					
						
							|  |  |  |       if (bottomOfWindow) { | 
					
						
							|  |  |  |         infiniteScroll(); | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  |       } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }, | 
					
						
							| 
									
										
										
										
											2025-07-30 20:37:02 +02:00
										 |  |  | ); | 
					
						
							| 
									
										
										
										
											2023-04-25 12:46:00 -05:00
										 |  |  | </script> |