Compare commits

10 Commits

Author SHA1 Message Date
8d194701b3 Fix path 2023-12-02 15:43:54 -05:00
7658ba2bed Move images around, so they get served 2023-12-02 15:39:52 -05:00
9d1d7398b9 Schedule UX tweaks 2023-12-01 22:20:23 -05:00
8600000e24 Many tweaks to booking form. 2023-12-01 00:08:29 -05:00
aed0462e05 Refinements to date handling in booking form 2023-11-28 21:03:39 -05:00
a3cdbbfbbd Developing Booking Form 2023-11-26 09:21:04 -05:00
8200bcde52 Basic calendar view 2023-11-24 07:52:01 -05:00
a6540a2a02 More page mockups 2023-11-22 10:11:46 -05:00
caf9535849 Update Toolbar 2023-11-21 20:18:58 -05:00
c307f62a05 Update Reference Store 2023-11-21 08:43:56 -05:00
45 changed files with 1095 additions and 160 deletions

View File

@@ -22,6 +22,7 @@
},
"devDependencies": {
"@quasar/app-vite": "^1.3.0",
"@quasar/quasar-app-extension-qcalendar": "^4.0.0-beta.15",
"@types/node": "^12.20.21",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",

View File

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 276 KiB

View File

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -30,7 +30,7 @@ module.exports = configure(function (/* ctx */) {
boot: ['appwrite'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'],
css: ['app.sass'],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
@@ -79,8 +79,16 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
open: true, // opens browser window automatically
// https: true,
// open: true, // opens browser window automatically
port: 4000,
strictport: true,
// For reverse-proxying via haproxy
// hmr: {
// clientPort: 443,
// protocol: 'wss',
// timeout: 0,
// },
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

3
quasar.extensions.json Normal file
View File

@@ -0,0 +1,3 @@
{
"@quasar/qcalendar": {}
}

View File

@@ -23,7 +23,7 @@ const account = new Account(client);
const databases = new Databases(client);
let appRouter: Router;
export default boot(async ({ app, router }) => {
export default boot(async ({ router }) => {
// Initialize store
const authStore = useAuthStore();
await authStore.init();

View File

@@ -0,0 +1,26 @@
<template>
<q-tabs class="mobile-only">
<!-- <q-tab name="Home" icon="home" to="index"></q-tab> -->
<q-route-tab name="Boats" icon="sailing" to="/boat"></q-route-tab>
<q-route-tab
name="Schedule"
icon="calendar_month"
to="/schedule"
></q-route-tab>
<q-route-tab
name="Checklists"
icon="checklist"
to="/checklist"
></q-route-tab>
<q-route-tab
name="Reference"
icon="info_outline"
to="/reference"
></q-route-tab>
<q-route-tab name="Tasks" icon="build" to="/task">
<q-badge color="red" floating> NEW </q-badge>
</q-route-tab>
</q-tabs>
</template>
<script setup lang="ts"></script>

View File

@@ -3,7 +3,7 @@
:model-value="drawer"
show-if-above
:width="200"
:breakpoint="500"
:breakpoint="1024"
@update:model-value="$emit('drawer-toggle')"
>
<q-scroll-area class="fit">

View File

@@ -3,7 +3,6 @@
flat
bordered
class="my-card"
:class="$q.dark.isActive ? 'bg-grey-9' : 'bg-grey-2'"
v-for="entry in entries"
:key="entry.title"
>
@@ -43,7 +42,7 @@
</template>
<script setup lang="ts">
import { ReferenceEntry } from './models';
import { ReferenceEntry } from 'src/stores/reference';
defineProps({
entries: Array<ReferenceEntry>,

View File

@@ -0,0 +1,225 @@
<template>
<q-card-section>
<div class="text-caption text-justify">
Use the calendar to pick a date. Tap a box in the grid for the boat and
start time. Select the duration below.
</div>
<div style="width: 100%; display: flex; justify-content: center">
<div
style="
width: 50%;
max-width: 350px;
display: flex;
justify-content: space-between;
"
>
<span
class="q-button"
style="cursor: pointer; user-select: none"
@click="onPrev"
>&lt;</span
>
{{ formattedMonth }}
<span
class="q-button"
style="cursor: pointer; user-select: none"
@click="onNext"
>&gt;</span
>
</div>
</div>
<div
style="
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
"
>
<div style="display: flex; width: 100%">
<q-calendar-month
ref="calendar"
v-model="selectedDate"
:disabled-before="disabledBefore"
animated
bordered
mini-mode
date-type="rounded"
@change="onChange"
@moved="onMoved"
@click-date="onClickDate"
/>
</div></div
></q-card-section>
<q-calendar-resource
v-model="selectedDate"
:model-resources="boatStore.boats"
resource-key="id"
resource-label="name"
:interval-start="12"
:interval-count="36"
:interval-minutes="30"
cell-width="48"
resource-min-height="40"
animated
bordered
@change="onChange"
@moved="onMoved"
@resource-expanded="onResourceExpanded"
@click-date="onClickDate"
@click-time="onClickTime"
@click-resource="onClickResource"
@click-head-resources="onClickHeadResources"
@click-interval="onClickInterval"
>
<template #resource-intervals="{ scope }">
<template v-for="(event, index) in getEvents(scope)" :key="index">
<q-badge outline :label="event.title" :style="getStyle(event)" />
</template>
</template>
<template #resource-label="{ scope: { resource } }">
<div class="col-12">
{{ resource.name }}
<q-icon v-if="resource.defects" name="warning" color="warning" />
</div>
</template>
</q-calendar-resource>
<q-card-section>
<q-select
filled
v-model="duration"
:options="durations"
dense
@update:model-value="onUpdateDuration"
label="Duration (hours)"
stack-label
><template v-slot:append><q-icon name="timelapse" /></template></q-select
></q-card-section>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
QCalendarResource,
TimestampOrNull,
today,
parseDate,
parseTimestamp,
addToDate,
Timestamp,
} from '@quasar/quasar-ui-qcalendar';
import { Boat, useBoatStore } from 'src/stores/boat';
import { useScheduleStore } from 'src/stores/schedule';
import { date } from 'quasar';
import { computed } from 'vue';
const durations = [1, 1.5, 2, 2.5, 3, 3.5, 4];
type ResourceIntervalScope = {
resource: Boat;
intervals: [];
timeStartPosX(start: TimestampOrNull): number;
timeDurationWidth(duration: number): number;
};
const statusLookup = {
confirmed: ['#14539a', 'white'],
pending: ['#f2c037', 'white'],
tentative: ['white', 'grey'],
};
const calendar = ref();
const boatStore = useBoatStore();
const scheduleStore = useScheduleStore();
const selectedDate = ref(today());
const duration = ref(1);
const formattedMonth = computed(() => {
const date = new Date(selectedDate.value);
return monthFormatter()?.format(date);
});
const disabledBefore = computed(() => {
const todayTs = parseTimestamp(today()) as Timestamp;
return addToDate(todayTs, { day: -1 }).date;
});
function monthFormatter() {
try {
return new Intl.DateTimeFormat('en-CA' || undefined, {
month: 'long',
timeZone: 'UTC',
});
} catch (e) {
//
}
}
function getEvents(scope: ResourceIntervalScope) {
const resourceEvents = scheduleStore.getBoatReservations(
scope.resource.id,
date.extractDate(selectedDate.value, 'YYYY-MM-DD')
);
return resourceEvents.map((event) => {
return {
left: scope.timeStartPosX(parseDate(event.start)),
width: scope.timeDurationWidth(
date.getDateDiff(event.end, event.start, 'minutes')
),
title: event.user,
status: event.status,
};
});
}
function getStyle(event: {
left: number;
width: number;
title: string;
status: 'tentative' | 'confirmed' | 'pending';
}) {
return {
position: 'absolute',
background: event.status ? statusLookup[event.status][0] : 'white',
color: event.status ? statusLookup[event.status][1] : '#14539a',
left: `${event.left}px`,
width: `${event.width}px`,
height: '32px',
overflow: 'hidden',
};
}
const emit = defineEmits(['onClickTime', 'onUpdateDuration']);
function onPrev() {
calendar.value.prev();
}
function onNext() {
calendar.value.next();
}
function onClickDate(data) {
return;
}
function onClickTime(data) {
// TODO: Add a duration picker, here.
emit('onClickTime', data);
}
function onUpdateDuration(value) {
emit('onUpdateDuration', value);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onClickInterval = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onClickHeadResources = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onClickResource = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onResourceExpanded = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onMoved = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onChange = () => {};
</script>

View File

@@ -0,0 +1,33 @@
<template>
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title> {{ pageTitle }} </q-toolbar-title>
<q-tabs shrink>
<q-tab> </q-tab>
</q-tabs>
</q-toolbar>
</q-header>
<LeftDrawer :drawer="leftDrawerOpen" @drawer-toggle="toggleLeftDrawer" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import LeftDrawer from 'components/LeftDrawer.vue';
const leftDrawerOpen = ref(false);
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
defineProps({
pageTitle: String,
});
</script>

View File

@@ -0,0 +1,40 @@
<template>
<q-select
v-model="boat"
:options="boats"
option-value="id"
option-label="name"
label="Boat"
>
<template v-slot:prepend>
<q-item-section avatar>
<q-img v-if="boat?.iconsrc" :src="boat?.iconsrc" />
<q-icon v-else name="sailing" />
</q-item-section>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-img :src="scope.opt.iconsrc" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
<q-item-label caption>{{ scope.opt.class }}</q-item-label>
</q-item-section>
<q-item-section avatar v-if="scope.opt.defects">
<q-icon name="warning" color="warning" />
<q-tooltip class="bg-amber text-black shadow-7"
>This boat has notices. Select it to see details.
</q-tooltip>
</q-item-section>
</q-item>
</template>
</q-select>
</template>
<script setup lang="ts">
import { Boat, useBoatStore } from 'src/stores/boat';
const boats = useBoatStore().boats;
const boat = <Boat | undefined>undefined;
</script>

View File

@@ -1,5 +1,5 @@
<template>
<q-card v-for="boat in boats" :key="boat.id" flat>
<q-card v-for="boat in boats" :key="boat.id" flat class="mobile-card">
<q-card-section>
<q-img :src="boat.imgsrc" :fit="'scale-down'">
<div class="row absolute-top">
@@ -21,15 +21,9 @@
</template>
<script setup lang="ts">
import { Boat } from './models';
import { Boat } from 'src/stores/boat';
defineProps({
boats: Array<Boat>,
});
</script>
<style lang="sass" scoped>
.my-card
width: 100%
max-width: 400px
</style>

View File

@@ -1,10 +0,0 @@
export interface test {
totalCount: number;
}
export interface ReferenceEntry {
id: number;
title: string;
subtitle: string;
content: string;
}

4
src/css/app.sass Normal file
View File

@@ -0,0 +1,4 @@
// app global css in SASS form
.mobile-card
width: 100%
max-width: 450px

View File

@@ -1 +0,0 @@
// app global css in SCSS form

View File

@@ -1,49 +1,19 @@
<template>
<q-layout view="hHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title> OYS Borrow a Boat </q-toolbar-title>
<q-tabs shrink>
<q-tab>
<div v-if="loggedInUser">{{ loggedInUser.name.split(' ')[0] }}</div>
</q-tab>
</q-tabs>
</q-toolbar>
</q-header>
<LeftDrawer :drawer="leftDrawerOpen" @drawer-toggle="toggleLeftDrawer" />
<q-layout view="hHh Lpr fFf">
<q-page-container>
<router-view />
</q-page-container>
<q-footer>
<BottomNavComponent />
</q-footer>
</q-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { useAuthStore } from 'src/stores/auth';
import LeftDrawer from 'components/LeftDrawer.vue';
import BottomNavComponent from 'src/components/BottomNavComponent.vue';
const q = useQuasar();
const leftDrawerOpen = ref(false);
const authStore = useAuthStore();
const loggedInUser = authStore.currentUser;
// q.fullscreen.request();
q.addressbarColor.set('#14539a');
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
</script>

View File

@@ -1,13 +1,15 @@
<template>
<q-page padding>
<toolbar-component pageTitle="Boats" />
<q-page>
<boat-preview-component :boats="boats" />
</q-page>
</template>
<script lang="ts" setup>
import BoatPreviewComponent from 'src/components/BoatPreviewComponent.vue';
import BoatPreviewComponent from 'src/components/boat/BoatPreviewComponent.vue';
import { ref } from 'vue';
import { useBoatStore } from 'src/stores/boat';
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
const boats = ref(useBoatStore().boats);
</script>

View File

@@ -1,8 +0,0 @@
<template>
<q-page padding>
<!-- content -->
</q-page>
</template>
<script setup lang="ts">
</script>

View File

@@ -1,4 +1,5 @@
<template>
<toolbar-component pageTitle="Certification" />
<q-page padding>
<CertificationComponent />
</q-page>
@@ -6,4 +7,5 @@
<script setup lang="ts">
import CertificationComponent from 'components/CertificationComponent.vue';
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
</script>

View File

@@ -1,8 +1,25 @@
<template>
<toolbar-component pageTitle="Checklists" />
<q-page padding>
<!-- content -->
<q-card bordered separator style="max-width: 400px">
<q-card-section clickable v-ripple>
<div class="text-h6">Engine Starting</div>
<div class="text-subtitle2">
Proper procedures for starting an outboard engine.
</div>
</q-card-section>
</q-card>
<q-card bordered separator style="max-width: 400px">
<q-card-section clickable v-ripple>
<div class="text-h6">Pre-Sail Checklist J/27</div>
<div class="text-subtitle2">
Mandatory Safety and Equipment readiness check for J/27 class boats.
</div>
</q-card-section>
</q-card>
</q-page>
</template>
<script setup lang="ts">
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
</script>

View File

@@ -1,4 +1,5 @@
<template>
<ToolbarComponent />
<q-page class="row justify-center">
<q-img alt="OYS Logo" src="~assets/oysqn_logo.png" fit="scale-down" />
<q-list class="full-width mobile-only">
@@ -7,7 +8,6 @@
:key="link.name"
>
<q-btn
:v-model="auth.ready"
:icon="link.icon"
color="primary"
:size="`1.25em`"
@@ -24,7 +24,5 @@
<script lang="ts" setup>
import { links } from 'src/router/navlinks.js';
import { useAuthStore } from 'stores/auth';
const auth = useAuthStore();
import ToolbarComponent from 'components/ToolbarComponent.vue';
</script>

View File

@@ -1,8 +1,45 @@
<template>
<toolbar-component pageTitle="Member Profile" />
<q-page padding>
<!-- content -->
<q-list bordered>
<q-separator />
<q-item>
<q-item-section avatar>
<q-avatar icon="person" />
</q-item-section>
<q-item-section>
Ricky Gervais
<q-item-label caption>Name</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-avatar icon="numbers" />
</q-item-section>
<q-item-section>
12345
<q-item-label caption>Member ID</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<q-item>
<q-item-section>
<q-item-label overline>Certifications</q-item-label>
<q-chip square icon="verified" color="primary" text-color="white"
>J/27</q-chip
>
<q-chip square icon="verified" color="green" text-color="white"
>Capri25</q-chip
>
<q-chip square icon="verified" color="grey-8" text-color="white"
>Night</q-chip
>
</q-item-section>
</q-item>
</q-list>
</q-page>
</template>
<script setup lang="ts">
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
</script>

View File

@@ -1,25 +0,0 @@
<template>
<q-page padding> <ReferencePreviewComponent :entries="items" /> </q-page>
</template>
<script setup lang="ts">
import ReferencePreviewComponent from 'src/components/ReferencePreviewComponent.vue';
import { ref } from 'vue';
const items = ref([
{
id: 1,
title: 'J/27 Background',
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.
J27moor
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.`,
},
]);
</script>

View File

@@ -1,8 +1,26 @@
<template>
<toolbar-component pageTitle="Tasks" />
<q-page padding>
<!-- content -->
<q-card bordered separator class="mobile-card">
<q-card-section clickable v-ripple>
<div class="text-h6">Launch Prep</div>
<div class="text-subtitle2">Prepare for Launch</div>
<q-chip size="md" color="green" text-color="white" icon="alarm">
APR 1,2024
</q-chip>
<q-chip size="md" icon="build"> 24 tasks </q-chip>
</q-card-section>
</q-card>
<q-card bordered separator class="mobile-card">
<q-card-section clickable v-ripple>
<div class="text-h6">General Maintenance</div>
<div class="text-subtitle2">Day to day maintenance and upkeep</div>
<q-chip size="md" icon="build"> 4 tasks </q-chip>
</q-card-section>
</q-card>
</q-page>
</template>
<script setup lang="ts">
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
</script>

View File

@@ -0,0 +1,12 @@
<template>
<q-page padding>
<q-page padding> <ReferencePreviewComponent :entries="items" /> </q-page>
</q-page>
</template>
<script setup lang="ts">
import ReferencePreviewComponent from 'src/components/ReferencePreviewComponent.vue';
import { ref } from 'vue';
import { useReferenceStore } from 'src/stores/reference';
const items = ref(useReferenceStore().allItems);
</script>

View File

@@ -0,0 +1,12 @@
<template>
<q-page padding>
<div class="text-h4">Engine Starting</div>
<q-video
title="Engine Starting"
:ratio="16 / 9"
src="https://www.youtube.com/embed/GMHMLDlkKcE"
></q-video>
</q-page>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,8 @@
<template>
<toolbar-component pageTitle="Reference" />
<router-view />
</template>
<script setup lang="ts">
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
</script>

View File

@@ -0,0 +1,163 @@
<template>
<q-page padding>
<q-list>
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<q-input
bottom-slots
v-model="bookingForm.name"
label="Creating reservation for"
readonly
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<q-expansion-item
expand-separator
v-model="resourceView"
icon="calendar_month"
label="Boat and Time"
default-opened
:caption="bookingSummary"
>
<q-separator />
<resource-schedule-viewer-component
@on-click-time="onClickTime"
@on-update-duration="
(value) => {
bookingForm.duration = value;
}
"
/>
<q-banner
rounded
class="bg-warning text-grey-10"
v-if="bookingForm.boat?.defects"
>
<template v-slot:avatar>
<q-icon name="warning" color="grey-10" />
</template>
{{ bookingForm.boat.name }} currently has the following notices:
<ol>
<li
v-for="defect in bookingForm.boat.defects"
:key="defect.description"
>
{{ defect.description }}
</li>
</ol>
</q-banner>
<q-card-section>
<q-btn
color="primary"
class="full-width"
icon="keyboard_arrow_down"
icon-right="keyboard_arrow_down"
label="Next: Crew & Passengers"
@click="resourceView = false"
/></q-card-section>
</q-expansion-item>
<q-expansion-item
expand-separator
icon="people"
label="Crew and Passengers"
default-opened
>
<q-separator />
</q-expansion-item>
<q-item-section>
<q-btn label="Submit" type="submit" color="primary" />
</q-item-section> </q-form
></q-list>
</q-page>
</template>
<script setup lang="ts">
import { reactive, ref, computed, watch } from 'vue';
import { useAuthStore } from 'src/stores/auth';
import { Boat, useBoatStore } from 'src/stores/boat';
import { Dialog, date } from 'quasar';
import ResourceScheduleViewerComponent from 'src/components/ResourceScheduleViewerComponent.vue';
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
import { useScheduleStore, Reservation } from 'src/stores/schedule';
const auth = useAuthStore();
const dateFormat = 'ddd MMM D, YYYY h:mm A';
const resourceView = ref(true);
const scheduleStore = useScheduleStore();
const bookingForm = reactive({
bookingId: scheduleStore.getNewId(),
name: auth.currentUser?.name,
boat: <Boat | undefined>undefined,
startDate: date.formatDate(new Date(), dateFormat),
endDate: computed(() =>
date.formatDate(
date.addToDate(bookingForm.startDate, {
hours: bookingForm.duration,
}),
dateFormat
)
),
duration: 1,
});
watch(bookingForm, (b, a) => {
const newRes = <Reservation>{
id: b.bookingId,
user: b.name,
resource: b.boat,
start: date.extractDate(b.startDate, dateFormat),
end: date.extractDate(b.endDate, dateFormat),
reservationDate: new Date(),
status: 'tentative',
};
//TODO: Turn this into a validator.
scheduleStore.isOverlapped(newRes)
? Dialog.create({ message: 'This booking overlaps another!' })
: scheduleStore.addOrCreateReservation(newRes);
});
const onReset = () => {
// TODO
};
const onSubmit = () => {
// TODO
};
const onClickTime = (data) => {
bookingForm.boat = data.scope.resource;
bookingForm.startDate = date.formatDate(
date.addToDate(makeDateTime(data.scope.timestamp), { hours: 5 }), // A terrible hack to convert back to EST. TODO: FIX!!!!
dateFormat
);
console.log(bookingForm.startDate);
};
const bookingDuration = computed(() => {
const diff = date.getDateDiff(
bookingForm.endDate,
bookingForm.startDate,
'minutes'
);
return diff <= 0
? 'Invalid'
: (diff > 60 ? Math.trunc(diff / 60) + ' hours' : '') +
(diff % 60 > 0 ? ' ' + (diff % 60) + ' minutes' : '');
});
const bookingSummary = computed(() => {
return bookingForm.boat && bookingForm.startDate && bookingForm.endDate
? `${bookingForm.boat.name} @ ${bookingForm.startDate} for ${bookingDuration.value}`
: '';
});
const limitDate = (startDate: string) => {
return date.isBetweenDates(
startDate,
new Date(),
date.addToDate(new Date(), { days: 21 }),
{ inclusiveFrom: true, inclusiveTo: true, onlyDate: true }
);
};
</script>

View File

@@ -0,0 +1,12 @@
<template>
<q-page padding>
<!-- content -->
</q-page>
</template>
<script setup lang="ts">
import { useScheduleStore } from 'src/stores/schedule';
const scheduleStore = useScheduleStore();
scheduleStore.loadSampleData();
</script>

View File

@@ -0,0 +1,27 @@
<template>
<q-page padding>
<q-item v-for="link in navlinks" :key="link.label">
<q-btn
:icon="link.icon"
color="primary"
size="1.25em"
:to="link.to"
:label="link.label"
rounded
class="full-width"
align="left"
/>
</q-item>
</q-page>
</template>
<script setup lang="ts">
const navlinks = [
{
icon: 'more_time',
to: '/schedule/book',
label: 'Create a Reservation',
},
{ icon: 'calendar_month', to: '/schedule/view', label: 'View Schedule' },
];
</script>

View File

@@ -0,0 +1,8 @@
<template>
<toolbar-component pageTitle="Schedule" />
<router-view />
</template>
<script setup lang="ts">
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
</script>

View File

@@ -7,43 +7,43 @@ export const links = [
},
{
name: 'Profile',
to: 'profile',
to: '/profile',
icon: 'account_circle',
front_links: false,
},
{
name: 'Boats',
to: 'boat',
to: '/boat',
icon: 'sailing',
front_links: true,
},
{
name: 'Booking',
to: 'booking',
name: 'Schedule',
to: '/schedule',
icon: 'calendar_month',
front_links: true,
},
{
name: 'Certifications',
to: 'certification',
to: '/certification',
icon: 'verified',
front_links: true,
},
{
name: 'Checklists',
to: 'checklist',
to: '/checklist',
icon: 'checklist',
front_links: true,
},
{
name: 'Reference',
to: 'reference',
to: '/reference',
icon: 'info_outline',
front_links: true,
},
{
name: 'Tasks',
to: 'task',
to: '/task',
icon: 'build',
front_links: true,
},

View File

@@ -1,49 +1,96 @@
import ScheduleIndexPage from 'pages/schedule/ScheduleIndexPage.vue';
import ChecklistPageVue from 'pages/ChecklistPage.vue';
import LoginPageVue from 'pages/LoginPage.vue';
import ReferencePageVue from 'src/pages/reference/ReferencePage.vue';
import ReferenceIndexPageVue from 'src/pages/reference/ReferenceIndexPage.vue';
import ReferenceItemPageVue from 'src/pages/reference/ReferenceItemPage.vue';
import MainLayoutVue from 'src/layouts/MainLayout.vue';
import BoatPageVue from 'src/pages/BoatPage.vue';
import CertificationPageVue from 'src/pages/CertificationPage.vue';
import IndexPageVue from 'src/pages/IndexPage.vue';
import ProfilePageVue from 'src/pages/ProfilePage.vue';
import TaskPageVue from 'src/pages/TaskPage.vue';
import { RouteRecordRaw } from 'vue-router';
import SchedulePageView from 'pages/schedule/SchedulePageView.vue';
import BoatReservationPageVue from 'src/pages/schedule/BoatReservationPage.vue';
import BoatScheduleViewVue from 'src/pages/schedule/BoatScheduleView.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
component: MainLayoutVue,
// If we get so big we need lazy loading, we can use imports again
// component: () => import('layouts/MainLayout.vue'),
children: [
{
path: '',
component: () => import('pages/IndexPage.vue'),
// If we get so big we need lazy loading, we can use imports again
// component: () => import('pages/IndexPage.vue'),
component: IndexPageVue,
name: 'index',
},
{
path: '/boat',
component: () => import('pages/BoatPage.vue'),
component: BoatPageVue,
name: 'boat',
},
{
path: '/booking',
component: () => import('pages/BookingPage.vue'),
name: 'booking',
path: '/schedule',
component: SchedulePageView,
name: 'schedule',
children: [
{
path: '',
component: ScheduleIndexPage,
name: 'schedule-index',
},
{
path: 'book',
component: BoatReservationPageVue,
name: 'reserve-boat',
},
{
path: 'view',
component: BoatScheduleViewVue,
name: 'boat-schedule',
},
],
},
{
path: '/certification',
component: () => import('pages/CertificationPage.vue'),
component: CertificationPageVue,
name: 'certification',
},
{
path: '/task',
component: () => import('pages/TaskPage.vue'),
component: TaskPageVue,
name: 'task',
},
{
path: '/checklist',
component: () => import('pages/ChecklistPage.vue'),
component: ChecklistPageVue,
name: 'checklist',
},
{
path: '/profile',
component: () => import('pages/ProfilePage.vue'),
component: ProfilePageVue,
name: 'profile',
},
{
path: '/reference',
component: () => import('pages/ReferencePage.vue'),
component: ReferencePageVue,
name: 'reference',
children: [
{
path: '',
component: ReferenceIndexPageVue,
name: 'reference-index',
},
{
path: '/reference/:id/view',
component: ReferenceItemPageVue,
},
],
},
],
},
@@ -65,7 +112,7 @@ const routes: RouteRecordRaw[] = [
},
{
path: '/login',
component: () => import('pages/LoginPage.vue'),
component: LoginPageVue,
name: 'login',
meta: {
publicRoute: true,

View File

@@ -5,9 +5,22 @@ import { defineStore } from 'pinia';
export interface Boat {
id: number;
name: string;
class: string;
year: number;
imgsrc: string;
class?: string;
year?: number;
imgsrc?: string;
iconsrc?: string;
booking?: {
available: boolean;
requiredCerts: string[];
maxDuration: number;
maxPassengers: number;
};
defects?: {
type: string;
severity: string;
description: string;
detail?: string;
}[];
}
const getSampleData = () => [
@@ -16,21 +29,40 @@ const getSampleData = () => [
name: 'ProjectX',
class: 'J/27',
year: 1981,
imgsrc: '/src/assets/j27.png',
imgsrc: '/tmpimg/j27.png',
iconsrc: '/tmpimg/projectx_avatar256.png',
defects: [
{
type: 'engine',
severity: 'moderate',
description: 'Fuel line leaks at engine fitting.',
detail: `The gasket in the end of the fuel hose is damaged, and does not properly seal.
This will cause fuel to leak, and will allow air into the fuel chamber, causing a lean mixture,
and rough engine performance.`,
},
{
type: 'rigging',
severity: 'moderate',
description: 'Tiller extension is broken.',
detail:
'The tiller extension swivel is broken, and will not attach to the tiller.',
},
],
},
{
id: 2,
name: 'Take5',
class: 'J/27',
year: 1985,
imgsrc: '/src/assets/j27.png',
imgsrc: '/tmpimg/j27.png',
iconsrc: '/tmpimg/take5_avatar32.png',
},
{
id: 3,
name: 'WeeBeestie',
class: 'Capri 25',
year: 1989,
imgsrc: '/src/assets/capri25.png',
imgsrc: '/tmpimg/capri25.png',
},
];

View File

@@ -0,0 +1,33 @@
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(),
}),
// getters: {
// doubleCount (state) {
// return state.counter * 2;
// }
// },
// actions: {
// increment () {
// this.counter++;
// }
// }
});

127
src/stores/reference.ts Normal file
View File

@@ -0,0 +1,127 @@
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) {
(category: string) => {
return state.allItems.filter((c) => c.category === category);
};
},
},
actions: {
// increment () {
// this.counter++;
// }
},
});

140
src/stores/schedule.ts Normal file
View File

@@ -0,0 +1,140 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { Boat, useBoatStore } from './boat';
import { date } from 'quasar';
import { DateOptions } from 'quasar';
export interface Reservation {
id: number;
user: string;
start: Date;
end: Date;
resource: Boat;
reservationDate: Date;
status?: string;
}
function getSampleData(): Reservation[] {
const sampleData = [
{
id: 1,
user: 'John Smith',
start: '12:00',
end: '14:00',
boat: 1,
status: 'confirmed',
},
{
id: 2,
user: 'Bob Barker',
start: '18:00',
end: '20:00',
boat: 1,
status: 'confirmed',
},
{
id: 3,
user: 'Peter Parker',
start: '8:00',
end: '10:00',
boat: 2,
status: 'tentative',
},
{
id: 4,
user: 'Vince McMahon',
start: '13:00',
end: '17:00',
boat: 2,
status: 'pending',
},
{
id: 5,
user: 'Heather Graham',
start: '06:00',
end: '09:00',
boat: 3,
status: 'confirmed',
},
{
id: 6,
user: 'Lawrence Fishburne',
start: '18:00',
end: '20:00',
boat: 3,
},
];
const boatStore = useBoatStore();
const now = new Date();
const splitTime = (x: string): string[] => {
return x.split(':');
};
const makeOpts = (x: string[]): DateOptions => {
return { hour: parseInt(x[0]), minute: parseInt(x[1]) };
};
return sampleData.map((entry): Reservation => {
const boat = <Boat>boatStore.boats.find((b) => b.id == entry.boat);
return {
id: entry.id,
user: entry.user,
start: date.adjustDate(now, makeOpts(splitTime(entry.start))),
end: date.adjustDate(now, makeOpts(splitTime(entry.end))),
resource: boat,
reservationDate: now,
status: entry.status,
};
});
}
export const useScheduleStore = defineStore('schedule', () => {
const reservations = ref<Reservation[]>(getSampleData());
const getBoatReservations = (
boat: number | string,
curDate: Date
): Reservation[] => {
return reservations.value.filter((x) => {
return (
(x.start.getDate() == curDate.getDate() ||
x.end.getDate() == curDate.getDate()) &&
x.resource != undefined &&
(typeof boat == 'number'
? x.resource.id == boat
: x.resource.name == boat)
);
});
};
const isOverlapped = (res: Reservation) => {
const lapped = reservations.value.filter(
(entry: Reservation) =>
entry.id != res.id &&
entry.resource == res.resource &&
((entry.start <= res.start && entry.end > res.start) ||
(entry.end >= res.end && entry.start <= res.end))
);
return lapped.length > 0;
};
const getNewId = () => {
// Trivial placeholder
return Math.max(...reservations.value.map((item) => item.id)) + 1;
};
const addOrCreateReservation = (reservation: Reservation) => {
const index = reservations.value.findIndex(
(res) => res.id == reservation.id
);
index != -1
? (reservations.value[index] = reservation)
: reservations.value.push(reservation);
};
return {
reservations,
getBoatReservations,
getNewId,
addOrCreateReservation,
isOverlapped,
};
});

View File

@@ -1,12 +0,0 @@
import { defineStore } from 'pinia';
import state from './state';
import * as getters from './getters';
import * as mutations from './mutations';
import * as actions from './actions';
export const useScheduleStore = defineStore('schedule', {
state,
getters,
mutations,
actions,
});

View File

@@ -1,19 +0,0 @@
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0
}),
getters: {
doubleCount (state) {
return state.counter * 2;
}
},
actions: {
increment () {
this.counter++;
}
}
});

View File

@@ -1110,6 +1110,18 @@
resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.7.tgz#9416689f2e55f594f1135383c35b9ab03875c522"
integrity sha512-nYF3gVE/si1YJ/D4qmAiHGwxoJIDCvTT8NI6ZmbTMPrur4J8xBKhfhfhyLoQ4k2jJZP6Rx0rUcB71FBNC2C8vQ==
"@quasar/quasar-app-extension-qcalendar@^4.0.0-beta.15":
version "4.0.0-beta.15"
resolved "https://registry.yarnpkg.com/@quasar/quasar-app-extension-qcalendar/-/quasar-app-extension-qcalendar-4.0.0-beta.15.tgz#1e85626a104c3a33083b7237f50ccf5f9048926a"
integrity sha512-i6hQkcP70LXLfVMPZMKQjSg3681gjZmASV3vq6ULzc0LhtBiPneLdVNNtH2itkWxAmaUj+1heQDI5Pa0F7VKLQ==
dependencies:
"@quasar/quasar-ui-qcalendar" "^4.0.0-beta.15"
"@quasar/quasar-ui-qcalendar@^4.0.0-beta.15":
version "4.0.0-beta.16"
resolved "https://registry.yarnpkg.com/@quasar/quasar-ui-qcalendar/-/quasar-ui-qcalendar-4.0.0-beta.16.tgz#90dca0962f1fe1068361f387893df6c5da7522e2"
integrity sha512-KVbFJD1HQp91tiklv+6XsG7bq8FKK6mhhnoVzmjgoyhUAEb9csfbDPbpegy1/FzXy3o0wITe6mmRZ8nbaiMEZg==
"@quasar/render-ssr-error@^1.0.1":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@quasar/render-ssr-error/-/render-ssr-error-1.0.2.tgz#92abb0d61cfdfbf51c2ec3cc2e4aadbf1c79f04f"