30 Commits
v0.6.1 ... main

Author SHA1 Message Date
ab6b909fba fix: semantic-release now working correctly in development 2024-06-22 14:39:50 -04:00
9fdab2acc9 fix: correct paths to version 2024-06-22 12:11:45 -04:00
68c242ae81 feat: Add automatic version.js generation 2024-06-22 12:01:59 -04:00
cb3c1ab05f mend 2024-06-22 11:20:32 -04:00
02dae967a2 mend 2024-06-22 11:14:02 -04:00
77ae081031 mend 2024-06-22 11:13:58 -04:00
aed60cc0d5 mend 2024-06-22 11:09:29 -04:00
278c7309b7 feat: add semantic-release 2024-06-22 11:07:08 -04:00
a11b2a0568 fix: reactivity bug with ListReservationsPage
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m32s
2024-06-21 23:44:34 -04:00
ff8e54449a feat: add realtime updates of interval and reservation 2024-06-21 23:13:30 -04:00
64a59e856f feat: rudimentary realtime update of intervals 2024-06-20 23:36:05 -04:00
5e8c5a1631 feat: enable websocket proxy for dev 2024-06-20 23:14:20 -04:00
e97949cab3 fix: Improve reactivity in intervals 2024-06-20 21:52:00 -04:00
b7a3608e67 fix: dev targets 2024-06-19 23:02:01 -04:00
bbb544c029 chore: bump version
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m26s
2024-06-19 19:13:33 -04:00
da42f6ed22 chore: Update gitignore
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m8s
2024-06-17 16:31:29 -04:00
8016e20451 fix: remove dotenv files from repo 2024-06-17 16:30:59 -04:00
64ee8f4fea chore: Change actions to only run on devel branch
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m13s
2024-06-17 16:25:44 -04:00
17e8d7dc37 chore: manually bump version 2024-06-17 16:20:20 -04:00
a409b0a5c7 refactor: Configuration improvement
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m13s
2024-06-17 15:37:45 -04:00
6ec4a1e025 feat: Re-enable profile page and allow editing name
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 4m48s
2024-06-15 10:28:38 -04:00
d063b0cf0d fix: (auth) token login fix 2024-06-15 00:05:41 -04:00
643d74e29d feat: (auth) switch to OTP code via e-mail
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m24s
2024-06-14 16:23:48 -04:00
1526a10630 feat: (auth) Add ability to signup with e-mail 2024-06-14 15:19:29 -04:00
fc035106d0 fix: trying to load resources before auth
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m29s
2024-06-13 23:46:43 -04:00
8ae855838b feat: (auth) Add Google and DIscord Auth
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m0s
2024-06-13 23:38:03 -04:00
9bd10b56d9 Update TOS and Privacy Policy 2024-06-13 22:53:03 -04:00
1a78f82c5e chore: update version strings 2024-06-13 20:35:13 -04:00
475ba45248 Fix imports
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m58s
2024-06-10 11:44:28 -04:00
2a949d771a refactor: Redo env var names to work with vite. Add version
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m1s
2024-06-09 08:53:34 -04:00
43 changed files with 4904 additions and 1503 deletions

View File

@@ -1,2 +0,0 @@
APPWRITE_API_ENDPOINT='https://appwrite.oys.undock.ca/v1'
APPWRITE_API_PROJECT='bab'

View File

@@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} is building a BAB App artifact 🚀
on: on:
push: push:
branches: branches:
- main - devel
jobs: jobs:
build: build:

5
.gitignore vendored
View File

@@ -34,4 +34,7 @@ yarn-error.log*
*.sln *.sln
# local .env files # local .env files
.env.local* .env*
# version file
src/version.js

23
.releaserc.json Normal file
View File

@@ -0,0 +1,23 @@
{
"branches": [
"main",
"next",
{ "name": "beta", "prerelease": true },
{ "name": "alpha", "prerelease": true }
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@saithodev/semantic-release-gitea",
{
"assets": [
{
"path": "dist/build-*.tar.gz",
"label": "package distribution"
}
]
}
]
]
}

View File

@@ -41,3 +41,7 @@ quasar build
### Customize the configuration ### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js). See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
### TODO
https://github.com/semantic-release/semantic-release

28
generate-version.js Normal file
View File

@@ -0,0 +1,28 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
try {
// Run semantic-release to get the next version number
const dryRunOutput = execSync('npx semantic-release --dry-run').toString();
// Extract the version number from the semantic-release output
const versionMatch = dryRunOutput.match(
/The next release version is ([\S]+)/
);
if (!versionMatch) {
throw new Error('Version number not found in semantic-release output');
}
const version = versionMatch[1];
// Create version content
const versionContent = `export const APP_VERSION = '${version}';\n`;
const versionFilePath = path.resolve(__dirname, 'src/version.js');
// Write version to file
fs.writeFileSync(versionFilePath, versionContent, 'utf8');
console.log(`Version file generated with version: ${version}`);
} catch (error) {
console.error('Error generating version file:', error);
process.exit(1);
}

36
nohup.out Normal file
View File

@@ -0,0 +1,36 @@
2024-06-06 07:42:15,841 - vorta.i18n - DEBUG - Loading translation failed for ['en-CA', 'en-Latn-CA'].
QObject::connect: No such signal QPlatformNativeInterface::systemTrayWindowChanged(QScreen*)
2024-06-06 07:42:15,884 - root - DEBUG - Not a private SSH key file: authorized_keys
2024-06-06 07:42:15,885 - root - DEBUG - Not a private SSH key file: github_rsa.pub_bak-github
2024-06-06 07:42:15,886 - root - DEBUG - Not a private SSH key file: other_keys.seahorse
2024-06-06 07:42:16,077 - root - INFO - Using NetworkManagerMonitor NetworkStatusMonitor implementation.
Requested decoration "adwaita" not found, falling back to default
qt.qpa.wayland: Wayland does not support QWindow::requestActivate()
2024-06-06 07:42:16,209 - vorta.borg.jobs_manager - DEBUG - Add job for site default
2024-06-06 07:42:16,210 - vorta.borg.jobs_manager - DEBUG - Start job on site: default
2024-06-06 07:42:16,237 - vorta.borg.borg_job - INFO - Running command /usr/bin/borg --version
2024-06-06 07:42:20,564 - vorta.borg.jobs_manager - DEBUG - Finish job for site: default
2024-06-06 07:42:20,565 - vorta.borg.jobs_manager - DEBUG - No more jobs for site: default
2024-06-06 07:42:20,566 - vorta.scheduler - DEBUG - Refreshing all scheduler timers
2024-06-06 07:42:20,568 - vorta.scheduler - DEBUG - Nothing scheduled for profile 1 because of unset repo.
qt.qpa.wayland: Wayland does not support QWindow::requestActivate()
2024-06-06 07:42:23,190 - root - DEBUG - Not a private SSH key file: authorized_keys
2024-06-06 07:42:23,191 - root - DEBUG - Not a private SSH key file: github_rsa.pub_bak-github
2024-06-06 07:42:23,191 - root - DEBUG - Not a private SSH key file: other_keys.seahorse
2024-06-06 07:42:23,204 - vorta.keyring.abc - DEBUG - Only available on macOS
2024-06-06 07:42:23,244 - asyncio - DEBUG - Using selector: EpollSelector
2024-06-06 07:42:23,245 - vorta.keyring.abc - DEBUG - Using VortaSecretStorageKeyring
2024-06-06 07:49:53,786 - vorta.keyring.abc - DEBUG - Only available on macOS
2024-06-06 07:49:53,788 - asyncio - DEBUG - Using selector: EpollSelector
2024-06-06 07:49:53,788 - vorta.keyring.abc - DEBUG - Using VortaSecretStorageKeyring
2024-06-06 07:49:53,789 - asyncio - DEBUG - Using selector: EpollSelector
2024-06-06 07:49:53,790 - vorta.keyring.secretstorage - DEBUG - Found 0 passwords matching repo URL.
qt.qpa.wayland: Wayland does not support QWindow::requestActivate()
2024-06-06 07:50:10,009 - vorta.keyring.abc - DEBUG - Only available on macOS
2024-06-06 07:50:10,011 - asyncio - DEBUG - Using selector: EpollSelector
2024-06-06 07:50:10,012 - vorta.keyring.abc - DEBUG - Using VortaSecretStorageKeyring
2024-06-06 07:50:10,012 - vorta.borg.borg_job - DEBUG - Using VortaSecretStorageKeyring keyring to store passwords.
2024-06-06 07:50:10,013 - asyncio - DEBUG - Using selector: EpollSelector
2024-06-06 07:50:10,013 - vorta.keyring.secretstorage - DEBUG - Found 0 passwords matching repo URL.
2024-06-06 07:50:10,013 - vorta.borg.borg_job - DEBUG - Password not found in primary keyring. Falling back to VortaDBKeyring.
2024-06-06 07:50:10,029 - vorta.borg.borg_job - INFO - Running command /usr/bin/borg info --info --json --log-json ssh://borg@borg.toal.ca:12022/./ptoal-linux

View File

@@ -1,16 +1,17 @@
{ {
"name": "oys_bab", "name": "oys_bab",
"version": "0.6.1", "version": "0.0.0",
"description": "Manage a Borrow a Boat program for a Yacht Club", "description": "Manage a Borrow a Boat program for a Yacht Club",
"productName": "OYS Borrow a Boat", "productName": "OYS Borrow a Boat",
"author": "Patrick Toal <ptoal@takeflight.ca>", "author": "Patrick Toal <ptoal@takeflight.ca>",
"private": true, "private": true,
"scripts": { "scripts": {
"generate-version": "node generate-version.js",
"lint": "eslint --ext .js,.ts,.vue ./", "lint": "eslint --ext .js,.ts,.vue ./",
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"No test specified\" && exit 0", "test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev", "dev": "npm run generate-version && quasar dev",
"build": "quasar build" "build": "npm run generate-version && quasar build"
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.11", "@quasar/extras": "^1.16.11",
@@ -26,6 +27,10 @@
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-vite": "^1.9.1", "@quasar/app-vite": "^1.9.1",
"@saithodev/semantic-release-gitea": "^2.1.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/github": "^10.0.6",
"@semantic-release/npm": "^12.0.1",
"@types/node": "^12.20.21", "@types/node": "^12.20.21",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.10.0",
@@ -34,8 +39,10 @@
"eslint": "^8.10.0", "eslint": "^8.10.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^9.0.0", "eslint-plugin-vue": "^9.0.0",
"git-commit-info": "^2.0.2",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"quasar": "^2.16.0", "quasar": "^2.16.0",
"semantic-release": "^24.0.0",
"typescript": "~5.3.0", "typescript": "~5.3.0",
"vite-plugin-checker": "^0.6.4", "vite-plugin-checker": "^0.6.4",
"vue-tsc": "^1.8.22", "vue-tsc": "^1.8.22",

BIN
public/tmpimg/projectX.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -10,7 +10,7 @@
const { configure } = require('quasar/wrappers'); const { configure } = require('quasar/wrappers');
module.exports = configure(function (/* ctx */) { module.exports = configure(function () {
return { return {
eslint: { eslint: {
// fix: true, // fix: true,
@@ -48,7 +48,6 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: { build: {
env: require('dotenv').config().parsed,
target: { target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node16', node: 'node16',
@@ -102,12 +101,19 @@ module.exports = configure(function (/* ctx */) {
secure: false, secure: false,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''),
}, },
'/function': { '/api/v1/realtime': {
target: 'https://6640382951eacb568371.f.appwrite.toal.ca/', target: 'wss://apidev.bab.toal.ca',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
secure: false, secure: false,
rewrite: (path) => path.replace(/^\/function/, ''), ws: true,
}, },
// '/function': {
// target: 'https://6640382951eacb568371.f.appwrite.toal.ca/',
// changeOrigin: true,
// secure: false,
// rewrite: (path) => path.replace(/^\/function/, ''),
// },
}, },
// For reverse-proxying via haproxy // For reverse-proxying via haproxy
// hmr: { // hmr: {

View File

@@ -1,5 +1,5 @@
{ {
"orientation": "portrait", "orientation": "natural",
"background_color": "#ffffff", "background_color": "#ffffff",
"theme_color": "#027be3", "theme_color": "#027be3",
"icons": [ "icons": [

View File

@@ -9,33 +9,35 @@ register(process.env.SERVICE_WORKER_FILE, {
// to ServiceWorkerContainer.register() // to ServiceWorkerContainer.register()
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter
// registrationOptions: { scope: './' }, registrationOptions: { scope: './' },
ready (/* registration */) { ready(/* registration */) {
// console.log('Service worker is active.') console.log('Service worker is active.');
}, },
registered (/* registration */) { registered(/* registration */) {
// console.log('Service worker has been registered.') console.log('Service worker has been registered.');
}, },
cached (/* registration */) { cached(/* registration */) {
// console.log('Content has been cached for offline use.') console.log('Content has been cached for offline use.');
}, },
updatefound (/* registration */) { updatefound(/* registration */) {
// console.log('New content is downloading.') console.log('New content is downloading.');
}, },
updated (/* registration */) { updated(/* registration */) {
// console.log('New content is available; please refresh.') console.log('New content is available; please refresh.');
}, },
offline () { offline() {
// console.log('No internet connection found. App is running in offline mode.') console.log(
'No internet connection found. App is running in offline mode.'
);
}, },
error (/* err */) { error(err) {
// console.error('Error during service worker registration:', err) console.error('Error during service worker registration:', err);
}, },
}); });

View File

@@ -5,8 +5,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, onMounted } from 'vue'; import { defineComponent, onMounted } from 'vue';
import { useAuthStore } from './stores/auth'; import { useAuthStore } from './stores/auth';
import { useBoatStore } from './stores/boat';
import { useReservationStore } from './stores/reservation';
defineComponent({ defineComponent({
name: 'OYS Borrow-a-Boat', name: 'OYS Borrow-a-Boat',
@@ -14,7 +12,5 @@ defineComponent({
onMounted(async () => { onMounted(async () => {
await useAuthStore().init(); await useAuthStore().init();
await useBoatStore().fetchBoats();
await useReservationStore().fetchUserReservations();
}); });
</script> </script>

View File

@@ -14,56 +14,72 @@ import type { Router } from 'vue-router';
const client = new Client(); const client = new Client();
let APPWRITE_API_ENDPOINT, APPWRITE_API_PROJECT; const API_ENDPOINT = import.meta.env.VITE_APPWRITE_API_ENDPOINT;
const API_PROJECT = import.meta.env.VITE_APPWRITE_API_PROJECT;
// Private self-hosted appwrite if (API_ENDPOINT && API_PROJECT) {
if (process.env.APPWRITE_API_ENDPOINT && process.env.APPWRITE_API_PROJECT) { client.setEndpoint(API_ENDPOINT).setProject(API_PROJECT);
APPWRITE_API_ENDPOINT = process.env.APPWRITE_API_ENDPOINT;
APPWRITE_API_PROJECT = process.env.APPWRITE_API_PROJECT;
} else if (process.env.DEV) {
APPWRITE_API_ENDPOINT = 'http://localhost:4000/api/v1';
APPWRITE_API_PROJECT = '65ede55a213134f2b688';
} else { } else {
APPWRITE_API_ENDPOINT = 'https://appwrite.oys.undock.ca/v1'; console.error(
APPWRITE_API_PROJECT = 'bab'; 'Must configure VITE_APPWRITE_API_ENDPOINT and VITE_APPWRITE_API_PROJECT'
);
} }
client.setEndpoint(APPWRITE_API_ENDPOINT).setProject(APPWRITE_API_PROJECT);
const pwresetUrl = process.env.DEV type AppwriteIDConfig = {
? 'http://localhost:4000/pwreset' databaseId: string;
: 'https://oys.undock.ca/pwreset'; collection: {
boat: string;
reservation: string;
skillTags: string;
task: string;
taskTags: string;
interval: string;
intervalTemplate: string;
};
function: {
userinfo: string;
};
};
const AppwriteIds = process.env.DEV let AppwriteIds = <AppwriteIDConfig>{};
? {
databaseId: '65ee1cbf9c2493faf15f', console.log(API_ENDPOINT);
collection: { if (
boat: 'boat', API_ENDPOINT === 'https://apidev.bab.toal.ca/v1' ||
reservation: 'reservation', API_ENDPOINT === 'http://localhost:4000/api/v1'
skillTags: 'skillTags', ) {
task: 'task', AppwriteIds = {
taskTags: 'taskTags', databaseId: '65ee1cbf9c2493faf15f',
interval: 'interval', collection: {
intervalTemplate: 'intervalTemplate', boat: 'boat',
}, reservation: 'reservation',
function: { skillTags: 'skillTags',
userinfo: 'userinfo', task: 'task',
}, taskTags: 'taskTags',
} interval: 'interval',
: { intervalTemplate: 'intervalTemplate',
databaseId: 'bab_prod', },
collection: { function: {
boat: 'boat', userinfo: 'userinfo',
reservation: 'reservation', },
skillTags: 'skillTags', };
task: 'task', } else if (API_ENDPOINT === 'https://appwrite.oys.undock.ca/v1') {
taskTags: 'taskTags', AppwriteIds = {
interval: 'interval', databaseId: 'bab_prod',
intervalTemplate: 'intervalTemplate', collection: {
}, boat: 'boat',
function: { reservation: 'reservation',
userinfo: '664038294b5473ef0c8d', skillTags: 'skillTags',
}, task: 'task',
}; taskTags: 'taskTags',
interval: 'interval',
intervalTemplate: 'intervalTemplate',
},
function: {
userinfo: '664038294b5473ef0c8d',
},
};
}
const account = new Account(client); const account = new Account(client);
const databases = new Databases(client); const databases = new Databases(client);
@@ -120,6 +136,7 @@ async function login(email: string, password: string) {
}); });
appRouter.replace({ name: 'index' }); appRouter.replace({ name: 'index' });
} catch (error: unknown) { } catch (error: unknown) {
console.log(error);
if (error instanceof AppwriteException) { if (error instanceof AppwriteException) {
if (error.type === 'user_session_already_exists') { if (error.type === 'user_session_already_exists') {
appRouter.replace({ name: 'index' }); appRouter.replace({ name: 'index' });
@@ -147,7 +164,7 @@ async function login(email: string, password: string) {
} }
async function resetPassword(email: string) { async function resetPassword(email: string) {
await account.createRecovery(email, pwresetUrl); await account.createRecovery(email, window.location.origin + '/pwreset');
} }
export { export {

View File

@@ -164,7 +164,7 @@ const reservationStore = useReservationStore();
const boatSelect = ref(false); const boatSelect = ref(false);
const bookingForm = ref<BookingForm>({ ...newForm }); const bookingForm = ref<BookingForm>({ ...newForm });
const $q = useQuasar(); const $q = useQuasar();
const router = useRouter(); const $router = useRouter();
watch(reservation, (newReservation) => { watch(reservation, (newReservation) => {
if (!newReservation) { if (!newReservation) {
@@ -183,7 +183,7 @@ watch(reservation, (newReservation) => {
} }
}); });
const updateInterval = (interval: Interval) => { const updateInterval = (interval: Interval | null) => {
bookingForm.value.interval = interval; bookingForm.value.interval = interval;
boatSelect.value = false; boatSelect.value = false;
}; };
@@ -210,7 +210,8 @@ const boat = computed((): Boat | null => {
}); });
const onDelete = () => { const onDelete = () => {
reservationStore.deleteReservation(reservation.value?.id); reservationStore.deleteReservation(reservation.value?.$id);
$router.go(-1);
}; };
const onReset = () => { const onReset = () => {
@@ -279,6 +280,6 @@ const onSubmit = async () => {
message: 'Failed to book!' + e, message: 'Failed to book!' + e,
}); });
} }
router.go(-1); $router.go(-1);
}; };
</script> </script>

View File

@@ -0,0 +1,19 @@
<template>
<q-btn
@click="auth.discordLogin()"
style="width: 300px">
<q-avatar
left
size="sm"
class="q-ma-xs">
<img
src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/636e0a6a49cf127bf92de1e2_icon_clyde_blurple_RGB.png" />
</q-avatar>
Login with Discord
</q-btn>
</template>
<script setup lang="ts">
import { useAuthStore } from 'src/stores/auth';
const auth = useAuthStore();
</script>

View File

@@ -1,5 +1,16 @@
<template> <template>
<div @click="auth.googleLogin()">Login with Google</div> <q-btn
@click="auth.googleLogin()"
style="width: 300px">
<q-avatar
left
class="q-ma-xs"
size="sm">
<img
src="https://lh3.googleusercontent.com/COxitqgJr1sJnIDe8-jiKhxDx1FrYbtRHKJ9z_hELisAlapwE9LUPh6fcXIfb5vwpbMl4xl9H9TRFPc5NOO8Sb3VSgIBrfRYvW6cUA" />
</q-avatar>
<div>Login with Google</div>
</q-btn>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -0,0 +1,62 @@
<template>
<q-card-section class="q-ma-sm">
<q-input
v-model="password"
label="New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<q-input
v-model="confirmPassword"
label="Confirm New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<div class="text-caption q-py-md">Enter a new password.</div>
</q-card-section>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const password = ref('');
const confirmPassword = ref('');
const newPassword = defineModel();
const validatePasswordStrength = (val: string) => {
const hasUpperCase = /[A-Z]/.test(val);
const hasLowerCase = /[a-z]/.test(val);
const hasNumbers = /[0-9]/.test(val);
const hasNonAlphas = /[\W_]/.test(val);
const isValidLength = val.length >= 8;
return (
(hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasNonAlphas &&
isValidLength) ||
'Password must be at least 8 characters long and include uppercase, lowercase, number, and special character.'
);
};
const validatePasswordsMatch = (val: string) => {
return val === password.value || 'Passwords do not match.';
};
watch([password, confirmPassword], ([newpw, newpw1]) => {
if (
validatePasswordStrength(newpw) === true &&
validatePasswordsMatch(newpw1) === true
) {
newPassword.value = newpw;
} else {
newPassword.value = '';
}
});
</script>

View File

@@ -184,7 +184,7 @@ function getEvents(scope: ResourceIntervalScope) {
scope.resource.$id scope.resource.$id
); );
return resourceEvents.map((event) => { return resourceEvents.value.map((event) => {
return { return {
left: scope.timeStartPosX(parsed(event.start)), left: scope.timeStartPosX(parsed(event.start)),
width: scope.timeDurationWidth( width: scope.timeDurationWidth(

View File

@@ -11,7 +11,7 @@
<q-toolbar-title>{{ pageTitle }}</q-toolbar-title> <q-toolbar-title>{{ pageTitle }}</q-toolbar-title>
<q-space /> <q-space />
<div>v2024.6.4.2</div> <div>v{{ APP_VERSION }}</div>
</q-toolbar> </q-toolbar>
</q-header> </q-header>
<LeftDrawer <LeftDrawer
@@ -22,6 +22,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import LeftDrawer from 'components/LeftDrawer.vue'; import LeftDrawer from 'components/LeftDrawer.vue';
import { APP_VERSION } from 'src/version';
const leftDrawerOpen = ref(false); const leftDrawerOpen = ref(false);
function toggleLeftDrawer() { function toggleLeftDrawer() {

View File

@@ -3,7 +3,7 @@
<q-card <q-card
v-for="boat in boats" v-for="boat in boats"
:key="boat.id" :key="boat.id"
class="mobile-card q-ma-sm"> class="q-ma-sm">
<q-card-section> <q-card-section>
<q-img <q-img
:src="boat.imgSrc" :src="boat.imgSrc"

View File

@@ -38,7 +38,7 @@
v-for="block in getAvailableIntervals( v-for="block in getAvailableIntervals(
scope.timestamp, scope.timestamp,
boats[scope.columnIndex] boats[scope.columnIndex]
)" ).value"
:key="block.$id"> :key="block.$id">
<div <div
class="timeblock" class="timeblock"
@@ -207,7 +207,7 @@ function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
const boatReservations = computed((): Record<string, Reservation[]> => { const boatReservations = computed((): Record<string, Reservation[]> => {
return reservationStore return reservationStore
.getReservationsByDate(selectedDate.value) .getReservationsByDate(selectedDate.value)
.reduce((result, reservation) => { .value.reduce((result, reservation) => {
if (!result[reservation.resource]) result[reservation.resource] = []; if (!result[reservation.resource]) result[reservation.resource] = [];
result[reservation.resource].push(reservation); result[reservation.resource].push(reservation);
return result; return result;

View File

@@ -14,7 +14,7 @@
<div class="col text-h6">Log in</div> <div class="col text-h6">Log in</div>
</div> </div>
</q-card-section> </q-card-section>
<q-form> <q-form @keydown.enter.prevent="doTokenLogin">
<q-card-section class="q-gutter-md"> <q-card-section class="q-gutter-md">
<q-input <q-input
v-model="email" v-model="email"
@@ -23,35 +23,37 @@
color="darkblue" color="darkblue"
filled></q-input> filled></q-input>
<q-input <q-input
v-model="password" v-if="userId"
label="Password" v-model="token"
type="password" label="6-digit code"
type="number"
color="darkblue" color="darkblue"
filled></q-input> filled></q-input>
<q-card-actions>
<q-btn
type="button"
@click="doLogin"
label="Login"
color="primary"></q-btn>
<q-space />
<q-btn
flat
color="secondary"
to="/pwreset">
Reset password
</q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions>
</q-card-section> </q-card-section>
</q-form> </q-form>
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> --> <q-card-section class="q-pa-none">
<div class="row justify-center q-ma-sm">
<q-btn
type="button"
@click="doTokenLogin"
color="primary"
label="Login with E-mail"
style="width: 300px" />
</div>
<div class="row justify-center q-ma-sm">
<GoogleOauthComponent />
</div>
<div class="row justify-center q-ma-sm">
<DiscordOauthComponent />
</div>
<div class="row justify-center">
<q-btn
flat
color="secondary"
to="/pwreset"
label="Forgot Password?" />
</div>
</q-card-section>
</q-card> </q-card>
</q-page> </q-page>
</q-page-container> </q-page-container>
@@ -75,13 +77,77 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { login } from 'boot/appwrite'; import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue'; import DiscordOauthComponent from 'src/components/DiscordOauthComponent.vue';
import { Dialog, Notify } from 'quasar';
import { useAuthStore } from 'src/stores/auth';
import { useRouter } from 'vue-router';
import { AppwriteException } from 'appwrite';
import { APP_VERSION } from 'src/version.js';
const email = ref(''); const email = ref('');
const password = ref(''); const token = ref('');
const userId = ref();
const router = useRouter();
const doLogin = async () => { console.log('version:' + APP_VERSION);
login(email.value, password.value);
const doTokenLogin = async () => {
const authStore = useAuthStore();
if (!userId.value) {
try {
const sessionToken = await authStore.createTokenSession(email.value);
userId.value = sessionToken.userId;
Dialog.create({ message: 'Check your e-mail for your login code.' });
} catch (e) {
Dialog.create({
message: 'An error occurred. Please ask for help in Discord',
});
}
} else {
const notification = Notify.create({
type: 'primary',
position: 'top',
spinner: true,
message: 'Logging you in...',
timeout: 8000,
group: false,
});
try {
await authStore.tokenLogin(userId.value, token.value);
notification({
type: 'positive',
message: 'Logged in!',
timeout: 2000,
spinner: false,
icon: 'check_circle',
});
router.replace({ name: 'index' });
} catch (error: unknown) {
if (error instanceof AppwriteException) {
if (error.type === 'user_session_already_exists') {
useRouter().replace({ name: 'index' });
notification({
type: 'positive',
message: 'Already Logged in!',
timeout: 2000,
spinner: false,
icon: 'check_circle',
});
return;
}
Dialog.create({
title: 'Login Error!',
message: error.message,
persistent: true,
});
}
notification({
type: 'negative',
message: 'Login failed.',
timeout: 2000,
});
}
}
}; };
</script> </script>

View File

@@ -1,163 +1,173 @@
<template> <template>
<q-page padding> <q-layout>
<h1>Privacy Policy for bab.toal.ca</h1> <q-page-container>
<q-page padding>
<h1>Privacy Policy for Undock.ca</h1>
<p> <p>
At OYS BAB Test, accessible from https://bab.toal.ca, one of our main At Undock, accessible from https://undock.ca, one of our main
priorities is the privacy of our visitors. This Privacy Policy document priorities is the privacy of our visitors. This Privacy Policy
contains types of information that is collected and recorded by OYS BAB document contains types of information that is collected and recorded
Test and how we use it. by Undock and how we use it.
</p> </p>
<p> <p>
If you have additional questions or require more information about our If you have additional questions or require more information about our
Privacy Policy, do not hesitate to contact us. Our Privacy Policy was Privacy Policy, do not hesitate to contact us. Our Privacy Policy was
generated with the help of generated with the help of
<a href="https://www.gdprprivacypolicy.net/" <a href="https://www.gdprprivacypolicy.net/">
>GDPR Privacy Policy Generator</a GDPR Privacy Policy Generator
> </a>
</p> </p>
<h2>General Data Protection Regulation (GDPR)</h2> <h2>General Data Protection Regulation (GDPR)</h2>
<p>We are a Data Controller of your information.</p> <p>We are a Data Controller of your information.</p>
<p> <p>
bab.toal.ca legal basis for collecting and using the personal information Undock's legal basis for collecting and using the personal information
described in this Privacy Policy depends on the Personal Information we described in this Privacy Policy depends on the Personal Information
collect and the specific context in which we collect the information: we collect and the specific context in which we collect the
</p> information:
<ul> </p>
<li>bab.toal.ca needs to perform a contract with you</li> <ul>
<li>You have given bab.toal.ca permission to do so</li> <li>Undock needs to perform a contract with you</li>
<li> <li>You have given Undock permission to do so</li>
Processing your personal information is in bab.toal.ca legitimate <li>
interests Processing your personal information is in Undock legitimate
</li> interests
<li>bab.toal.ca needs to comply with the law</li> </li>
</ul> <li>Undock needs to comply with the law</li>
</ul>
<p> <p>
bab.toal.ca will retain your personal information only for as long as is Undock will retain your personal information only for as long as is
necessary for the purposes set out in this Privacy Policy. We will retain necessary for the purposes set out in this Privacy Policy. We will
and use your information to the extent necessary to comply with our legal retain and use your information to the extent necessary to comply with
obligations, resolve disputes, and enforce our policies. our legal obligations, resolve disputes, and enforce our policies.
</p> </p>
<p> <p>
If you are a resident of the European Economic Area (EEA), you have If you are a resident of the European Economic Area (EEA), you have
certain data protection rights. If you wish to be informed what Personal certain data protection rights. If you wish to be informed what
Information we hold about you and if you want it to be removed from our Personal Information we hold about you and if you want it to be
systems, please contact us. removed from our systems, please contact us.
</p> </p>
<p> <p>
In certain circumstances, you have the following data protection rights: In certain circumstances, you have the following data protection
</p> rights:
<ul> </p>
<li> <ul>
The right to access, update or to delete the information we have on you. <li>
</li> The right to access, update or to delete the information we have on
<li>The right of rectification.</li> you.
<li>The right to object.</li> </li>
<li>The right of restriction.</li> <li>The right of rectification.</li>
<li>The right to data portability</li> <li>The right to object.</li>
<li>The right to withdraw consent</li> <li>The right of restriction.</li>
</ul> <li>The right to data portability</li>
<li>The right to withdraw consent</li>
</ul>
<h2>Log Files</h2> <h2>Log Files</h2>
<p> <p>
OYS BAB Test follows a standard procedure of using log files. These files Undock follows a standard procedure of using log files. These files
log visitors when they visit websites. All hosting companies do this and a log visitors when they visit websites. All hosting companies do this
part of hosting services' analytics. The information collected by log and a part of hosting services' analytics. The information collected
files include internet protocol (IP) addresses, browser type, Internet by log files include internet protocol (IP) addresses, browser type,
Service Provider (ISP), date and time stamp, referring/exit pages, and Internet Service Provider (ISP), date and time stamp, referring/exit
possibly the number of clicks. These are not linked to any information pages, and possibly the number of clicks. These are not linked to any
that is personally identifiable. The purpose of the information is for information that is personally identifiable. The purpose of the
analyzing trends, administering the site, tracking users' movement on the information is for analyzing trends, administering the site, tracking
website, and gathering demographic information. users' movement on the website, and gathering demographic information.
</p> </p>
<h2>Cookies and Web Beacons</h2> <h2>Cookies and Web Beacons</h2>
<p> <p>
Like any other website, OYS BAB Test uses "cookies". These cookies are Like any other website, Undock uses "cookies". These cookies are used
used to store information including visitors' preferences, and the pages to store information including visitors' preferences, and the pages on
on the website that the visitor accessed or visited. The information is the website that the visitor accessed or visited. The information is
used to optimize the users' experience by customizing our web page content used to optimize the users' experience by customizing our web page
based on visitors' browser type and/or other information. content based on visitors' browser type and/or other information.
</p> </p>
<h2>Privacy Policies</h2> <h2>Privacy Policies</h2>
<P <P>
>You may consult this list to find the Privacy Policy for each of the You may consult this list to find the Privacy Policy for each of the
advertising partners of OYS BAB Test.</P advertising partners of Undock.
> </P>
<p> <p>
Third-party ad servers or ad networks uses technologies like cookies, Third-party ad servers or ad networks uses technologies like cookies,
JavaScript, or Web Beacons that are used in their respective JavaScript, or Web Beacons that are used in their respective
advertisements and links that appear on OYS BAB Test, which are sent advertisements and links that appear on Undock, which are sent
directly to users' browser. They automatically receive your IP address directly to users' browser. They automatically receive your IP address
when this occurs. These technologies are used to measure the effectiveness when this occurs. These technologies are used to measure the
of their advertising campaigns and/or to personalize the advertising effectiveness of their advertising campaigns and/or to personalize the
content that you see on websites that you visit. advertising content that you see on websites that you visit.
</p> </p>
<p> <p>
Note that OYS BAB Test has no access to or control over these cookies that Note that Undock has no access to or control over these cookies that
are used by third-party advertisers. are used by third-party advertisers.
</p> </p>
<h2>Third Party Privacy Policies</h2> <h2>Third Party Privacy Policies</h2>
<p> <p>
OYS BAB Test's Privacy Policy does not apply to other advertisers or Undock's Privacy Policy does not apply to other advertisers or
websites. Thus, we are advising you to consult the respective Privacy websites. Thus, we are advising you to consult the respective Privacy
Policies of these third-party ad servers for more detailed information. It Policies of these third-party ad servers for more detailed
may include their practices and instructions about how to opt-out of information. It may include their practices and instructions about how
certain options. to opt-out of certain options.
</p> </p>
<p> <p>
You can choose to disable cookies through your individual browser options. You can choose to disable cookies through your individual browser
To know more detailed information about cookie management with specific options. To know more detailed information about cookie management
web browsers, it can be found at the browsers' respective websites. with specific web browsers, it can be found at the browsers'
</p> respective websites.
</p>
<h2>Children's Information</h2> <h2>Children's Information</h2>
<p> <p>
Another part of our priority is adding protection for children while using Another part of our priority is adding protection for children while
the internet. We encourage parents and guardians to observe, participate using the internet. We encourage parents and guardians to observe,
in, and/or monitor and guide their online activity. participate in, and/or monitor and guide their online activity.
</p> </p>
<p> <p>
OYS BAB Test does not knowingly collect any Personal Identifiable Undock does not knowingly collect any Personal Identifiable
Information from children under the age of 13. If you think that your Information from children under the age of 13. If you think that your
child provided this kind of information on our website, we strongly child provided this kind of information on our website, we strongly
encourage you to contact us immediately and we will do our best efforts to encourage you to contact us immediately and we will do our best
promptly remove such information from our records. efforts to promptly remove such information from our records.
</p> </p>
<h2>Online Privacy Policy Only</h2> <h2>Online Privacy Policy Only</h2>
<p> <p>
Our Privacy Policy applies only to our online activities and is valid for Our Privacy Policy applies only to our online activities and is valid
visitors to our website with regards to the information that they shared for visitors to our website with regards to the information that they
and/or collect in OYS BAB Test. This policy is not applicable to any shared and/or collect in Undock. This policy is not applicable to any
information collected offline or via channels other than this website. information collected offline or via channels other than this website.
</p> </p>
<h2>Consent</h2> <h2>Consent</h2>
<p> <p>
By using our website, you hereby consent to our Privacy Policy and agree By using our website, you hereby consent to our Privacy Policy and
to its terms. agree to its
</p> <a href="/terms-of-service">terms</a>
</q-page> .
</p>
</q-page>
</q-page-container>
</q-layout>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts"></script>

View File

@@ -1,15 +1,45 @@
<template> <template>
<toolbar-component pageTitle="Member Profile" /> <toolbar-component pageTitle="Member Profile" />
<q-page padding> <q-page
<q-list bordered> padding
class="row">
<q-list class="col-sm-4 col-12">
<q-separator /> <q-separator />
<q-item> <q-item>
<q-item-section avatar> <q-item-section avatar>
<q-avatar icon="person" /> <q-avatar icon="person" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
{{ authStore.currentUser?.name }}
<q-item-label caption>Name</q-item-label> <q-item-label caption>Name</q-item-label>
<q-input
filled
v-model="newName"
@keydown.enter.prevent="editName"
v-if="newName !== undefined" />
<div v-else>
{{ authStore.currentUser?.name }}
</div>
</q-item-section>
<q-item-section avatar>
<q-btn
square
@click="editName"
:icon="newName !== undefined ? 'check' : 'edit'" />
<q-btn
v-if="newName !== undefined"
square
color="negative"
@click="newName = undefined"
icon="cancel" />
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-avatar icon="email" />
</q-item-section>
<q-item-section>
<q-item-label caption>E-mail</q-item-label>
{{ authStore.currentUser?.email }}
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator /> <q-separator />
@@ -17,15 +47,27 @@
<q-item-section> <q-item-section>
<q-item-label overline>Certifications</q-item-label> <q-item-label overline>Certifications</q-item-label>
<div> <div>
<q-chip square icon="verified" color="green" text-color="white" <q-chip
>J/27</q-chip square
> icon="verified"
<q-chip square icon="verified" color="blue" text-color="white" color="green"
>Capri25</q-chip text-color="white">
> J/27
<q-chip square icon="verified" color="grey-9" text-color="white" </q-chip>
>Night</q-chip <q-chip
> square
icon="verified"
color="blue"
text-color="white">
Capri25
</q-chip>
<q-chip
square
icon="verified"
color="grey-9"
text-color="white">
Night
</q-chip>
</div> </div>
</q-item-section> </q-item-section>
</q-item> </q-item>
@@ -36,6 +78,21 @@
<script setup lang="ts"> <script setup lang="ts">
import ToolbarComponent from 'src/components/ToolbarComponent.vue'; import ToolbarComponent from 'src/components/ToolbarComponent.vue';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
import { ref } from 'vue';
const authStore = useAuthStore(); const authStore = useAuthStore();
const newName = ref();
const editName = async () => {
if (newName.value) {
try {
await authStore.updateName(newName.value);
newName.value = undefined;
} catch (e) {
console.log(e);
}
} else {
newName.value = authStore.currentUser?.name || '';
}
};
</script> </script>

View File

@@ -34,58 +34,27 @@
@click="resetPw" @click="resetPw"
label="Send Reset Link" label="Send Reset Link"
color="primary"></q-btn> color="primary"></q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions> </q-card-actions>
</q-card-section> </q-card-section>
</q-form> </q-form>
<q-form <div v-else-if="validResetLink()">
@submit="submitNewPw" <q-form
v-else-if="validResetLink()"> @submit="submitNewPw"
<q-card-section class="q-ma-sm"> @keydown.enter.prevent="resetPw">
<q-input <NewPasswordComponent v-model="newPassword" />
v-model="password" <q-card-actions>
label="New Password" <q-btn
type="password" type="submit"
color="darkblue" label="Reset Password"
:rules="[validatePasswordStrength]" color="primary"></q-btn>
lazy-rules </q-card-actions>
filled></q-input> </q-form>
<q-input </div>
v-model="confirmPassword"
label="Confirm New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<div class="text-caption q-py-md">Enter a new password.</div>
</q-card-section>
<q-card-actions>
<q-btn
type="submit"
label="Reset Password"
color="primary"></q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions>
</q-form>
<q-card <q-card
v-else v-else
class="text-center"> class="text-center">
<span class="text-h5">Invalid reset link.</span> <span class="text-h5">Invalid reset link.</span>
</q-card> </q-card>
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
</q-card> </q-card>
</q-page> </q-page>
</q-page-container> </q-page-container>
@@ -112,38 +81,11 @@ import { ref } from 'vue';
import { account, resetPassword } from 'boot/appwrite'; import { account, resetPassword } from 'boot/appwrite';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Dialog } from 'quasar'; import { Dialog } from 'quasar';
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue'; import NewPasswordComponent from 'components/NewPasswordComponent.vue';
const email = ref(''); const email = ref('');
const router = useRouter(); const router = useRouter();
const password = ref(''); const newPassword = ref();
const confirmPassword = ref('');
const validatePasswordStrength = (val: string) => {
const hasUpperCase = /[A-Z]/.test(val);
const hasLowerCase = /[a-z]/.test(val);
const hasNumbers = /[0-9]/.test(val);
const hasNonAlphas = /[\W_]/.test(val);
const isValidLength = val.length >= 8;
return (
(hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasNonAlphas &&
isValidLength) ||
'Password must be at least 8 characters long and include uppercase, lowercase, number, and special character.'
);
};
const validatePasswordsMatch = (val: string) => {
return val === password.value || 'Passwords do not match.';
};
function isPasswordResetLink() {
const query = router.currentRoute.value.query;
return query && query.secret && query.userId && query.expire;
}
function validResetLink(): boolean { function validResetLink(): boolean {
const query = router.currentRoute.value.query; const query = router.currentRoute.value.query;
@@ -153,27 +95,34 @@ function validResetLink(): boolean {
); );
} }
function isPasswordResetLink() {
const query = router.currentRoute.value.query;
return query && query.secret && query.userId && query.expire;
}
function submitNewPw() { function submitNewPw() {
const query = router.currentRoute.value.query; const query = router.currentRoute.value.query;
if ( if (newPassword.value) {
validatePasswordStrength(password.value) === true &&
validatePasswordsMatch(confirmPassword.value) === true
) {
account account
.updateRecovery( .updateRecovery(
query.userId as string, query.userId as string,
query.secret as string, query.secret as string,
password.value newPassword.value
) )
.then(() => { .then(() => {
Dialog.create({ message: 'Password Changed!' }); Dialog.create({ message: 'Password Changed!' }).onOk(() =>
router.replace('/login'); router.replace('/login')
);
}) })
.catch((e) => .catch((e) =>
Dialog.create({ Dialog.create({
message: 'Password change failed! Error: ' + e.message, message: 'Password change failed! Error: ' + e.message,
}) })
); );
} else {
Dialog.create({
message: 'Invalid password. Try again',
});
} }
} }

87
src/pages/SignupPage.vue Normal file
View File

@@ -0,0 +1,87 @@
<template>
<q-layout>
<q-page-container>
<q-page class="flex bg-image flex-center">
<q-card
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }">
<q-card-section>
<q-img
fit="scale-down"
src="~assets/oysqn_logo.png" />
</q-card-section>
<q-card-section>
<div class="text-center q-pt-sm">
<div class="col text-h6">Sign Up</div>
</div>
</q-card-section>
<q-form>
<q-card-section class="q-gutter-md">
<q-input
v-model="email"
label="E-Mail"
type="email"
color="darkblue"
:rules="['email']"
filled></q-input>
<NewPasswordComponent v-model="password" />
<q-card-actions>
<q-space />
<q-btn
type="button"
@click="doRegister"
label="Sign Up"
color="primary"></q-btn>
</q-card-actions>
</q-card-section>
</q-form>
</q-card>
</q-page>
</q-page-container>
</q-layout>
</template>
<style>
.bg-image {
background-image: url('/src/assets/oys_lighthouse.jpg');
background-repeat: no-repeat;
background-position-x: center;
background-size: cover;
/* background-image: linear-gradient(
135deg,
#ed232a 0%,
#ffffff 75%,
#14539a 100%
); */
}
</style>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from 'src/stores/auth';
import NewPasswordComponent from 'src/components/NewPasswordComponent.vue';
import { Dialog } from 'quasar';
import { useRouter } from 'vue-router';
import { APP_VERSION } from 'src/version.js';
const email = ref('');
const password = ref('');
const router = useRouter();
console.log('version:' + APP_VERSION);
const doRegister = async () => {
if (email.value && password.value) {
try {
await useAuthStore().register(email.value, password.value);
Dialog.create({
message: 'Account Created! Now log-in with your e-mail / password.',
}).onOk(() => router.replace('/login'));
} catch (e) {
console.log(e);
Dialog.create({
message: 'An error occurred. Please ask for support in Discord',
});
}
}
};
</script>

View File

@@ -1,119 +1,128 @@
<template> <template>
<q-page padding> <q-layout>
<h1>Website Terms and Conditions of Use</h1> <q-page-container>
<q-page padding>
<h1>Website Terms and Conditions of Use</h1>
<h2>1. Terms</h2> <h2>1. Terms</h2>
<p> <p>
By accessing this Website, accessible from https://bab.toal.ca, you are By accessing this Website, accessible from https://undock.ca, you are
agreeing to be bound by these Website Terms and Conditions of Use and agreeing to be bound by these Website Terms and Conditions of Use and
agree that you are responsible for the agreement with any applicable local agree that you are responsible for the agreement with any applicable
laws. If you disagree with any of these terms, you are prohibited from local laws. If you disagree with any of these terms, you are
accessing this site. The materials contained in this Website are protected prohibited from accessing this site. The materials contained in this
by copyright and trade mark law. Website are protected by copyright and trade mark law.
</p> </p>
<h2>2. Use License</h2> <h2>2. Use License</h2>
<p> <p>
Permission is granted to temporarily download one copy of the materials on Permission is granted to temporarily download one copy of the
bab.toal.ca's Website for personal, non-commercial transitory viewing materials on undock.ca's Website for personal, non-commercial
only. This is the grant of a license, not a transfer of title, and under transitory viewing only. This is the grant of a license, not a
this license you may not: transfer of title, and under this license you may not:
</p> </p>
<ul> <ul>
<li>modify or copy the materials;</li> <li>modify or copy the materials;</li>
<li> <li>
use the materials for any commercial purpose or for any public display; use the materials for any commercial purpose or for any public
</li> display;
<li> </li>
attempt to reverse engineer any software contained on bab.toal.ca's <li>
Website; attempt to reverse engineer any software contained on undock.ca's
</li> Website;
<li> </li>
remove any copyright or other proprietary notations from the materials; <li>
or remove any copyright or other proprietary notations from the
</li> materials; or
<li> </li>
transferring the materials to another person or "mirror" the materials <li>
on any other server. transferring the materials to another person or "mirror" the
</li> materials on any other server.
</ul> </li>
</ul>
<p> <p>
This will let bab.toal.ca to terminate upon violations of any of these This will let undock.ca to terminate upon violations of any of these
restrictions. Upon termination, your viewing right will also be terminated restrictions. Upon termination, your viewing right will also be
and you should destroy any downloaded materials in your possession whether terminated and you should destroy any downloaded materials in your
it is printed or electronic format. These Terms of Service has been possession whether it is printed or electronic format. These Terms of
created with the help of the Service has been created with the help of the
<a href="https://www.termsofservicegenerator.net" <a href="https://www.termsofservicegenerator.net">
>Terms Of Service Generator</a Terms Of Service Generator
>. </a>
</p> .
</p>
<h2>3. Disclaimer</h2> <h2>3. Disclaimer</h2>
<p> <p>
All the materials on bab.toal.ca's Website are provided "as is". All the materials on undock.ca's Website are provided "as is".
bab.toal.ca makes no warranties, may it be expressed or implied, therefore undock.ca makes no warranties, may it be expressed or implied,
negates all other warranties. Furthermore, bab.toal.ca does not make any therefore negates all other warranties. Furthermore, undock.ca does
representations concerning the accuracy or reliability of the use of the not make any representations concerning the accuracy or reliability of
materials on its Website or otherwise relating to such materials or any the use of the materials on its Website or otherwise relating to such
sites linked to this Website. materials or any sites linked to this Website.
</p> </p>
<h2>4. Limitations</h2> <h2>4. Limitations</h2>
<p> <p>
bab.toal.ca or its suppliers will not be hold accountable for any damages undock.ca or its suppliers will not be hold accountable for any
that will arise with the use or inability to use the materials on damages that will arise with the use or inability to use the materials
bab.toal.ca's Website, even if bab.toal.ca or an authorize representative on undock.ca's Website, even if bab.toal.ca or an authorize
of this Website has been notified, orally or written, of the possibility representative of this Website has been notified, orally or written,
of such damage. Some jurisdiction does not allow limitations on implied of the possibility of such damage. Some jurisdiction does not allow
warranties or limitations of liability for incidental damages, these limitations on implied warranties or limitations of liability for
limitations may not apply to you. incidental damages, these limitations may not apply to you.
</p> </p>
<h2>5. Revisions and Errata</h2> <h2>5. Revisions and Errata</h2>
<p> <p>
The materials appearing on bab.toal.ca's Website may include technical, The materials appearing on undock.ca's Website may include technical,
typographical, or photographic errors. bab.toal.ca will not promise that typographical, or photographic errors. undock.ca will not promise that
any of the materials in this Website are accurate, complete, or current. any of the materials in this Website are accurate, complete, or
bab.toal.ca may change the materials contained on its Website at any time current. undock.ca may change the materials contained on its Website
without notice. bab.toal.ca does not make any commitment to update the at any time without notice. undock.ca does not make any commitment to
materials. update the materials.
</p> </p>
<h2>6. Links</h2> <h2>6. Links</h2>
<p> <p>
bab.toal.ca has not reviewed all of the sites linked to its Website and is undock.ca has not reviewed all of the sites linked to its Website and
not responsible for the contents of any such linked site. The presence of is not responsible for the contents of any such linked site. The
any link does not imply endorsement by bab.toal.ca of the site. The use of presence of any link does not imply endorsement by undock.ca of the
any linked website is at the user's own risk. site. The use of any linked website is at the user's own risk.
</p> </p>
<h2>7. Site Terms of Use Modifications</h2> <h2>7. Site Terms of Use Modifications</h2>
<p> <p>
bab.toal.ca may revise these Terms of Use for its Website at any time undock.ca may revise these Terms of Use for its Website at any time
without prior notice. By using this Website, you are agreeing to be bound without prior notice. By using this Website, you are agreeing to be
by the current version of these Terms and Conditions of Use. bound by the current version of these Terms and Conditions of Use.
</p> </p>
<h2>8. Your Privacy</h2> <h2>8. Your Privacy</h2>
<p>Please read our Privacy Policy.</p> <p>
Please read our
<a href="/privacy-policy">Privacy Policy.</a>
</p>
<h2>9. Governing Law</h2> <h2>9. Governing Law</h2>
<p> <p>
Any claim related to bab.toal.ca's Website shall be governed by the laws Any claim related to undock.ca's Website shall be governed by the laws
of ca without regards to its conflict of law provisions. of ca without regards to its conflict of law provisions.
</p> </p>
</q-page> </q-page>
</q-page-container>
</q-layout>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts"></script>

View File

@@ -92,7 +92,7 @@ const currentUser = useAuthStore().currentUser;
const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => { const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => {
return getAvailableIntervals(timestamp, boat) return getAvailableIntervals(timestamp, boat)
.concat(boatReservationEvents(timestamp, boat)) .value.concat(boatReservationEvents(timestamp, boat))
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start)); .sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
}; };
// Method declarations // Method declarations
@@ -134,16 +134,16 @@ const createReservationFromInterval = (interval: Interval | Reservation) => {
function handleSwipe({ ...event }) { function handleSwipe({ ...event }) {
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next(); event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
} }
function boatReservationEvents( const boatReservationEvents = (
timestamp: Timestamp, timestamp: Timestamp,
resource: Boat | undefined resource: Boat | undefined
) { ): Reservation[] => {
if (!resource) return []; if (!resource) return [] as Reservation[];
return reservationStore.getReservationsByDate( return reservationStore.getReservationsByDate(
getDate(timestamp), getDate(timestamp),
(resource as Boat).$id (resource as Boat).$id
); ).value;
} };
function onToday() { function onToday() {
calendar.value.moveToToday(); calendar.value.moveToToday();
} }

View File

@@ -22,7 +22,7 @@
class="q-pa-none"> class="q-pa-none">
<q-card <q-card
clas="q-ma-md" clas="q-ma-md"
v-if="!futureUserReservations.length"> v-if="!reservationStore.futureUserReservations.length">
<q-card-section> <q-card-section>
<div class="text-h6">You don't have any upcoming bookings!</div> <div class="text-h6">You don't have any upcoming bookings!</div>
<div class="text-h8">Why don't you go make one?</div> <div class="text-h8">Why don't you go make one?</div>
@@ -41,7 +41,7 @@
</q-card> </q-card>
<div v-else> <div v-else>
<div <div
v-for="reservation in futureUserReservations" v-for="reservation in reservationStore.futureUserReservations"
:key="reservation.$id"> :key="reservation.$id">
<ReservationCardComponent :modelValue="reservation" /> <ReservationCardComponent :modelValue="reservation" />
</div> </div>
@@ -51,7 +51,7 @@
name="past" name="past"
class="q-pa-none"> class="q-pa-none">
<div <div
v-for="reservation in pastUserReservations" v-for="reservation in reservationStore.pastUserReservations"
:key="reservation.$id"> :key="reservation.$id">
<ReservationCardComponent :modelValue="reservation" /> <ReservationCardComponent :modelValue="reservation" />
</div> </div>
@@ -63,7 +63,7 @@ import { useReservationStore } from 'src/stores/reservation';
import ReservationCardComponent from 'src/components/scheduling/ReservationCardComponent.vue'; import ReservationCardComponent from 'src/components/scheduling/ReservationCardComponent.vue';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
const { futureUserReservations, pastUserReservations } = useReservationStore(); const reservationStore = useReservationStore();
onMounted(() => useReservationStore().fetchUserReservations()); onMounted(() => useReservationStore().fetchUserReservations());

View File

@@ -26,7 +26,9 @@
cell-width="150px"> cell-width="150px">
<template #day="{ scope }"> <template #day="{ scope }">
<div <div
v-if="filteredIntervals(scope.timestamp, scope.resource).length" v-if="
filteredIntervals(scope.timestamp, scope.resource).value.length
"
style=" style="
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -35,10 +37,8 @@
font-size: 12px; font-size: 12px;
"> ">
<template <template
v-for="block in sortedIntervals( v-for="block in sortedIntervals(scope.timestamp, scope.resource)
scope.timestamp, .value"
scope.resource
)"
:key="block.id"> :key="block.id">
<q-chip class="cursor-pointer"> <q-chip class="cursor-pointer">
{{ date.formatDate(block.start, 'HH:mm') }} - {{ date.formatDate(block.start, 'HH:mm') }} -
@@ -163,7 +163,7 @@ import {
} from '@quasar/quasar-ui-qcalendar'; } from '@quasar/quasar-ui-qcalendar';
import { Boat, useBoatStore } from 'src/stores/boat'; import { Boat, useBoatStore } from 'src/stores/boat';
import { useIntervalStore } from 'src/stores/interval'; import { useIntervalStore } from 'src/stores/interval';
import { onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import type { import type {
Interval, Interval,
IntervalTemplate, IntervalTemplate,
@@ -208,8 +208,10 @@ const filteredIntervals = (date: Timestamp, boat: Boat) => {
}; };
const sortedIntervals = (date: Timestamp, boat: Boat) => { const sortedIntervals = (date: Timestamp, boat: Boat) => {
return filteredIntervals(date, boat).sort( return computed(() =>
(a, b) => Date.parse(a.start) - Date.parse(b.start) filteredIntervals(date, boat).value.sort(
(a, b) => Date.parse(a.start) - Date.parse(b.start)
)
); );
}; };
@@ -293,7 +295,7 @@ function onDrop(
overlapped.value = boatsToApply overlapped.value = boatsToApply
.map((boat) => .map((boat) =>
intervalsOverlapped( intervalsOverlapped(
existingIntervals.concat( existingIntervals.value.concat(
intervalsFromTemplate(boat, templateId, date) intervalsFromTemplate(boat, templateId, date)
) )
) )

View File

@@ -1,163 +1,173 @@
<template> <template>
<q-page padding> <q-layout>
<h1>Privacy Policy for bab.toal.ca</h1> <q-page-container>
<q-page padding>
<h1>Privacy Policy for Undock</h1>
<p> <p>
At OYS BAB Test, accessible from https://bab.toal.ca, one of our main At Undock, accessible from https://Undock, one of our main priorities
priorities is the privacy of our visitors. This Privacy Policy document is the privacy of our visitors. This Privacy Policy document contains
contains types of information that is collected and recorded by OYS BAB types of information that is collected and recorded by OYS BAB Test
Test and how we use it. and how we use it.
</p> </p>
<p> <p>
If you have additional questions or require more information about our If you have additional questions or require more information about our
Privacy Policy, do not hesitate to contact us. Our Privacy Policy was Privacy Policy, do not hesitate to contact us. Our Privacy Policy was
generated with the help of generated with the help of
<a href="https://www.gdprprivacypolicy.net/" <a href="https://www.gdprprivacypolicy.net/">
>GDPR Privacy Policy Generator</a GDPR Privacy Policy Generator
> </a>
</p> </p>
<h2>General Data Protection Regulation (GDPR)</h2> <h2>General Data Protection Regulation (GDPR)</h2>
<p>We are a Data Controller of your information.</p> <p>We are a Data Controller of your information.</p>
<p> <p>
bab.toal.ca legal basis for collecting and using the personal information Undock legal basis for collecting and using the personal information
described in this Privacy Policy depends on the Personal Information we described in this Privacy Policy depends on the Personal Information
collect and the specific context in which we collect the information: we collect and the specific context in which we collect the
</p> information:
<ul> </p>
<li>bab.toal.ca needs to perform a contract with you</li> <ul>
<li>You have given bab.toal.ca permission to do so</li> <li>Undock needs to perform a contract with you</li>
<li> <li>You have given Undock permission to do so</li>
Processing your personal information is in bab.toal.ca legitimate <li>
interests Processing your personal information is in Undock legitimate
</li> interests
<li>bab.toal.ca needs to comply with the law</li> </li>
</ul> <li>Undock needs to comply with the law</li>
</ul>
<p> <p>
bab.toal.ca will retain your personal information only for as long as is Undock will retain your personal information only for as long as is
necessary for the purposes set out in this Privacy Policy. We will retain necessary for the purposes set out in this Privacy Policy. We will
and use your information to the extent necessary to comply with our legal retain and use your information to the extent necessary to comply with
obligations, resolve disputes, and enforce our policies. our legal obligations, resolve disputes, and enforce our policies.
</p> </p>
<p> <p>
If you are a resident of the European Economic Area (EEA), you have If you are a resident of the European Economic Area (EEA), you have
certain data protection rights. If you wish to be informed what Personal certain data protection rights. If you wish to be informed what
Information we hold about you and if you want it to be removed from our Personal Information we hold about you and if you want it to be
systems, please contact us. removed from our systems, please contact us.
</p> </p>
<p> <p>
In certain circumstances, you have the following data protection rights: In certain circumstances, you have the following data protection
</p> rights:
<ul> </p>
<li> <ul>
The right to access, update or to delete the information we have on you. <li>
</li> The right to access, update or to delete the information we have on
<li>The right of rectification.</li> you.
<li>The right to object.</li> </li>
<li>The right of restriction.</li> <li>The right of rectification.</li>
<li>The right to data portability</li> <li>The right to object.</li>
<li>The right to withdraw consent</li> <li>The right of restriction.</li>
</ul> <li>The right to data portability</li>
<li>The right to withdraw consent</li>
</ul>
<h2>Log Files</h2> <h2>Log Files</h2>
<p> <p>
OYS BAB Test follows a standard procedure of using log files. These files Undock follows a standard procedure of using log files. These files
log visitors when they visit websites. All hosting companies do this and a log visitors when they visit websites. All hosting companies do this
part of hosting services' analytics. The information collected by log and a part of hosting services' analytics. The information collected
files include internet protocol (IP) addresses, browser type, Internet by log files include internet protocol (IP) addresses, browser type,
Service Provider (ISP), date and time stamp, referring/exit pages, and Internet Service Provider (ISP), date and time stamp, referring/exit
possibly the number of clicks. These are not linked to any information pages, and possibly the number of clicks. These are not linked to any
that is personally identifiable. The purpose of the information is for information that is personally identifiable. The purpose of the
analyzing trends, administering the site, tracking users' movement on the information is for analyzing trends, administering the site, tracking
website, and gathering demographic information. users' movement on the website, and gathering demographic information.
</p> </p>
<h2>Cookies and Web Beacons</h2> <h2>Cookies and Web Beacons</h2>
<p> <p>
Like any other website, OYS BAB Test uses "cookies". These cookies are Like any other website, Undock uses "cookies". These cookies are used
used to store information including visitors' preferences, and the pages to store information including visitors' preferences, and the pages on
on the website that the visitor accessed or visited. The information is the website that the visitor accessed or visited. The information is
used to optimize the users' experience by customizing our web page content used to optimize the users' experience by customizing our web page
based on visitors' browser type and/or other information. content based on visitors' browser type and/or other information.
</p> </p>
<h2>Privacy Policies</h2> <h2>Privacy Policies</h2>
<P <p>
>You may consult this list to find the Privacy Policy for each of the You may consult this list to find the Privacy Policy for each of the
advertising partners of OYS BAB Test.</P advertising partners of Undock.
> </p>
<p> <p>
Third-party ad servers or ad networks uses technologies like cookies, Third-party ad servers or ad networks uses technologies like cookies,
JavaScript, or Web Beacons that are used in their respective JavaScript, or Web Beacons that are used in their respective
advertisements and links that appear on OYS BAB Test, which are sent advertisements and links that appear on Undock, which are sent
directly to users' browser. They automatically receive your IP address directly to users' browser. They automatically receive your IP address
when this occurs. These technologies are used to measure the effectiveness when this occurs. These technologies are used to measure the
of their advertising campaigns and/or to personalize the advertising effectiveness of their advertising campaigns and/or to personalize the
content that you see on websites that you visit. advertising content that you see on websites that you visit.
</p> </p>
<p> <p>
Note that OYS BAB Test has no access to or control over these cookies that Note that Undock has no access to or control over these cookies that
are used by third-party advertisers. are used by third-party advertisers.
</p> </p>
<h2>Third Party Privacy Policies</h2> <h2>Third Party Privacy Policies</h2>
<p> <p>
OYS BAB Test's Privacy Policy does not apply to other advertisers or Undock's Privacy Policy does not apply to other advertisers or
websites. Thus, we are advising you to consult the respective Privacy websites. Thus, we are advising you to consult the respective Privacy
Policies of these third-party ad servers for more detailed information. It Policies of these third-party ad servers for more detailed
may include their practices and instructions about how to opt-out of information. It may include their practices and instructions about how
certain options. to opt-out of certain options.
</p> </p>
<p> <p>
You can choose to disable cookies through your individual browser options. You can choose to disable cookies through your individual browser
To know more detailed information about cookie management with specific options. To know more detailed information about cookie management
web browsers, it can be found at the browsers' respective websites. with specific web browsers, it can be found at the browsers'
</p> respective websites.
</p>
<h2>Children's Information</h2> <h2>Children's Information</h2>
<p> <p>
Another part of our priority is adding protection for children while using Another part of our priority is adding protection for children while
the internet. We encourage parents and guardians to observe, participate using the internet. We encourage parents and guardians to observe,
in, and/or monitor and guide their online activity. participate in, and/or monitor and guide their online activity.
</p> </p>
<p> <p>
OYS BAB Test does not knowingly collect any Personal Identifiable Undock does not knowingly collect any Personal Identifiable
Information from children under the age of 13. If you think that your Information from children under the age of 13. If you think that your
child provided this kind of information on our website, we strongly child provided this kind of information on our website, we strongly
encourage you to contact us immediately and we will do our best efforts to encourage you to contact us immediately and we will do our best
promptly remove such information from our records. efforts to promptly remove such information from our records.
</p> </p>
<h2>Online Privacy Policy Only</h2> <h2>Online Privacy Policy Only</h2>
<p> <p>
Our Privacy Policy applies only to our online activities and is valid for Our Privacy Policy applies only to our online activities and is valid
visitors to our website with regards to the information that they shared for visitors to our website with regards to the information that they
and/or collect in OYS BAB Test. This policy is not applicable to any shared and/or collect in Undock. This policy is not applicable to any
information collected offline or via channels other than this website. information collected offline or via channels other than this website.
</p> </p>
<h2>Consent</h2> <h2>Consent</h2>
<p> <p>
By using our website, you hereby consent to our Privacy Policy and agree By using our website, you hereby consent to our Privacy Policy and
to its terms. agree to its terms.
</p> </p>
</q-page> </q-page>
</q-page-container>
</q-layout>
</template> </template>
<script setup lang="ts"></script> <script
setup
lang="ts"></script>

View File

@@ -49,6 +49,10 @@ export default route(function (/* { store, ssrContext } */) {
return next('/login'); return next('/login');
} }
if (to.name === 'login' && currentUser) {
return next('/');
}
if (requiredRoles) { if (requiredRoles) {
if (!currentUser) { if (!currentUser) {
return next('/login'); return next('/login');

View File

@@ -24,7 +24,7 @@ export const links = <Link[]>[
to: '/profile', to: '/profile',
icon: 'account_circle', icon: 'account_circle',
front_links: false, front_links: false,
enabled: false, enabled: true,
}, },
{ {
name: 'Boats', name: 'Boats',

View File

@@ -168,14 +168,14 @@ const routes: RouteRecordRaw[] = [
publicRoute: true, publicRoute: true,
}, },
}, },
// { {
// path: '/register', path: '/signup',
// component: () => import('pages/RegisterPage.vue'), component: () => import('pages/SignupPage.vue'),
// name: 'register' name: 'signup',
// meta: { meta: {
// accountRoute: true, publicRoute: true,
// } },
// }, },
// Always leave this as last one, // Always leave this as last one,
// but you can also remove it // but you can also remove it
{ {

View File

@@ -2,6 +2,8 @@ import { defineStore } from 'pinia';
import { ID, account, functions, teams } from 'boot/appwrite'; import { ID, account, functions, teams } from 'boot/appwrite';
import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite'; import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useBoatStore } from './boat';
import { useReservationStore } from './reservation';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const currentUser = ref<Models.User<Models.Preferences> | null>(null); const currentUser = ref<Models.User<Models.Preferences> | null>(null);
@@ -14,6 +16,8 @@ export const useAuthStore = defineStore('auth', () => {
try { try {
currentUser.value = await account.get(); currentUser.value = await account.get();
currentUserTeams.value = await teams.list(); currentUserTeams.value = await teams.list();
await useBoatStore().fetchBoats();
await useReservationStore().fetchUserReservations();
} catch { } catch {
currentUser.value = null; currentUser.value = null;
currentUserTeams.value = null; currentUserTeams.value = null;
@@ -41,13 +45,31 @@ export const useAuthStore = defineStore('auth', () => {
await init(); await init();
} }
async function createTokenSession(email: string) {
return await account.createEmailToken(ID.unique(), email);
}
async function googleLogin() { async function googleLogin() {
account.createOAuth2Session( await account.createOAuth2Session(
OAuthProvider.Google, OAuthProvider.Google,
'https://bab.toal.ca/', 'https://oys.undock.ca',
'https://bab.toal.ca/#/login' 'https://oys.undock.ca/login'
); );
currentUser.value = await account.get(); await init();
}
async function discordLogin() {
await account.createOAuth2Session(
OAuthProvider.Discord,
'https://oys.undock.ca',
'https://oys.undock.ca/login'
);
await init();
}
async function tokenLogin(userId: string, token: string) {
await account.createSession(userId, token);
await init();
} }
function getUserNameById(id: string | undefined | null): string { function getUserNameById(id: string | undefined | null): string {
@@ -81,13 +103,22 @@ export const useAuthStore = defineStore('auth', () => {
return account.deleteSession('current').then((currentUser.value = null)); return account.deleteSession('current').then((currentUser.value = null));
} }
async function updateName(name: string) {
await account.updateName(name);
currentUser.value = await account.get();
}
return { return {
currentUser, currentUser,
getUserNameById, getUserNameById,
hasRequiredRole, hasRequiredRole,
register, register,
updateName,
login, login,
googleLogin, googleLogin,
discordLogin,
createTokenSession,
tokenLogin,
logout, logout,
init, init,
}; };

View File

@@ -2,25 +2,44 @@ import { defineStore } from 'pinia';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { Boat } from './boat'; import { Boat } from './boat';
import { Timestamp, today } from '@quasar/quasar-ui-qcalendar'; import { Timestamp, today } from '@quasar/quasar-ui-qcalendar';
import { Interval } from './schedule.types';
import { Interval, IntervalRecord } from './schedule.types';
import { AppwriteIds, databases } from 'src/boot/appwrite'; import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Query } from 'appwrite'; import { ID, Query } from 'appwrite';
import { useReservationStore } from './reservation'; import { useReservationStore } from './reservation';
import { LoadingTypes } from 'src/utils/misc';
import { useRealtimeStore } from './realtime';
export const useIntervalStore = defineStore('interval', () => { export const useIntervalStore = defineStore('interval', () => {
// TODO: Implement functions to dynamically pull this data. const intervals = ref(new Map<string, Interval>()); // Intervals by DocID
const intervals = ref<Map<string, Interval>>(new Map()); const dateStatus = ref(new Map<string, LoadingTypes>()); // State of load by date
const intervalDates = ref<IntervalRecord>({});
const reservationStore = useReservationStore();
const selectedDate = ref<string>(today()); const selectedDate = ref<string>(today());
const getIntervals = (date: Timestamp | string, boat?: Boat): Interval[] => { const reservationStore = useReservationStore();
const realtimeStore = useRealtimeStore();
realtimeStore.register(
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.interval}.documents`,
(response) => {
const payload = response.payload as Interval;
if (!payload.$id) return;
if (
response.events.includes('databases.*.collections.*.documents.*.delete')
) {
intervals.value.delete(payload.$id);
} else {
intervals.value.set(payload.$id, payload);
}
}
);
const getIntervals = (date: Timestamp | string, boat?: Boat) => {
const searchDate = typeof date === 'string' ? date : date.date; const searchDate = typeof date === 'string' ? date : date.date;
const dayStart = new Date(searchDate + 'T00:00'); const dayStart = new Date(searchDate + 'T00:00');
const dayEnd = new Date(searchDate + 'T23:59'); const dayEnd = new Date(searchDate + 'T23:59');
if (!intervalDates.value[searchDate]) { if (dateStatus.value.get(searchDate) === undefined) {
intervalDates.value[searchDate] = 'pending'; dateStatus.value.set(searchDate, 'pending');
fetchIntervals(searchDate); fetchIntervals(searchDate);
} }
return computed(() => { return computed(() => {
@@ -32,22 +51,19 @@ export const useIntervalStore = defineStore('interval', () => {
const matchesBoat = boat ? boat.$id === interval.resource : true; const matchesBoat = boat ? boat.$id === interval.resource : true;
return isWithinDay && matchesBoat; return isWithinDay && matchesBoat;
}); });
}).value; });
}; };
const getAvailableIntervals = ( const getAvailableIntervals = (date: Timestamp | string, boat?: Boat) => {
date: Timestamp | string, return computed(() =>
boat?: Boat getIntervals(date, boat).value.filter((interval) => {
): Interval[] => {
return computed(() => {
return getIntervals(date, boat).filter((interval) => {
return !reservationStore.isResourceTimeOverlapped( return !reservationStore.isResourceTimeOverlapped(
interval.resource, interval.resource,
new Date(interval.start), new Date(interval.start),
new Date(interval.end) new Date(interval.end)
); );
}); })
}).value; );
}; };
async function fetchInterval(id: string): Promise<Interval> { async function fetchInterval(id: string): Promise<Interval> {
@@ -78,11 +94,11 @@ export const useIntervalStore = defineStore('interval', () => {
response.documents.forEach((d) => response.documents.forEach((d) =>
intervals.value.set(d.$id, d as Interval) intervals.value.set(d.$id, d as Interval)
); );
intervalDates.value[dateString] = 'loaded'; dateStatus.value.set(dateString, 'loaded');
console.info(`Loaded ${response.documents.length} intervals from server`); console.info(`Loaded ${response.documents.length} intervals from server`);
} catch (error) { } catch (error) {
console.error('Failed to fetch intervals', error); console.error('Failed to fetch intervals', error);
intervalDates.value[dateString] = 'error'; dateStatus.value.set(dateString, 'error');
} }
} }
@@ -140,5 +156,6 @@ export const useIntervalStore = defineStore('interval', () => {
updateInterval, updateInterval,
deleteInterval, deleteInterval,
selectedDate, selectedDate,
intervals,
}; };
}); });

21
src/stores/realtime.ts Normal file
View File

@@ -0,0 +1,21 @@
import { defineStore } from 'pinia';
import { client } from 'src/boot/appwrite';
import { Interval } from './schedule.types';
import { ref } from 'vue';
import { RealtimeResponseEvent } from 'appwrite';
export const useRealtimeStore = defineStore('realtime', () => {
const subscriptions = ref<Map<string, () => void>>(new Map());
const register = (
channel: string,
fn: (response: RealtimeResponseEvent<Interval>) => void
) => {
if (subscriptions.value.has(channel)) return; // Already subscribed. But maybe different callback fn?
subscriptions.value.set(channel, client.subscribe(channel, fn));
};
return {
register,
};
});

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import type { Reservation } from './schedule.types'; import type { Reservation } from './schedule.types';
import { computed, ref, watch } from 'vue'; import { ComputedRef, computed, reactive } from 'vue';
import { AppwriteIds, databases } from 'src/boot/appwrite'; import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Query } from 'appwrite'; import { ID, Query } from 'appwrite';
import { date, useQuasar } from 'quasar'; import { date, useQuasar } from 'quasar';
@@ -8,15 +8,37 @@ import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
import { LoadingTypes } from 'src/utils/misc'; import { LoadingTypes } from 'src/utils/misc';
import { useAuthStore } from './auth'; import { useAuthStore } from './auth';
import { isPast } from 'src/utils/schedule'; import { isPast } from 'src/utils/schedule';
import { useRealtimeStore } from './realtime';
export const useReservationStore = defineStore('reservation', () => { export const useReservationStore = defineStore('reservation', () => {
const reservations = ref<Map<string, Reservation>>(new Map()); const reservations = reactive<Map<string, Reservation>>(new Map());
const datesLoaded = ref<Record<string, LoadingTypes>>({}); const datesLoaded = reactive<Record<string, LoadingTypes>>({});
const userReservations = ref<Map<string, Reservation>>(new Map()); const userReservations = reactive<Map<string, Reservation>>(new Map());
// TODO: Come up with a better way of storing reservations by date & reservations for user
const authStore = useAuthStore(); const authStore = useAuthStore();
const $q = useQuasar(); const $q = useQuasar();
const realtimeStore = useRealtimeStore();
realtimeStore.register(
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.reservation}.documents`,
(response) => {
const payload = response.payload as Reservation;
if (payload.$id) {
if (
response.events.includes(
'databases.*.collections.*.documents.*.delete'
)
) {
reservations.delete(payload.$id);
userReservations.delete(payload.$id);
} else {
reservations.set(payload.$id, payload);
if (payload.user === authStore.currentUser?.$id)
userReservations.set(payload.$id, payload);
}
}
}
);
// Fetch reservations for a specific date range // Fetch reservations for a specific date range
const fetchReservationsForDateRange = async ( const fetchReservationsForDateRange = async (
start: string = today(), start: string = today(),
@@ -40,7 +62,7 @@ export const useReservationStore = defineStore('reservation', () => {
); );
response.documents.forEach((d) => response.documents.forEach((d) =>
reservations.value.set(d.$id, d as Reservation) reservations.set(d.$id, d as Reservation)
); );
setDateLoaded(startDate, endDate, 'loaded'); setDateLoaded(startDate, endDate, 'loaded');
} catch (error) { } catch (error) {
@@ -81,8 +103,8 @@ export const useReservationStore = defineStore('reservation', () => {
reservation reservation
); );
} }
reservations.value.set(response.$id, response as Reservation); reservations.set(response.$id, response as Reservation);
userReservations.value.set(response.$id, response as Reservation); userReservations.set(response.$id, response as Reservation);
console.info('Reservation booked: ', response); console.info('Reservation booked: ', response);
return response as Reservation; return response as Reservation;
} catch (e) { } catch (e) {
@@ -95,14 +117,8 @@ export const useReservationStore = defineStore('reservation', () => {
reservation: string | Reservation | null | undefined reservation: string | Reservation | null | undefined
) => { ) => {
if (!reservation) return false; if (!reservation) return false;
let id; const id = typeof reservation === 'string' ? reservation : reservation.$id;
if (typeof reservation === 'string') { if (!id) return false;
id = reservation;
} else if ('$id' in reservation && typeof reservation.$id === 'string') {
id = reservation.$id;
} else {
return false;
}
const status = $q.notify({ const status = $q.notify({
color: 'secondary', color: 'secondary',
@@ -120,8 +136,8 @@ export const useReservationStore = defineStore('reservation', () => {
AppwriteIds.collection.reservation, AppwriteIds.collection.reservation,
id id
); );
reservations.value.delete(id); reservations.delete(id);
userReservations.value.delete(id); userReservations.delete(id);
console.info(`Deleted reservation: ${id}`); console.info(`Deleted reservation: ${id}`);
status({ status({
color: 'warning', color: 'warning',
@@ -146,7 +162,7 @@ export const useReservationStore = defineStore('reservation', () => {
if (start > end) return []; if (start > end) return [];
let curDate = start; let curDate = start;
while (curDate < end) { while (curDate < end) {
datesLoaded.value[(parseDate(curDate) as Timestamp).date] = state; datesLoaded[(parseDate(curDate) as Timestamp).date] = state;
curDate = date.addToDate(curDate, { days: 1 }); curDate = date.addToDate(curDate, { days: 1 });
} }
}; };
@@ -157,8 +173,7 @@ export const useReservationStore = defineStore('reservation', () => {
const unloaded = []; const unloaded = [];
while (curDate < end) { while (curDate < end) {
const parsedDate = (parseDate(curDate) as Timestamp).date; const parsedDate = (parseDate(curDate) as Timestamp).date;
if (datesLoaded.value[parsedDate] === undefined) if (datesLoaded[parsedDate] === undefined) unloaded.push(parsedDate);
unloaded.push(parsedDate);
curDate = date.addToDate(curDate, { days: 1 }); curDate = date.addToDate(curDate, { days: 1 });
} }
return unloaded; return unloaded;
@@ -168,15 +183,15 @@ export const useReservationStore = defineStore('reservation', () => {
const getReservationsByDate = ( const getReservationsByDate = (
searchDate: string, searchDate: string,
boat?: string boat?: string
): Reservation[] => { ): ComputedRef<Reservation[]> => {
if (!datesLoaded.value[searchDate]) { if (!datesLoaded[searchDate]) {
fetchReservationsForDateRange(searchDate); fetchReservationsForDateRange(searchDate);
} }
const dayStart = new Date(searchDate + 'T00:00'); const dayStart = new Date(searchDate + 'T00:00');
const dayEnd = new Date(searchDate + 'T23:59'); const dayEnd = new Date(searchDate + 'T23:59');
return computed(() => { return computed(() => {
return Array.from(reservations.value.values()).filter((reservation) => { return Array.from(reservations.values()).filter((reservation) => {
const reservationStart = new Date(reservation.start); const reservationStart = new Date(reservation.start);
const reservationEnd = new Date(reservation.end); const reservationEnd = new Date(reservation.end);
@@ -185,7 +200,7 @@ export const useReservationStore = defineStore('reservation', () => {
const matchesBoat = boat ? boat === reservation.resource : true; const matchesBoat = boat ? boat === reservation.resource : true;
return isWithinDay && matchesBoat; return isWithinDay && matchesBoat;
}); });
}).value; });
}; };
// Get conflicting reservations for a resource within a time range // Get conflicting reservations for a resource within a time range
@@ -194,7 +209,7 @@ export const useReservationStore = defineStore('reservation', () => {
start: Date, start: Date,
end: Date end: Date
): Reservation[] => { ): Reservation[] => {
return Array.from(reservations.value.values()).filter( return Array.from(reservations.values()).filter(
(entry) => (entry) =>
entry.resource === resource && entry.resource === resource &&
new Date(entry.start) < end && new Date(entry.start) < end &&
@@ -229,7 +244,7 @@ export const useReservationStore = defineStore('reservation', () => {
[Query.equal('user', authStore.currentUser.$id)] [Query.equal('user', authStore.currentUser.$id)]
); );
response.documents.forEach((d) => response.documents.forEach((d) =>
userReservations.value.set(d.$id, d as Reservation) userReservations.set(d.$id, d as Reservation)
); );
} catch (error) { } catch (error) {
console.error('Failed to fetch reservations for user: ', error); console.error('Failed to fetch reservations for user: ', error);
@@ -237,7 +252,7 @@ export const useReservationStore = defineStore('reservation', () => {
}; };
const sortedUserReservations = computed((): Reservation[] => const sortedUserReservations = computed((): Reservation[] =>
[...userReservations.value?.values()].sort( [...userReservations.values()].sort(
(a, b) => new Date(b.start).getTime() - new Date(a.start).getTime() (a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()
) )
); );
@@ -252,27 +267,6 @@ export const useReservationStore = defineStore('reservation', () => {
return sortedUserReservations.value?.filter((b) => isPast(b.end)); return sortedUserReservations.value?.filter((b) => isPast(b.end));
}); });
// Ensure reactivity for computed properties when Map is modified
watch(
reservations,
() => {
sortedUserReservations.value;
futureUserReservations.value;
pastUserReservations.value;
},
{ deep: true }
);
watch(
userReservations,
() => {
sortedUserReservations.value;
futureUserReservations.value;
pastUserReservations.value;
},
{ deep: true }
);
return { return {
getReservationsByDate, getReservationsByDate,
getReservationById, getReservationById,

View File

@@ -5,7 +5,7 @@ export const getSampleData = () => [
displayName: 'PX', displayName: 'PX',
class: 'J/27', class: 'J/27',
year: 1981, year: 1981,
imgSrc: '/tmpimg/j27.png', imgSrc: '/tmpimg/projectX.jpg',
iconSrc: '/tmpimg/projectx_avatar256.png', iconSrc: '/tmpimg/projectx_avatar256.png',
bookingAvailable: true, bookingAvailable: true,
maxPassengers: 8, maxPassengers: 8,

View File

@@ -1,5 +1,4 @@
import { Models } from 'appwrite'; import { Models } from 'appwrite';
import { LoadingTypes } from 'src/utils/misc';
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined; export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
export type Reservation = Interval & { export type Reservation = Interval & {
@@ -29,7 +28,3 @@ export type IntervalTemplate = Partial<Models.Document> & {
name: string; name: string;
timeTuples: TimeTuple[]; timeTuples: TimeTuple[];
}; };
export interface IntervalRecord {
[key: string]: LoadingTypes;
}

0
v1
View File

4577
yarn.lock

File diff suppressed because it is too large Load Diff