Compare commits
10 Commits
7c0507daee
...
8d194701b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
8d194701b3
|
|||
|
7658ba2bed
|
|||
|
9d1d7398b9
|
|||
|
8600000e24
|
|||
|
aed0462e05
|
|||
|
a3cdbbfbbd
|
|||
|
8200bcde52
|
|||
|
a6540a2a02
|
|||
|
caf9535849
|
|||
|
c307f62a05
|
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 273 KiB |
BIN
public/tmpimg/projectx_avatar.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
public/tmpimg/projectx_avatar256.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
public/tmpimg/take5_avatar32.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"@quasar/qcalendar": {}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
26
src/components/BottomNavComponent.vue
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>,
|
||||
|
||||
225
src/components/ResourceScheduleViewerComponent.vue
Normal 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"
|
||||
><</span
|
||||
>
|
||||
{{ formattedMonth }}
|
||||
<span
|
||||
class="q-button"
|
||||
style="cursor: pointer; user-select: none"
|
||||
@click="onNext"
|
||||
>></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>
|
||||
33
src/components/ToolbarComponent.vue
Normal 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>
|
||||
40
src/components/boat/BoatPickerComponent.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -0,0 +1,4 @@
|
||||
// app global css in SASS form
|
||||
.mobile-card
|
||||
width: 100%
|
||||
max-width: 450px
|
||||
@@ -1 +0,0 @@
|
||||
// app global css in SCSS form
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<q-page padding>
|
||||
<!-- content -->
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
12
src/pages/reference/ReferenceIndexPage.vue
Normal 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>
|
||||
12
src/pages/reference/ReferenceItemPage.vue
Normal 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>
|
||||
8
src/pages/reference/ReferencePage.vue
Normal 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>
|
||||
163
src/pages/schedule/BoatReservationPage.vue
Normal 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>
|
||||
12
src/pages/schedule/BoatScheduleView.vue
Normal 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>
|
||||
27
src/pages/schedule/ScheduleIndexPage.vue
Normal 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>
|
||||
8
src/pages/schedule/SchedulePageView.vue
Normal 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>
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
33
src/stores/memberProfile.ts
Normal 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
@@ -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
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
});
|
||||
12
yarn.lock
@@ -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"
|
||||
|
||||