refactor: everything to nuxt.js

This commit is contained in:
2026-03-19 14:30:36 -04:00
parent 6e1f58cd8e
commit bb3042014e
159 changed files with 6786 additions and 11198 deletions

111
app/stores/auth.ts Normal file
View File

@@ -0,0 +1,111 @@
import { defineStore } from 'pinia';
import { ID, account, functions, teams } from '~/utils/appwrite';
import { ExecutionMethod, type Models } from 'appwrite';
import { computed, ref } from 'vue';
import { useBoatStore } from './boat';
import { useReservationStore } from './reservation';
export const useAuthStore = defineStore('auth', () => {
const currentUser = ref<Models.User<Models.Preferences> | null>(null);
const currentUserTeams = ref<Models.TeamList<Models.Preferences> | null>(
null
);
const userNames = ref<Record<string, string>>({});
async function init() {
try {
currentUser.value = await account.get();
currentUserTeams.value = await teams.list();
await useBoatStore().fetchBoats();
await useReservationStore().fetchUserReservations();
} catch {
currentUser.value = null;
currentUserTeams.value = null;
}
}
const currentUserTeamNames = computed(() =>
currentUserTeams.value
? currentUserTeams.value.teams.map((team) => team.name)
: []
);
const hasRequiredRole = (requiredRoles: string[]): boolean => {
return requiredRoles.some((role) =>
currentUserTeamNames.value.includes(role)
);
};
async function createTokenSession(email: string) {
return await account.createEmailToken(ID.unique(), email);
}
async function createMagicURLSession(email: string) {
return await account.createMagicURLToken(
ID.unique(),
email,
window.location.origin + '/auth/callback'
);
}
async function tokenLogin(userId: string, token: string) {
await account.createSession(userId, token);
await init();
}
async function magicURLLogin(userId: string, secret: string) {
await account.updateMagicURLSession(userId, secret);
await init();
}
function getUserNameById(id: string | undefined | null): string {
if (!id) return 'No User';
try {
if (!userNames.value[id]) {
userNames.value[id] = 'Loading...';
functions
.createExecution(
'userinfo',
'',
false,
'/userinfo/' + id,
ExecutionMethod.GET
)
.then((res) => {
if (res.responseBody) {
userNames.value[id] = JSON.parse(res.responseBody).name;
} else {
console.error(res, id);
}
});
}
} catch (e) {
console.error('Failed to get username. Error: ' + e);
}
return userNames.value[id] ?? 'Unknown';
}
function logout() {
return account.deleteSession('current').then(() => {
currentUser.value = null;
});
}
async function updateName(name: string) {
await account.updateName(name);
currentUser.value = await account.get();
}
return {
currentUser,
getUserNameById,
hasRequiredRole,
updateName,
createTokenSession,
createMagicURLSession,
tokenLogin,
magicURLLogin,
logout,
init,
};
});

29
app/stores/boat.ts Normal file
View File

@@ -0,0 +1,29 @@
import { defineStore } from 'pinia';
import { AppwriteIds, databases } from '~/utils/appwrite';
import { ref } from 'vue';
import type { Boat } from '~/utils/boat.types';
export { type Boat } from '~/utils/boat.types';
export const useBoatStore = defineStore('boat', () => {
const boats = ref<Boat[]>([]);
async function fetchBoats() {
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.boat
);
boats.value = response.documents as unknown as Boat[];
} catch (error) {
console.error('Failed to fetch boats', error);
}
}
const getBoatById = (id: string | null | undefined): Boat | null => {
if (!id) return null;
return boats.value?.find((b) => b.$id === id) || null;
};
return { boats, fetchBoats, getBoatById };
});

163
app/stores/interval.ts Normal file
View File

@@ -0,0 +1,163 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import type { Boat } from '~/utils/boat.types';
import type { Timestamp } from '@quasar/quasar-ui-qcalendar';
import { today } from '@quasar/quasar-ui-qcalendar';
import type { Interval } from '~/utils/schedule.types';
import { AppwriteIds, databases } from '~/utils/appwrite';
import { ID, Query } from 'appwrite';
import { useReservationStore } from './reservation';
import type { LoadingTypes } from '~/utils/misc';
import { useRealtimeStore } from './realtime';
export const useIntervalStore = defineStore('interval', () => {
const intervals = ref(new Map<string, Interval>());
const dateStatus = ref(new Map<string, LoadingTypes>());
const selectedDate = ref<string>(today());
const reservationStore = useReservationStore();
const realtimeStore = useRealtimeStore();
realtimeStore.register(
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.interval}.documents`,
(response) => {
const payload = response.payload as unknown as Interval;
if (!payload.$id) return;
if (
response.events.includes('databases.*.collections.*.documents.*.delete')
) {
intervals.value.delete(payload.$id);
} else {
intervals.value.set(payload.$id, payload);
}
}
);
const getIntervals = (date: Timestamp | string, boat?: Boat) => {
const searchDate = typeof date === 'string' ? date : date.date;
const dayStart = new Date(searchDate + 'T00:00');
const dayEnd = new Date(searchDate + 'T23:59');
if (dateStatus.value.get(searchDate) === undefined) {
dateStatus.value.set(searchDate, 'pending');
fetchIntervals(searchDate);
}
return computed(() => {
return Array.from(intervals.value.values()).filter((interval) => {
const intervalStart = new Date(interval.start);
const intervalEnd = new Date(interval.end);
const isWithinDay = intervalStart < dayEnd && intervalEnd > dayStart;
const matchesBoat = boat ? boat.$id === interval.resource : true;
return isWithinDay && matchesBoat;
});
});
};
const getAvailableIntervals = (date: Timestamp | string, boat?: Boat) => {
return computed(() =>
getIntervals(date, boat).value.filter((interval) => {
return !reservationStore.isResourceTimeOverlapped(
interval.resource,
new Date(interval.start),
new Date(interval.end)
);
})
);
};
async function fetchInterval(id: string): Promise<Interval> {
return (await databases.getDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.interval,
id
)) as Interval;
}
async function fetchIntervals(dateString: string) {
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.interval,
[
Query.greaterThanEqual(
'end',
new Date(dateString + 'T00:00').toISOString()
),
Query.lessThanEqual(
'start',
new Date(dateString + 'T23:59').toISOString()
),
Query.limit(50),
]
);
response.documents.forEach((d) =>
intervals.value.set(d.$id, d as unknown as Interval)
);
dateStatus.value.set(dateString, 'loaded');
console.info(`Loaded ${response.documents.length} intervals from server`);
} catch (error) {
console.error('Failed to fetch intervals', error);
dateStatus.value.set(dateString, 'error');
}
}
const createInterval = async (interval: Interval) => {
try {
const response = await databases.createDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.interval,
ID.unique(),
interval
);
intervals.value.set(response.$id, response as unknown as Interval);
} catch (e) {
console.error('Error creating Interval: ' + e);
}
};
const updateInterval = async (interval: Interval) => {
try {
if (interval.$id) {
const response = await databases.updateDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.interval,
interval.$id,
{ ...interval, $id: undefined }
);
intervals.value.set(response.$id, response as unknown as Interval);
console.info(`Saved Interval: ${interval.$id}`);
} else {
console.error('Update interval called without an ID');
}
} catch (e) {
console.error('Error updating Interval: ' + e);
}
};
const deleteInterval = async (id: string) => {
try {
await databases.deleteDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.interval,
id
);
intervals.value.delete(id);
console.info(`Deleted interval: ${id}`);
} catch (e) {
console.error('Error deleting Interval: ' + e);
}
};
return {
getIntervals,
getAvailableIntervals,
fetchIntervals,
fetchInterval,
createInterval,
updateInterval,
deleteInterval,
selectedDate,
intervals,
};
});

View File

@@ -0,0 +1,101 @@
import type { Ref } from 'vue';
import { ref } from 'vue';
import type { IntervalTemplate } from '~/utils/schedule.types';
import { defineStore } from 'pinia';
import { AppwriteIds, databases } from '~/utils/appwrite';
import type { Models } from 'appwrite';
import { ID } from 'appwrite';
import { arrayToTimeTuples } from '~/utils/schedule';
export const useIntervalTemplateStore = defineStore('intervalTemplate', () => {
const intervalTemplates = ref<IntervalTemplate[]>([]);
const getIntervalTemplates = (): Ref<IntervalTemplate[]> => {
if (!intervalTemplates.value) fetchIntervalTemplates();
return intervalTemplates;
};
async function fetchIntervalTemplates() {
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate
);
intervalTemplates.value = response.documents.map((d): IntervalTemplate => {
const doc = d as unknown as { timeTuple: string[] } & Models.Document;
return {
...doc,
timeTuples: arrayToTimeTuples(doc.timeTuple),
} as unknown as IntervalTemplate;
});
} catch (error) {
console.error('Failed to fetch timeblock templates', error);
}
}
const createIntervalTemplate = async (template: IntervalTemplate) => {
try {
const response = await databases.createDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
ID.unique(),
{ name: template.name, timeTuple: template.timeTuples.flat(2) }
);
intervalTemplates.value.push(response as unknown as IntervalTemplate);
} catch (e) {
console.error('Error creating IntervalTemplate: ' + e);
}
};
const deleteIntervalTemplate = async (id: string) => {
try {
await databases.deleteDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
id
);
intervalTemplates.value = intervalTemplates.value.filter(
(template) => template.$id !== id
);
} catch (e) {
console.error('Error deleting IntervalTemplate: ' + e);
}
};
const updateIntervalTemplate = async (
template: IntervalTemplate,
id: string
) => {
try {
const response = await databases.updateDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate,
id,
{
name: template.name,
timeTuple: template.timeTuples.flat(2),
}
);
intervalTemplates.value = intervalTemplates.value.map((b) =>
b.$id !== id
? b
: ({
...response,
timeTuples: arrayToTimeTuples(
(response as unknown as { timeTuple: string[] }).timeTuple
),
} as unknown as IntervalTemplate)
);
} catch (e) {
console.error('Error updating IntervalTemplate: ' + e);
}
};
return {
getIntervalTemplates,
fetchIntervalTemplates,
createIntervalTemplate,
deleteIntervalTemplate,
updateIntervalTemplate,
};
});

View File

@@ -0,0 +1,21 @@
import { defineStore } from 'pinia';
export interface MemberProfile {
firstName: string;
lastName: string;
certs: string[];
slackID: string;
userID: string;
}
const getSampleData = () => ({
firstName: 'Billy',
lastName: 'Crystal',
certs: ['j27', 'capri25'],
});
export const useMemberProfileStore = defineStore('memberProfile', {
state: () => ({
...getSampleData(),
}),
});

20
app/stores/realtime.ts Normal file
View File

@@ -0,0 +1,20 @@
import { defineStore } from 'pinia';
import { client } from '~/utils/appwrite';
import { ref } from 'vue';
import type { RealtimeResponseEvent } from 'appwrite';
export const useRealtimeStore = defineStore('realtime', () => {
const subscriptions = ref<Map<string, () => void>>(new Map());
const register = (
channel: string,
fn: (response: RealtimeResponseEvent<unknown>) => void
) => {
if (subscriptions.value.has(channel)) return;
subscriptions.value.set(channel, client.subscribe(channel, fn));
};
return {
register,
};
});

123
app/stores/reference.ts Normal file
View File

@@ -0,0 +1,123 @@
import { defineStore } from 'pinia';
export interface ReferenceEntry {
id: number;
title: string;
category: string;
tags?: string[];
subtitle?: string;
content: string;
}
function getSampleData(): ReferenceEntry[] {
return [
{
id: 1,
title: 'J/27 Background',
category: 'general',
tags: ['j27', 'info'],
subtitle: 'Fast Fun Racer or Getaway Weekend Cruiser',
content: `Its hard to imagine that a modern 27 foot sailboat with a classic look, superb
stability, and easy to manage rig is such a fast boat. 123-126 PHRF. No longer do you
have to substitute speed for comfort, or own separate boats for racing and cruising. The
8' long cockpit seats 4 to 5 comfortably. Below deck you can sleep 5. And with head and
stove, the J/27 is the perfect weekend cruiser.
Fun and Fast. There are some impressive victories to back this up, but that doesn't
tell the whole story. The J/27 is fun and responsive. Nothing is more exhilarating than
popping the J/27's kite in a good breeze for a downhill sleigh ride. 15+ knots planing
off the wave-tops is easy. And most importantly, this off-wind speed doesn't sacrifice
upwind performance. Going to windward in the J/27 is a dream, it has the solid, balanced
"feel" of a traditional keelboat. The J/27 points higher and goes faster than many 30-35
footers!
One-Design Racing. Even more fun is sailing a one-design race around the buoys. The
J/27's close-windedness makes it very tactical, as even 5 degree wind shifts bring
significant gains. Then off wind, you quickly learn to play gibe angles as the boat's
acceleration gains you valuable ground on the competition. The J/27 is remarkably agile
and responsive in lighter winds, which is unusual for a boat that feels so solid.
All-Day Comfort. Sailing past larger boats is always satisfying... especially when it's
effortless and you can't be written off as being wet and uncomfortable. Design is the
difference. It's all done from a cockpit which holds several people more than is possible
on other 27-footers. Correctly angled backrests and decks at elbow level provide restful
and secure seating. Harken mainsheet, vang, traveler, and backstay systems; four Barient
winches; a beautiful double spreader, tapered, fractional rig spar by Hall . . . make
control and adjustment easy for crew members no matter what the wind.
Get-away Weekend Cruiser. Take a break from the pace of life on land and spend time with
family and friends sailing the J/27. It's a fun boat to sail, so everyone becomes involved.
The visibility, when steering with a responsive tiller gives the inexperienced that sense
of control not found when spinning a tiny wheel on small cruisers with large trunk cabins.
The J/27 has a comfortable, open interior in teak with off-white surfaces. A main structural
fiberglass bulkhead with oval opening separates the spacious double V-berth and head area
from the main cabin. The main settee berth converts to a double. Aft of the galley to
starboard is a comfortable quarter berth. Enough room below for a family of four or a
couple for a nice weekend romp to your favorite sailing anchorage.
Durable and Stable. The J/27's secure big boat feel is created by concentrating 1530
pounds of lead very low in the keel while using high strength to eight ratio laminates
in the hull. Unidirectional E-glass on either side of pre-sealed Baltek CK57 aircraft
grade, Lloyd's approved, end grain balsa sandwich construction means superior torsion and
impact resistance. Light ends, low freeboard, and the low center of gravity of a lead keel
coupled with low wetted surface and a generous sailplan of 362 sq. ft. achieves exceptional
sail area and stability relative to displacement. Hence, sparkling performance in both
light and heavy air...something that doesn't happen with iron keels and box-like hulls.
Strong Class Strict Rules. The J/27 Class Association, owner driven and over 190 boats
strong, sail in North American, Midwinter, and Regional championships. A superb J/27 Class
Newsletter keeps you up-to-date on Class activities, latest results, maintenance tips,
cruising points of interest, and "go-fasts". And the J/27 Class Rules have sail limitations
to help insure equal performance and resale value. The Class supports both the active
racer and cruising sailor in addition to fleets throughout the U.S.`,
},
{
id: 2,
title: 'Capri25 Background',
category: 'general',
tags: ['capri25', 'info'],
subtitle: 'The Capri 25 (by Catalina) is nothing like a Catalina 25',
content: `The Capri 25 (by Catalina) is nothing like a Catalina 25 ... The Capri
is five inches shorter on deck, three feet shorter on the waterline, and weighs
almost 1,400 pounds less than the Catalina, so we suppose you could call her a Catalina
"Lite," especially since her towing weight is over a ton less, so you can use a smaller,
lighter towing vehicle on the highway. Besides her lower weight, she has slightly more
sail area and a sleeker fin keel, so she is also faster—way faster. In fact, her average
PHRF rating is 171, which is, amazingly, 3 seconds per mile less than the legendary J/24,
and a whopping 54 seconds less than the Catalina 25. Needless to say, part of her weight
loss is accomplished by the omission of cabin furniture and other niceties like the
Catalina's on-deck anchor locker. Other weight saving is achieved by eliminating 600
pounds of ballast, and by using a then-new material, Coremat, to replace some of the
hull and deck laminate. Best features: If you like round-the-buoys racing and/or
socializing in a one-design fleet, this may be the boat for you. She has a bit more space
below than a J/24, and six inches more headroom, but otherwise her character is in the
same range. Worst features: Nothing significant noticed."`,
},
{
id: 3,
title: 'Outboard Engine Operation',
subtitle: 'An overview of how outboard engines work.',
category: 'howto',
tags: ['manuals', 'howto', 'engine'],
content: 'Lorem ipsum dolor met.',
},
] as ReferenceEntry[];
}
export const useReferenceStore = defineStore('reference', {
state: () => ({
allItems: getSampleData(),
}),
getters: {
getCategory(state) {
return (category: string) => {
return state.allItems.filter((c) => c.category === category);
};
},
},
actions: {},
});

283
app/stores/reservation.ts Normal file
View File

@@ -0,0 +1,283 @@
import { defineStore } from 'pinia';
import type { Reservation } from '~/utils/schedule.types';
import type { ComputedRef } from 'vue';
import { computed, reactive } from 'vue';
import { AppwriteIds, databases } from '~/utils/appwrite';
import { ID, Query } from 'appwrite';
import { date, useQuasar } from 'quasar';
import type { Timestamp } from '@quasar/quasar-ui-qcalendar';
import { parseDate, today } from '@quasar/quasar-ui-qcalendar';
import type { LoadingTypes } from '~/utils/misc';
import { useAuthStore } from './auth';
import { isPast } from '~/utils/schedule';
import { useRealtimeStore } from './realtime';
export const useReservationStore = defineStore('reservation', () => {
const reservations = reactive<Map<string, Reservation>>(new Map());
const datesLoaded = reactive<Record<string, LoadingTypes>>({});
const userReservations = reactive<Map<string, Reservation>>(new Map());
const authStore = useAuthStore();
const $q = useQuasar();
const realtimeStore = useRealtimeStore();
realtimeStore.register(
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.reservation}.documents`,
(response) => {
const payload = response.payload as unknown as Reservation;
if (payload.$id) {
if (
response.events.includes(
'databases.*.collections.*.documents.*.delete'
)
) {
reservations.delete(payload.$id);
userReservations.delete(payload.$id);
} else {
reservations.set(payload.$id, payload);
if (payload.user === authStore.currentUser?.$id)
userReservations.set(payload.$id, payload);
}
}
}
);
const fetchReservationsForDateRange = async (
start: string = today(),
end: string = start
) => {
const startDate = new Date(start < end ? start : end + 'T00:00');
const endDate = new Date(start < end ? end : start + 'T23:59');
if (getUnloadedDates(startDate, endDate).length === 0) return;
setDateLoaded(startDate, endDate, 'pending');
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
[
Query.greaterThanEqual('end', startDate.toISOString()),
Query.lessThanEqual('start', endDate.toISOString()),
]
);
response.documents.forEach((d) =>
reservations.set(d.$id, d as unknown as Reservation)
);
setDateLoaded(startDate, endDate, 'loaded');
} catch (error) {
console.error('Failed to fetch reservations', error);
setDateLoaded(startDate, endDate, 'error');
}
};
const getReservationById = async (id: string) => {
try {
const response = await databases.getDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
id
);
return response as unknown as Reservation;
} catch (error) {
console.error('Failed to fetch reservation: ', error);
}
};
const createOrUpdateReservation = async (
reservation: Reservation
): Promise<Reservation> => {
let response;
try {
if (reservation.$id) {
response = await databases.updateDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
reservation.$id,
reservation
);
} else {
response = await databases.createDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
ID.unique(),
reservation
);
}
reservations.set(response.$id, response as unknown as Reservation);
userReservations.set(response.$id, response as unknown as Reservation);
console.info('Reservation booked: ', response);
return response as unknown as Reservation;
} catch (e) {
console.error('Error creating Reservation: ' + e);
throw e;
}
};
const deleteReservation = async (
reservation: string | Reservation | null | undefined
) => {
if (!reservation) return false;
const id = typeof reservation === 'string' ? reservation : reservation.$id;
if (!id) return false;
const status = $q.notify({
color: 'secondary',
textColor: 'white',
message: 'Deleting Reservation',
spinner: true,
closeBtn: 'Dismiss',
position: 'top',
timeout: 0,
group: false,
});
try {
await databases.deleteDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
id
);
reservations.delete(id);
userReservations.delete(id);
console.info(`Deleted reservation: ${id}`);
status({
color: 'warning',
message: 'Reservation Deleted',
spinner: false,
icon: 'delete',
timeout: 4000,
});
} catch (e) {
console.error('Error deleting reservation: ' + e);
status({
color: 'negative',
message: 'Failed to Delete Reservation',
spinner: false,
icon: 'error',
});
}
};
const setDateLoaded = (start: Date, end: Date, state: LoadingTypes) => {
if (start > end) return [];
let curDate = start;
while (curDate < end) {
datesLoaded[(parseDate(curDate) as Timestamp).date] = state;
curDate = date.addToDate(curDate, { days: 1 });
}
};
const getUnloadedDates = (start: Date, end: Date): string[] => {
if (start > end) return [];
let curDate = start;
const unloaded = [];
while (curDate < end) {
const parsedDate = (parseDate(curDate) as Timestamp).date;
if (datesLoaded[parsedDate] === undefined) unloaded.push(parsedDate);
curDate = date.addToDate(curDate, { days: 1 });
}
return unloaded;
};
const getReservationsByDate = (
searchDate: string,
boat?: string
): ComputedRef<Reservation[]> => {
if (!datesLoaded[searchDate]) {
fetchReservationsForDateRange(searchDate);
}
const dayStart = new Date(searchDate + 'T00:00');
const dayEnd = new Date(searchDate + 'T23:59');
return computed(() => {
return Array.from(reservations.values()).filter((reservation) => {
const reservationStart = new Date(reservation.start);
const reservationEnd = new Date(reservation.end);
const isWithinDay =
reservationStart < dayEnd && reservationEnd > dayStart;
const matchesBoat = boat ? boat === reservation.resource : true;
return isWithinDay && matchesBoat;
});
});
};
const getConflictingReservations = (
resource: string,
start: Date,
end: Date
): Reservation[] => {
return Array.from(reservations.values()).filter(
(entry) =>
entry.resource === resource &&
new Date(entry.start) < end &&
new Date(entry.end) > start
);
};
const isResourceTimeOverlapped = (
resource: string,
start: Date,
end: Date
): boolean => {
return getConflictingReservations(resource, start, end).length > 0;
};
const isReservationOverlapped = (res: Reservation): boolean => {
return isResourceTimeOverlapped(
res.resource,
new Date(res.start),
new Date(res.end)
);
};
const fetchUserReservations = async () => {
if (!authStore.currentUser) return;
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
[Query.equal('user', authStore.currentUser.$id)]
);
response.documents.forEach((d) =>
userReservations.set(d.$id, d as unknown as Reservation)
);
} catch (error) {
console.error('Failed to fetch reservations for user: ', error);
}
};
const sortedUserReservations = computed((): Reservation[] =>
[...userReservations.values()].sort(
(a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()
)
);
const futureUserReservations = computed((): Reservation[] => {
if (!sortedUserReservations.value) return [];
return sortedUserReservations.value.filter((b) => !isPast(b.end));
});
const pastUserReservations = computed((): Reservation[] => {
if (!sortedUserReservations.value) return [];
return sortedUserReservations.value?.filter((b) => isPast(b.end));
});
return {
getReservationsByDate,
getReservationById,
createOrUpdateReservation,
deleteReservation,
fetchReservationsForDateRange,
isReservationOverlapped,
isResourceTimeOverlapped,
getConflictingReservations,
fetchUserReservations,
sortedUserReservations,
futureUserReservations,
pastUserReservations,
userReservations,
};
});