49 Commits

Author SHA1 Message Date
2874ea3be1 fix(ui): hidden components on hard reload
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 1m16s
2026-03-15 22:44:07 -04:00
26bc33a095 fix(ui): layout fixes 2026-03-15 22:12:35 -04:00
67c7a3c050 chore: Update dependencies to latest
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 1m17s
fix: claude fixes to various errors
2026-03-15 10:41:12 -04:00
5d08b1c927 fix: Update AppwriteIDs for new dev
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m33s
2026-03-15 07:53:55 -04:00
148b8ff49d fix: add devel branch to builds
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m24s
2026-03-14 23:28:36 -04:00
c4113f63a4 Merge branch 'alpha' into devel 2026-03-14 23:25:03 -04:00
6274e4936d fix: Broken tag
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m48s
2026-03-14 23:07:10 -04:00
e1259688a4 chore: Add Claude Fix some bugs.
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m40s
2026-03-14 22:50:00 -04:00
e2a4dd851d Test
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m2s
2025-11-17 22:47:05 -05:00
2a61cc105f Test 2024-10-03 11:50:15 -04:00
d6f58ddabd chore: add version files.
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 4m41s
fix(ui): small tweaks to layout.
2024-06-25 08:33:05 -04:00
a1d9e147f9 fix(ci): New release
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m26s
2024-06-24 11:17:24 -04:00
92bfc7bafa chore: force new release
Some checks failed
Build BAB Application Deployment Artifact / build (push) Has been cancelled
2024-06-24 11:16:06 -04:00
6f61edd659 chore: change tar directory
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m43s
2024-06-24 10:25:51 -04:00
ea4e848e57 fix: make build process work
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m4s
2024-06-24 09:48:24 -04:00
c08fa6c2d8 fix: correct semantic-version step
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 1m56s
2024-06-23 23:54:43 -04:00
01aae9e8ff fix: correct use of secret
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m0s
2024-06-23 23:50:52 -04:00
70c6837858 refactor: update build pipeline
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m0s
2024-06-23 23:46:22 -04:00
6167a713dd refactor: Try new github actions script
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 1m48s
2024-06-23 00:56:47 -04:00
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
80 changed files with 16190 additions and 6662 deletions

View File

@@ -0,0 +1,8 @@
Write a session handoff file for the current session.
Steps:
1. Read `templates/claude-templates.md` and find the Session Handoff template (Template 4). Use the Light Handoff if this is a small project (under 5 sessions), Full Handoff otherwise.
2. Fill in every field based on what was accomplished in this session. Be specific — include exact file paths for every output, exact numbers discovered, and conditional logic established.
3. Write the handoff to `./docs/summaries/handoff-[today's date]-[topic].md`.
4. If a previous handoff file exists in `./docs/summaries/`, move it to `./docs/archive/handoffs/`.
5. Tell me the file path of the new handoff and summarize what it contains.

View File

@@ -0,0 +1,13 @@
Process an input document into a structured source summary.
Steps:
1. Read `templates/claude-templates.md` and find the Source Document Summary template (Template 1). Use the Light Source Summary if this is a small project (under 5 sessions), Full Source Summary otherwise.
2. Read the document at: $ARGUMENTS
3. Extract all information into the template format. Pay special attention to:
- EXACT numbers — do not round or paraphrase
- Requirements in IF/THEN/BUT/EXCEPT format
- Decisions with rationale and rejected alternatives
- Open questions marked as OPEN, ASSUMED, or MISSING
4. Write the summary to `./docs/summaries/source-[filename].md`.
5. Move the original document to `./docs/archive/`.
6. Tell me: what was extracted, what's unclear, and what needs follow-up.

View File

@@ -0,0 +1,13 @@
Report on the current project state.
Steps:
1. Read `./docs/summaries/00-project-brief.md` for project context.
2. Find and read the latest `handoff-*.md` file in `./docs/summaries/` for current state.
3. List all files in `./docs/summaries/` to understand what's been processed.
4. Report:
- **Project:** name and type from the project brief
- **Current phase:** based on the project phase tracker
- **Last session:** what was accomplished (from the latest handoff)
- **Next steps:** what the next session should do (from the latest handoff)
- **Open questions:** anything unresolved
- **Summary file count:** how many files in docs/summaries/ (warn if approaching 15)

View File

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

View File

@@ -1,8 +0,0 @@
/dist
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.js
/src-ssr
/quasar.config.*.temporary.compiled*

View File

@@ -1,90 +0,0 @@
module.exports = {
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
root: true,
// https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
// Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
parserOptions: {
parser: require.resolve('@typescript-eslint/parser'),
extraFileExtensions: [ '.vue' ]
},
env: {
browser: true,
es2021: true,
node: true,
'vue/setup-compiler-macros': true
},
// Rules order is important, please avoid shuffling them
extends: [
// Base ESLint recommended rules
// 'eslint:recommended',
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
// ESLint typescript rules
'plugin:@typescript-eslint/recommended',
// Uncomment any of the lines below to choose desired strictness,
// but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules
'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
// https://github.com/prettier/eslint-config-prettier#installation
// usage with Prettier, provided by 'eslint-config-prettier'.
'prettier'
],
plugins: [
// required to apply rules which need type information
'@typescript-eslint',
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
// required to lint *.vue files
'vue'
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
// Prettier has not been included as plugin to avoid performance impact
// add it as an extension for your IDE
],
globals: {
ga: 'readonly', // Google Analytics
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly'
},
// add your custom rules here
rules: {
'prefer-promise-reject-errors': 'off',
quotes: ['warn', 'single', { avoidEscape: true }],
// this rule, if on, would require explicit return type on the `render` function
'@typescript-eslint/explicit-function-return-type': 'off',
// in plain CommonJS modules, you can't use `import foo = require('foo')` to pass this rule, so it has to be disabled
'@typescript-eslint/no-var-requires': 'off',
// The core 'no-unused-vars' rules (in the eslint:recommended ruleset)
// does not work with type definitions
'no-unused-vars': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}

View File

@@ -4,9 +4,13 @@ on:
push: push:
branches: branches:
- main - main
- alpha
- devel
jobs: jobs:
build: build:
env:
RUNNER_TOOL_CACHE: /toolcache
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
steps: steps:
- name: Checkout - name: Checkout
@@ -14,7 +18,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '20.x' node-version: "20.x"
- name: Install yarn - name: Install yarn
run: npm install --global yarn run: npm install --global yarn
- name: Install yarn dependencies - name: Install yarn dependencies
@@ -29,23 +33,18 @@ jobs:
- name: Show env file - name: Show env file
run: | run: |
/bin/cat .env.local /bin/cat .env.local
- name: Build Project - name: Build and Release
run: quasar build -m pwa id: build
- name: Get Version Number
id: get_version
run: echo "::set-output name=VERSION::$(node -p "require('./package.json').version")"
- name: Tarfile
run: | run: |
cd dist/pwa npx semantic-release
tar czf ../../build-${{ steps.get_version.outputs.VERSION }}.tar.gz . env:
- name: Upload Artifact GITEA_TOKEN: ${{ secrets.GT_TOKEN }}
uses: actions/upload-artifact@v3 GITEA_URL: ${{ vars.GT_URL }}
with:
name: build-artifact-${{ steps.get_version.outputs.VERSION }}.${{ gitea.run_number }}
path: build-${{ steps.get_version.outputs.VERSION }}.tar.gz
- name: Trigger Ansible Deploy Playbook - name: Trigger Ansible Deploy Playbook
uses: https://github.com/distributhor/workflow-webhook@v3 uses: https://github.com/distributhor/workflow-webhook@v3
with: with:
webhook_url: ${{ vars.WEBHOOK_URL }} webhook_url: ${{ vars.WEBHOOK_URL }}
webhook_auth_type: "bearer"
webhook_auth: "Token:${{ secrets.WEBHOOK_SECRET }}"
verbose: true verbose: true
data: '{ "artifact_url": "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id}}/artifacts/build-artifact-${{ steps.get_version.outputs.VERSION }}.${{ gitea.run_number }}" }' data: '{ "artifact_url": "${{ gitea.server_url }}/${{ gitea.repository }}/releases/download/v${{ steps.build.outputs.VERSION }}/release-${{ steps.build.outputs.VERSION }}.tar.gz" }'

17
.gitignore vendored
View File

@@ -21,6 +21,12 @@ node_modules
/src-bex/www /src-bex/www
/src-bex/js/core /src-bex/js/core
# Yarn 4
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
# Log files # Log files
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
@@ -34,4 +40,13 @@ yarn-error.log*
*.sln *.sln
# local .env files # local .env files
.env.local* .env*
# version file
src/version.ts
VERSION
release-*.gz
CHANGELOG.md
# Quasar cruft
/quasar.config.*.temporary.compiled*

27
.releaserc.json Normal file
View File

@@ -0,0 +1,27 @@
{
"branches": [
"main",
"next",
{ "name": "beta", "prerelease": true },
{ "name": "devel", "prerelease": true },
{ "name": "alpha", "prerelease": true }
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
[
"@semantic-release/exec",
{
"prepareCmd": "npm run generate-version '${nextRelease.version}' && quasar build -m pwa",
"publishCmd": "tar -czvf release-${nextRelease.version}.tar.gz -C dist/pwa . && echo '::set-output name=VERSION::${nextRelease.version}'"
}
],
[
"@saithodev/semantic-release-gitea",
{
"assets": ["release-${nextRelease.version}.tar.gz"]
}
]
]
}

940
.yarn/releases/yarn-4.13.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

3
.yarnrc.yml Normal file
View File

@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.13.0.cjs

51
CLAUDE.md Normal file
View File

@@ -0,0 +1,51 @@
# CLAUDE.md
## Session Start
Read the latest handoff in docs/summaries/ if one exists. Load only the files that handoff references — not all summaries. If no handoff exists, ask: what is the project, what type of work, what is the target deliverable.
Before starting work, state: what you understand the project state to be, what you plan to do this session, and any open questions.
## Identity
You work with Patrick, a Solutions Architect, on the OYS Borrow a Boat (bab-app) project — a Quasar/Vue 3 app for managing a Borrow a Boat program for a Yacht Club. Backend is Appwrite.
## Project Overview
- **App**: OYS Borrow a Boat (oys_bab)
- **Stack**: Quasar (Vue 3), TypeScript, Appwrite (BaaS)
- **Purpose**: Manage a Borrow a Boat program for a Yacht Club
- **Docs**: docs/planning/ contains personas, user/role/permission model, and time-based logic
## Rules
1. Do not mix unrelated project contexts in one session.
2. Write state to disk, not conversation. After completing meaningful work, write a summary to docs/summaries/ using templates from templates/claude-templates.md. Include: decisions with rationale, exact numbers, file paths, open items.
3. Before compaction or session end, write to disk: every number, every decision with rationale, every open question, every file path, exact next action.
4. When switching work types (research → writing → review), write a handoff to docs/summaries/handoff-[date]-[topic].md and suggest a new session.
5. Do not silently resolve open questions. Mark them OPEN or ASSUMED.
6. Do not bulk-read documents. Process one at a time: read, summarize to disk, release from context before reading next. For the detailed protocol, read docs/context/processing-protocol.md.
7. Sub-agent returns must be structured, not free-form prose. Use output contracts from templates/claude-templates.md.
## Where Things Live
- templates/claude-templates.md — summary, handoff, decision, analysis, task, output contract templates (read on demand)
- docs/summaries/ — active session state (latest handoff + project brief + decision records + source summaries)
- docs/context/ — reusable domain knowledge, loaded only when relevant to the current task
- processing-protocol.md — full document processing steps
- archive-rules.md — summary lifecycle and file archival rules
- subagent-rules.md — rules for structured sub-agent outputs
- docs/planning/ — original planning documents (personas, roles/permissions, time logic)
- docs/archive/ — processed raw files. Do not read unless explicitly told.
- output/deliverables/ — final outputs
- src/ — Quasar/Vue app source
- src-pwa/ — PWA config
- appwrite.json — Appwrite project config
## Error Recovery
If context degrades or auto-compact fires unexpectedly: write current state to docs/summaries/recovery-[date].md, tell the user what may have been lost, suggest a fresh session.
## Before Delivering Output
Verify: exact numbers preserved, open questions marked OPEN, output matches what was requested (not assumed), claims backed by specific data, output consistent with stored decisions in docs/context/, summary written to disk for this session's work.

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

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.0.0

View File

@@ -0,0 +1,88 @@
# Session Handoff: Auth Refactor — Magic Link & Cleanup
**Date:** 2026-03-15
**Session Duration:** ~1 hour
**Session Focus:** Remove Google/Discord OAuth, add magic link login, add About dialog
**Context Usage at Handoff:** ~30%
## What Was Accomplished
1. Analyzed full auth flow (store, boot, login page, router guard) → no output file, inline analysis
2. Removed Google OAuth → deleted `src/components/GoogleOauthComponent.vue`
3. Removed Discord OAuth → deleted `src/components/DiscordOauthComponent.vue`
4. Removed `googleLogin`, `discordLogin` from auth store → `src/stores/auth.ts`
5. Removed `OAuthProvider` import from auth store → `src/stores/auth.ts`
6. Added `createMagicURLSession()` to auth store (calls `account.createMagicURLToken`) → `src/stores/auth.ts`
7. Added `magicURLLogin()` to auth store (calls `account.updateMagicURLSession`) → `src/stores/auth.ts`
8. Updated `LoginPage.vue` — removed OAuth component imports/usage, added "Send Magic Link" button, added `onMounted` handler to detect magic link callback params and call `magicURLLogin``src/pages/LoginPage.vue`
9. Added "About" item to left drawer → `src/components/LeftDrawer.vue` — opens a Quasar Dialog with app name, version, description
10. Converted `src/version.js``src/version.ts` to eliminate TS hint
11. Updated `generate-version.js` to write to `src/version.ts``generate-version.js`
12. Fixed stale import in `SignupPage.vue` (`src/version.js``src/version`) → `src/pages/SignupPage.vue`
13. Fixed `LeftDrawer.vue` import path `boot/appwrite``src/boot/appwrite` (was a TS module resolution error)
## Exact State of Work in Progress
- `.env.local` not being picked up by `quasar dev`: user interrupted the fix (was about to add `require('dotenv').config(...)` to `quasar.config.js`). **Status: UNRESOLVED.** User stopped this change — may prefer a different approach or wants to investigate themselves.
## Decisions Made This Session
- Use `account.updateMagicURLSession(userId, secret)` (not `createSession`) for magic link completion — BECAUSE Appwrite SDK v14 uses a separate method for magic URL vs OTP token sessions. `createSession` is for OTP only.
- Magic link callback URL = `window.location.origin + '/login'` — Appwrite appends `?userId=xxx&secret=xxx` and the `onMounted` handler in LoginPage detects and consumes these.
- Keep OTP code flow alongside magic link — user did not ask to remove it; both are available.
- About dialog placed in LeftDrawer (not a separate page) — appropriate pattern for simple info display in a mobile app.
## Key Numbers Generated or Discovered This Session
- Appwrite SDK version: `^14.0.1`
- `@quasar/app-vite` version: `^1.9.1`
- App version string source: `src/version.ts`, written by `generate-version.js` (takes version as CLI arg)
- Dev server port: `4000` (set in `quasar.config.js` `devServer.strictport`)
## Conditional Logic Established
- IF magic link callback detected (`query.userId && query.secret` in route on LoginPage mount) THEN call `magicURLLogin(userId, secret)` BECAUSE this uses `updateMagicURLSession` which is the correct Appwrite v14 API.
- IF user enters email and clicks "Send Code" THEN OTP flow runs (6-digit code emailed, entered in second input field).
- IF user enters email and clicks "Send Magic Link" THEN magic link email sent; user clicks link; page reloads at `/login?userId=xxx&secret=xxx`; `onMounted` auto-completes login.
## Files Created or Modified
| File Path | Action | Description |
|-----------|--------|-------------|
| `src/stores/auth.ts` | Modified | Removed `googleLogin`, `discordLogin`, `OAuthProvider` import; added `createMagicURLSession`, `magicURLLogin` |
| `src/pages/LoginPage.vue` | Modified | Removed OAuth components; added magic link button and `onMounted` callback handler |
| `src/components/LeftDrawer.vue` | Modified | Added "About" menu item that opens info dialog with version; fixed boot import path |
| `src/components/GoogleOauthComponent.vue` | Deleted | No longer used |
| `src/components/DiscordOauthComponent.vue` | Deleted | No longer used |
| `src/version.ts` | Renamed (was `.js`) | Eliminates TypeScript implicit-any hint |
| `src/pages/SignupPage.vue` | Modified | Updated `version.js` import to `version` |
| `generate-version.js` | Modified | Now writes to `src/version.ts` instead of `src/version.js` |
## What the NEXT Session Should Do
1. **First**: Verify magic link flow end-to-end in dev environment (send link, click it, confirm auto-login works)
2. **Then**: Resolve `.env.local` not being picked up — options are: (a) add `require('dotenv').config({ path: '.env.local' })` to top of `quasar.config.js`, or (b) use `.env` instead of `.env.local`, or (c) investigate if `@quasar/app-vite` 1.9.x has a bug
3. **Then**: Check if `SignupPage.vue` / `register()` flow is still intended — it creates email+password accounts but the login page only offers passwordless flows; this is an inconsistency
## Open Questions Requiring User Input
- [ ] Should the OTP (6-digit code) flow be kept, or replaced entirely by magic link? — impacts LoginPage UX
- [ ] Should the SignupPage (email+password registration) be removed in favour of magic link only? — impacts `src/pages/SignupPage.vue`, `src/stores/auth.ts` `register()`, router `/signup` route
- [ ] Should "Forgot Password?" link be removed from LoginPage now that magic link is the primary flow? — it was already removed from LoginPage in this session (not present in current code)
- [ ] `.env.local` fix approach — user stopped the `dotenv` approach; confirm preferred method
## Assumptions That Need Validation
- ASSUMED: `window.location.origin` is correct for magic link callback URL in all deployment environments — validate that prod URL matches what Appwrite Console has whitelisted as a redirect domain
- ASSUMED: Appwrite project has magic URL tokens enabled — validate in Appwrite Console → Auth settings
## What NOT to Re-Read
- `src/components/GoogleOauthComponent.vue` — deleted
- `src/components/DiscordOauthComponent.vue` — deleted
## Files to Load Next Session
- `src/stores/auth.ts` — primary auth logic, fully refactored this session
- `src/pages/LoginPage.vue` — magic link + OTP UI, modified this session
- `src/boot/appwrite.ts` — contains duplicate `login()` function (email/password) that may be dead code post-refactor
- `quasar.config.js` — if resolving `.env.local` issue

View File

@@ -0,0 +1,114 @@
# Session Handoff: Dependency Updates & ESLint Cleanup
**Date:** 2026-03-15
**Session Focus:** Complete Quasar v1→v2 migration, Appwrite SDK v14→v23 update, ESLint flat config cleanup
**Status:** BUILD PASSING — 0 errors, 0 warnings
## What Was Accomplished
### From prior sessions (captured in archived handoffs):
1. Auth flow refactored to passwordless (magic link + OTP), OAuth removed
2. Google/Discord OAuth components deleted
3. About dialog with version info added → `src/components/LeftDrawer.vue`
4. `quasar.config.js``quasar.config.ts` (ESM TypeScript)
5. `"type": "module"` added to `package.json`
6. Yarn 1.x → Yarn 4.13.0
7. ESLint legacy `.eslintrc.cjs` → flat config `eslint.config.js`
8. QCalendar app extension removed → direct npm package import
9. Boot/router/store wrappers updated to `#q-app/wrappers` imports
10. Appwrite SDK updated v14.0.1 → v23.0.0
11. `globals` package installed; browser + ES2021 globals added to ESLint config
### This session (build cleanup — all 30 TS errors + 12 ESLint issues resolved):
**TypeScript `as unknown as` casts** (Appwrite v23 `DefaultDocument` no longer overlaps domain types):
- `src/stores/boat.ts:36``as unknown as Boat[]`
- `src/stores/interval.ts:95,113,127``as unknown as Interval`
- `src/stores/intervalTemplate.ts` — map callback cast + `as unknown as IntervalTemplate` (3 places)
- `src/stores/reservation.ts:65,80,247``as unknown as Reservation`
- `src/stores/task.ts:53,65,77,109,132``as unknown as Task[]`, `TaskTag[]`, `SkillTag[]`, `Task`
**`.id``.$id` fixes** (Appwrite uses `$id`, not `id`):
- `src/components/boat/BoatPreviewComponent.vue:7`
- `src/components/scheduling/boat/BoatScheduleTableComponent.vue:54`
- `src/components/task/TaskListComponent.vue:4`
- `src/pages/schedule/ManageCalendar.vue:40`
- `src/stores/sampledata/schedule.ts:19,29,138` — also `id:``$id:` in object literals
**`defineProps` import conflict** (auto-imported in `<script setup>`, cannot also be explicitly imported):
- `src/components/task/TaskCardComponent.vue:20` — removed import; also removed `subtasks` template refs (not in Task type)
- `src/components/task/TaskTableComponent.vue:215` — removed `defineProps` from import
**ESLint fixes:**
- `eslint.config.js:52``process.env.NODE_ENV === 'production' ? 'error' : 'off'``'off'` (process not defined in .js ESLint globals)
- `src/components/ResourceScheduleViewerComponent.vue:173` — removed `|| undefined` (always truthy)
- `src/components/ResourceScheduleViewerComponent.vue:177``catch (e)``catch { }` (unused binding)
- `src/components/ResourceScheduleViewerComponent.vue:237-247` — removed unused `eslint-disable-next-line` comments
- `src/components/scheduling/boat/BoatScheduleTableComponent.vue:116``NodeJS.Timeout``ReturnType<typeof setInterval>`
- `src/components/scheduling/boat/BoatScheduleTableComponent.vue:129` — ternary as statement → `if/else`; also destructured `{ direction }` to fix unused `event` hint
- `src/pages/LoginPage.vue:131``catch (e)``catch { }`
**Other fixes:**
- `src-pwa/register-service-worker.ts` — installed `register-service-worker` package (was missing from package.json)
- `src/stores/sampledata/schedule.ts:50``template.blocks``template.timeTuples` (property was renamed)
- `src/stores/sampledata/schedule.ts:145` — removed `reservationDate` (not in Reservation type)
- `src/stores/intervalTemplate.ts:27``d.timeTuple` cast via `as unknown as { timeTuple: string[] }`
- `src/stores/intervalTemplate.ts:82``response.timeTuple` cast via `as unknown as { timeTuple: string[] }`
- `src/components/scheduling/boat/CalendarHeaderComponent.vue` — removed `getWeekdaySkips` (removed from qcalendar API); `createDayList` now takes `weekdays` directly as 4th arg; removed `weekdaySkips` computed
## Decisions Made
- **`as unknown as Type` pattern** — correct approach for Appwrite v23 `DefaultDocument` casts; v23 made `DefaultDocument` strict, no longer assignable to domain types without double-cast — STATUS: CONFIRMED
- **`getWeekdaySkips` removed from qcalendar** — `createDayList` now accepts `weekdays` array directly as 4th param — STATUS: CONFIRMED (verified from ESM source)
- **`subtasks` removed from TaskCardComponent template** — `Task` type has no `subtasks` field; feature was dead code — STATUS: ASSUMED safe (see open question)
- **`no-debugger: 'off'`** — hardcoded instead of `process.env.NODE_ENV` conditional because `process` is not in ESLint globals for `.js` files — STATUS: CONFIRMED
## Key Numbers
- TS errors at session start: 30 (in 14 files)
- ESLint errors at session start: 12 (6 errors, 6 warnings)
- TS errors at session end: 0
- ESLint errors at session end: 0
- Packages added: `register-service-worker`
## Files Modified This Session
| File | Change |
|------|--------|
| `eslint.config.js` | `no-debugger` rule hardcoded to `'off'` |
| `src/components/ResourceScheduleViewerComponent.vue` | 3 ESLint fixes |
| `src/components/scheduling/boat/BoatScheduleTableComponent.vue` | `.id``.$id`, NodeJS.Timeout, if/else |
| `src/components/scheduling/boat/CalendarHeaderComponent.vue` | Removed `getWeekdaySkips`; updated `createDayList` call |
| `src/pages/LoginPage.vue` | `catch { }` |
| `src/components/boat/BoatPreviewComponent.vue` | `.id``.$id` |
| `src/components/task/TaskCardComponent.vue` | Removed `defineProps` import + subtasks refs |
| `src/components/task/TaskListComponent.vue` | `.id``.$id` |
| `src/components/task/TaskTableComponent.vue` | Removed `defineProps` from import |
| `src/pages/schedule/ManageCalendar.vue` | `.id``.$id` |
| `src/stores/boat.ts` | `as unknown as Boat[]` |
| `src/stores/interval.ts` | `as unknown as Interval` (3 places) |
| `src/stores/intervalTemplate.ts` | Multiple cast fixes + `timeTuple` access |
| `src/stores/reservation.ts` | `as unknown as Reservation` (multiple) |
| `src/stores/sampledata/schedule.ts` | `id``$id`, `blocks``timeTuples`, removed `reservationDate` |
| `src/stores/task.ts` | `as unknown as` for Task/TaskTag/SkillTag |
| `package.json` / `yarn.lock` | Added `register-service-worker` |
## Open Questions
- [ ] `src/components/task/TaskCardComponent.vue``subtasks` removed from template. Should `subtasks?: Task[]` be added to the `Task` interface in `task.ts` for future use? OPEN
- [ ] Appwrite v23 deprecated positional-param overloads (hints in every store). Should stores be migrated to new object-param style? Low priority — code still works. OPEN
## Assumptions
- ASSUMED: `subtasks` feature in TaskCardComponent was dead/future code — safe to remove template refs
- ASSUMED: `no-debugger: 'off'` is fine for devel branch
## What NOT to Re-Read
- `docs/archive/handoffs/handoff-2026-03-15-auth-magic-link.md` — archived
## Next Session
- Commit all dependency update + build fix changes
- Test the app against the dev Appwrite backend (validate v23 API calls work at runtime)
- Consider migrating Appwrite calls from deprecated positional-param to object-param style (optional)
- Consider adding `subtasks?: Task[]` to `Task` interface if the feature is planned

View File

@@ -0,0 +1,16 @@
# Archive Rules
## Raw File Archival
After creating a Source Document Summary for any raw file:
1. Move the raw file to `docs/archive/`
2. Record the move in the source summary's header: `Archived From: [original path]`
3. Do not read from `docs/archive/` unless the user explicitly says "go back to the original [filename]"
## Summary Lifecycle Rules
1. **Session handoffs expire**: After a new handoff is written, the previous handoff moves to `docs/archive/handoffs/`. Only the latest handoff stays in `docs/summaries/`.
2. **Decision records persist**: Decision records (DR-*) stay in `docs/summaries/` permanently — they are institutional memory.
3. **Source summaries persist**: Source document summaries stay until the project ends — they replace raw documents.
4. **Analysis summaries**: Keep only the latest version. If re-run, the new one replaces the old (archive the old one).
5. **Maximum active summaries**: If `docs/summaries/` exceeds 15 files, consolidate older source summaries into a single `project-digest.md` and archive the originals.

View File

@@ -0,0 +1,23 @@
# Document Processing Protocol
Use this whenever you need to process multiple documents or large files.
## For 1-3 Short Documents (< 2K words each)
Read sequentially. After each document, write a Source Document Summary (Template 1 from `templates/claude-templates.md`) to disk. Then proceed with work using summaries only.
## For 4+ Documents OR Any Document > 2K Words
**Step 1:** List all documents with file sizes. Present to user for prioritization.
**Step 2:** Process each document individually:
- Read one document
- Extract into Source Document Summary format
- Write to `./docs/summaries/source-[filename].md`
- Release the document from active consideration before reading the next
**Step 3:** After all documents are processed, read only the summaries to form your working context.
**Step 4:** Cross-reference summaries for contradictions or dependencies. Note these explicitly.
**Step 5:** Proceed with the actual task using summaries as your reference.

View File

@@ -0,0 +1,18 @@
# Subagent Deployment Rules
## When to Use Subagent vs. Main Agent
| Situation | Approach | Why |
|-----------|----------|-----|
| Reading/analyzing documents | Subagent | Keeps source content out of main context |
| Research and competitive analysis | Subagent | Heavy reading, return summary only |
| Writing deliverables | Main agent | Needs full decision-making context |
| Schema/architecture design | Main agent | Needs holistic project understanding |
| Code generation | Subagent | Isolated implementation, return result |
| Review and QA | Subagent | Fresh perspective, no bias from writing |
## Output Requirements
Subagent output must conform to the Output Contracts in `templates/claude-templates.md`. No free-form prose returns.
Optimal subagent return size: 1,000-2,000 tokens of structured summary. Longer returns consume main agent context without proportional benefit.

View File

@@ -0,0 +1,101 @@
# Session Handoff: Build Fixes & Dev Environment
**Date:** 2026-03-15
**Session Duration:** ~2 hours
**Session Focus:** Resolve all TypeScript/ESLint build errors from dependency updates; fix dev server startup
**Context Usage at Handoff:** Medium
## What Was Accomplished
1. Fixed 30 TypeScript errors (14 files) → build now passes with 0 errors
2. Fixed 12 ESLint problems (6 errors, 6 warnings) → 0 remaining
3. Fixed `quasar dev` startup error (`FlatESLint is not a constructor`) → downgraded ESLint v10→v9
4. Fixed missing Appwrite env vars in `.env.local` → app connects to backend on dev
## Exact State of Work in Progress
- **Build**: CLEAN — `yarn quasar build` exits 0, no TS or ESLint errors
- **Dev server**: FUNCTIONAL — `quasar dev` starts without errors; ESLint inline checking via `vite-plugin-checker` is restored
- **Runtime**: UNTESTED this session — app has not been manually tested against the dev Appwrite backend
## Decisions Made This Session
- **`as unknown as Type` for all Appwrite store casts** — CONFIRMED: Appwrite v23 made `DefaultDocument` strict; it no longer overlaps domain types, so the double-cast is required. Applied to: `boat.ts`, `interval.ts`, `intervalTemplate.ts`, `reservation.ts`, `task.ts`
- **ESLint downgraded v10.0.3 → v9.39.4** — CONFIRMED: `vite-plugin-checker` v0.12.0 calls `FlatESLint` which was merged back into `ESLint` in v10; v9 preserves the API. Also downgraded `@eslint/js` (v10→v9) and `eslint-plugin-vue` (v10→v9)
- **`getWeekdaySkips` removed** — CONFIRMED: removed from `@quasar/quasar-ui-qcalendar` API; `createDayList` now takes `weekdays` array directly as 4th param (previously took `weekdaySkips` computed value)
- **`subtasks` removed from TaskCardComponent template** — ASSUMED SAFE: `Task` type has no `subtasks` field; template refs were dead code. See open question.
- **`no-debugger: 'off'`** — CONFIRMED: hardcoded because `process` is not available in ESLint globals when linting `.js` files (config file context)
- **`.env.local` variable names corrected** — CONFIRMED: file had `VITE_APPWRITE_ENDPOINT` / `VITE_APPWRITE_PROJECT`; `appwrite.ts` reads `VITE_APPWRITE_API_ENDPOINT` / `VITE_APPWRITE_API_PROJECT`
## Key Numbers Generated or Discovered This Session
- TypeScript errors at session start: 30 (across 14 files)
- ESLint problems at session start: 12 (6 errors, 6 warnings)
- TypeScript errors at session end: 0
- ESLint problems at session end: 0
- ESLint: v10.0.3 → v9.39.4
- `@eslint/js`: v10 → v9
- `eslint-plugin-vue`: v10 → v9
- `register-service-worker`: newly added (was missing from package.json)
## Conditional Logic Established
- IF Appwrite SDK returns `DefaultDocument` THEN cast via `as unknown as DomainType` BECAUSE v23 `DefaultDocument` is strict and no longer assignable to domain types that extend `Partial<Models.Document>`
- IF `vite-plugin-checker` is v0.12.x THEN ESLint must be v9.x BECAUSE v0.12.x uses `FlatESLint` constructor removed in ESLint v10
- IF `createDayList` is called from qcalendar THEN pass `weekdays` array as 4th arg directly BECAUSE `getWeekdaySkips` was removed from the qcalendar public API
- IF `.env.local` is updated THEN variable names must match `import.meta.env.VITE_APPWRITE_API_ENDPOINT` / `VITE_APPWRITE_API_PROJECT` as read in `src/boot/appwrite.ts`
## Files Created or Modified
| File Path | Action | Description |
|-----------|--------|-------------|
| `src/stores/boat.ts` | Modified | `as unknown as Boat[]` |
| `src/stores/interval.ts` | Modified | `as unknown as Interval` (3 places) |
| `src/stores/intervalTemplate.ts` | Modified | Map callback cast + `as unknown as IntervalTemplate` (3 places); `timeTuple` cast |
| `src/stores/reservation.ts` | Modified | `as unknown as Reservation` (5 places) |
| `src/stores/task.ts` | Modified | `as unknown as Task[]`, `TaskTag[]`, `SkillTag[]`, `Task` (5 places) |
| `src/stores/sampledata/schedule.ts` | Modified | `id``$id`, `blocks``timeTuples`, removed `reservationDate` |
| `src/components/boat/BoatPreviewComponent.vue` | Modified | `boat.id``boat.$id` |
| `src/components/scheduling/boat/BoatScheduleTableComponent.vue` | Modified | `block.id``block.$id`; `NodeJS.Timeout``ReturnType<typeof setInterval>`; ternary→if/else |
| `src/components/scheduling/boat/CalendarHeaderComponent.vue` | Modified | Removed `getWeekdaySkips` import+computed; `createDayList` now passes `weekdays` directly |
| `src/components/task/TaskCardComponent.vue` | Modified | Removed `defineProps` explicit import; removed `subtasks` template refs |
| `src/components/task/TaskListComponent.vue` | Modified | `task.id``task.$id` |
| `src/components/task/TaskTableComponent.vue` | Modified | Removed `defineProps` from explicit import |
| `src/components/ResourceScheduleViewerComponent.vue` | Modified | Removed `|| undefined`; `catch { }`; removed stale eslint-disable comments |
| `src/pages/LoginPage.vue` | Modified | `catch { }` |
| `src/pages/schedule/ManageCalendar.vue` | Modified | `block.id``block.$id` |
| `src/boot/appwrite.ts` | Modified | Removed stale `console.log(API_ENDPOINT)` |
| `eslint.config.js` | Modified | `no-debugger` hardcoded to `'off'` |
| `quasar.config.ts` | Modified | ESLint checker restored (had been temporarily removed) |
| `package.json` / `yarn.lock` | Modified | ESLint v10→v9; `@eslint/js` v10→v9; `eslint-plugin-vue` v10→v9; added `register-service-worker` |
| `.env.local` | Modified | Variable names corrected: `VITE_APPWRITE_ENDPOINT``VITE_APPWRITE_API_ENDPOINT`, `VITE_APPWRITE_PROJECT``VITE_APPWRITE_API_PROJECT`; endpoint URL updated to include `/v1` |
| `docs/summaries/handoff-2026-03-15-build-fixes.md` | Created | This file |
| `docs/archive/handoffs/handoff-2026-03-15-dependency-updates.md` | Archived | Superseded by this handoff |
## What the NEXT Session Should Do
1. **First**: Run `quasar dev` and manually test the login flow against the dev Appwrite backend to validate v23 API calls work at runtime
2. **Validate**: Boat listing, reservation creation/cancellation, interval loading — confirm no runtime errors from the v23 positional-param deprecations
3. **Commit**: Stage all modified files and commit as `"fix: Resolve build errors from dependency updates"` (single clean commit covering all TS/ESLint/qcalendar/env fixes)
4. **Optional**: Migrate Appwrite calls from deprecated positional-param style to object-param style (affects all stores — low priority, they still work)
5. **Optional**: Add `subtasks?: Task[]` to `Task` interface in `src/stores/task.ts` if that feature is planned
## Open Questions Requiring User Input
- [ ] `task.subtasks` removed from `TaskCardComponent` template — should `subtasks?: Task[]` be added to the `Task` interface for future use, or is subtask support not planned?
- [ ] Appwrite v23 deprecated positional-param overloads (hints in every store call). Migrate now or leave for later?
## Assumptions That Need Validation
- ASSUMED: Appwrite v23 positional-param API calls behave identically at runtime to v14 — validate by doing a full login + reservation flow against the dev backend
- ASSUMED: `subtasks` in `TaskCardComponent` was dead/future code — no user confirmed this
## What NOT to Re-Read
- `docs/archive/handoffs/handoff-2026-03-15-dependency-updates.md` — archived; superseded by this file
- `docs/archive/handoffs/handoff-2026-03-15-auth-magic-link.md` — archived; auth work complete
## Files to Load Next Session
- `src/stores/task.ts` — if adding `subtasks` to Task interface
- `src/boot/appwrite.ts` — if migrating to Appwrite v23 object-param style
- Any store file (`boat.ts`, `interval.ts`, `reservation.ts`, etc.) — if migrating Appwrite calls

56
eslint.config.js Normal file
View File

@@ -0,0 +1,56 @@
import js from '@eslint/js';
import pluginVue from 'eslint-plugin-vue';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
export default tseslint.config(
{
ignores: [
'dist/**',
'.quasar/**',
'node_modules/**',
'src-capacitor/**',
'src-cordova/**',
'quasar.config.*.temporary.compiled*',
'generate-version.cjs',
'src-pwa/.eslintrc.js',
'**/*.d.ts',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
{
files: ['**/*.ts', '**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
extraFileExtensions: ['.vue'],
},
globals: {
...globals.browser,
...globals.es2021,
ga: 'readonly',
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly',
},
},
rules: {
'prefer-promise-reject-errors': 'off',
quotes: ['warn', 'single', { avoidEscape: true }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-unused-vars': 'off',
'no-debugger': 'off',
},
},
prettier,
);

22
generate-version.cjs Normal file
View File

@@ -0,0 +1,22 @@
const fs = require('fs');
const path = require('path');
try {
const version = process.argv[2];
if (!version) throw Error('Must pass version on command line');
// Create version content
const versionContent = `export const APP_VERSION = '${version}';\n`;
const versionTxtFilePath = path.resolve(__dirname, './VERSION');
const versionFilePath = path.resolve(__dirname, 'src/version.ts');
// Write version to TXT file
fs.writeFileSync(versionTxtFilePath, version, 'utf8');
// Write version to js 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,56 +1,68 @@
{ {
"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,
"type": "module",
"scripts": { "scripts": {
"lint": "eslint --ext .js,.ts,.vue ./", "generate-version": "node generate-version.cjs",
"lint": "eslint .",
"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": "yarn generate-version && quasar dev -m pwa",
"build": "quasar build" "build": "yarn generate-version && quasar build -m pwa"
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.11", "@quasar/extras": "^1.17.0",
"@quasar/quasar-app-extension-qcalendar": "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/app-extension.tgz", "@quasar/quasar-ui-qcalendar": "^4.1.2",
"@quasar/quasar-ui-qcalendar": "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/qcalendar-ui.tgz", "appwrite": "^23.0.0",
"appwrite": "^14.0.1", "axios": "^1.13.6",
"axios": "^1.6.8",
"file": "^0.2.2", "file": "^0.2.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"register-service-worker": "^1.7.2",
"vue": "3", "vue": "3",
"vue-router": "4", "vue-router": "4",
"vue3-google-login": "^2.0.26" "vue3-google-login": "^2.0.37"
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-vite": "^1.9.1", "@eslint/js": "^9",
"@types/node": "^12.20.21", "@quasar/app-vite": "^2.4.1",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@saithodev/semantic-release-gitea": "^2.1.0",
"@typescript-eslint/parser": "^5.10.0", "@semantic-release/changelog": "^6.0.3",
"autoprefixer": "^10.4.2", "@semantic-release/exec": "^7.1.0",
"dotenv": "^16.3.1", "@semantic-release/github": "^12.0.6",
"eslint": "^8.10.0", "@semantic-release/npm": "^13.1.5",
"eslint-config-prettier": "^8.1.0", "@types/node": "^25.5.0",
"eslint-plugin-vue": "^9.0.0", "autoprefixer": "^10.4.27",
"prettier": "^2.5.1", "dotenv": "^17.3.1",
"quasar": "^2.16.0", "eslint": "^9",
"typescript": "~5.3.0", "eslint-config-prettier": "^10.1.8",
"vite-plugin-checker": "^0.6.4", "eslint-plugin-vue": "^9",
"vue-tsc": "^1.8.22", "git-commit-info": "^2.0.2",
"workbox-build": "^7.0.0", "globals": "^17.4.0",
"workbox-cacheable-response": "^7.0.0", "prettier": "^3.8.1",
"workbox-core": "^7.0.0", "quasar": "^2.18.6",
"workbox-expiration": "^7.0.0", "semantic-release": "^25.0.3",
"workbox-precaching": "^7.0.0", "typescript": "^5.9.3",
"workbox-routing": "^7.0.0", "typescript-eslint": "^8.57.0",
"workbox-strategies": "^7.0.0", "vite-plugin-checker": "^0.12.0",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.5",
"workbox-build": "^7.4.0",
"workbox-cacheable-response": "^7.4.0",
"workbox-core": "^7.4.0",
"workbox-expiration": "^7.4.0",
"workbox-precaching": "^7.4.0",
"workbox-routing": "^7.4.0",
"workbox-strategies": "^7.4.0",
"yarn": "^1.22.21" "yarn": "^1.22.21"
}, },
"engines": { "engines": {
"node": "^20 || ^18 || ^16 || ^14.19", "node": "^20 || ^18 || ^16 || ^14.19",
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1"
} },
"packageManager": "yarn@4.13.0"
} }

BIN
public/tmpimg/projectX.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,26 +1,10 @@
/* eslint-env node */
/*
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
* the ES6 features that are supported by your Node version. https://node.green/
*/
// Configuration for your app // Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { configure } = require('quasar/wrappers'); import { defineConfig } from '#q-app/wrappers';
module.exports = configure(function (/* ctx */) { export default defineConfig(function () {
return { return {
eslint: {
// fix: true,
// include: [],
// exclude: [],
// rawOptions: {},
warnings: true,
errors: true,
},
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
// preFetch: true, // preFetch: true,
@@ -48,7 +32,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',
@@ -80,7 +63,8 @@ module.exports = configure(function (/* ctx */) {
tsconfigPath: 'tsconfig.vue-tsc.json', tsconfigPath: 'tsconfig.vue-tsc.json',
}, },
eslint: { eslint: {
lintCommand: 'eslint "./**/*.{js,ts,mjs,cjs,vue}"', lintCommand: 'eslint .',
useFlatConfig: true,
}, },
}, },
{ server: false }, { server: false },
@@ -97,17 +81,24 @@ module.exports = configure(function (/* ctx */) {
// This works around CORS problems when developing locally, using the Appwrite backend // This works around CORS problems when developing locally, using the Appwrite backend
proxy: { proxy: {
'/api': { '/api': {
target: 'https://apidev.bab.toal.ca/', target: 'https://appwrite.toal.ca/',
changeOrigin: true, changeOrigin: true,
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://appwrite.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: {
@@ -119,9 +110,7 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: { framework: {
config: { config: {},
autoImportComponentCase: 'kebab', // or 'kebab' (default) or 'combined'
},
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack // lang: 'en-US', // Quasar language pack
@@ -183,7 +172,7 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
pwa: { pwa: {
workboxMode: 'generateSW', // or 'injectManifest' workboxMode: 'GenerateSW', // or 'InjectManifest'
injectPwaMetaTags: true, injectPwaMetaTags: true,
swFilename: 'sw.js', swFilename: 'sw.js',
manifestFilename: 'manifest.json', manifestFilename: 'manifest.json',
@@ -234,8 +223,6 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: { bex: {
contentScripts: ['my-content-script'],
// extendBexScriptsConf (esbuildConf) {} // extendBexScriptsConf (esbuildConf) {}
// extendBexManifestJson (json) {} // extendBexManifestJson (json) {}
}, },

View File

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

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

@@ -1,4 +1,4 @@
import { boot } from 'quasar/wrappers'; import { defineBoot } from '#q-app/wrappers';
import { import {
Client, Client,
Account, Account,
@@ -14,28 +14,37 @@ 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', AppwriteIds = {
databaseId: 'bab_prod',
collection: { collection: {
boat: 'boat', boat: 'boat',
reservation: 'reservation', reservation: 'reservation',
@@ -48,21 +57,6 @@ const AppwriteIds = process.env.DEV
function: { function: {
userinfo: 'userinfo', userinfo: 'userinfo',
}, },
}
: {
databaseId: 'bab_prod',
collection: {
boat: 'boat',
reservation: 'reservation',
skillTags: 'skillTags',
task: 'task',
taskTags: 'taskTags',
interval: 'interval',
intervalTemplate: 'intervalTemplate',
},
function: {
userinfo: '664038294b5473ef0c8d',
},
}; };
const account = new Account(client); const account = new Account(client);
@@ -72,7 +66,7 @@ const teams = new Teams(client);
let appRouter: Router; let appRouter: Router;
export default boot(async ({ router }) => { export default defineBoot(async ({ router }) => {
// Initialize store // Initialize store
const authStore = useAuthStore(); const authStore = useAuthStore();
await authStore.init(); await authStore.init();
@@ -120,6 +114,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 +142,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

@@ -1,8 +0,0 @@
<template>
<div @click="auth.googleLogin()">Login with Google</div>
</template>
<script setup lang="ts">
import { useAuthStore } from 'src/stores/auth';
const auth = useAuthStore();
</script>

View File

@@ -49,6 +49,13 @@
</div> </div>
</q-list> </q-list>
</template> </template>
<q-item
clickable
v-ripple
@click="showAbout()">
<q-item-section avatar><q-icon name="info" /></q-item-section>
<q-item-section>About</q-item-section>
</q-item>
<q-item <q-item
clickable clickable
v-ripple v-ripple
@@ -63,8 +70,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { Dialog } from 'quasar';
import { enabledLinks } from 'src/router/navlinks.js'; import { enabledLinks } from 'src/router/navlinks.js';
import { logout } from 'boot/appwrite'; import { logout } from 'src/boot/appwrite';
import { APP_VERSION } from 'src/version';
function showAbout() {
Dialog.create({
title: 'OYS Borrow a Boat',
message: `Version ${APP_VERSION}<br>Manage a Borrow a Boat program for a Yacht Club.<br><br>© Oakville Yacht Squadron`,
html: true,
});
}
defineProps(['drawer']); defineProps(['drawer']);
defineEmits(['drawer-toggle']); defineEmits(['drawer-toggle']);

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

@@ -111,6 +111,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { import {
QCalendarResource, QCalendarResource,
QCalendarMonth,
TimestampOrNull, TimestampOrNull,
today, today,
parseTimestamp, parseTimestamp,
@@ -169,11 +170,11 @@ const disabledBefore = computed(() => {
function monthFormatter() { function monthFormatter() {
try { try {
return new Intl.DateTimeFormat('en-CA' || undefined, { return new Intl.DateTimeFormat('en-CA', {
month: 'long', month: 'long',
timeZone: 'UTC', timeZone: 'UTC',
}); });
} catch (e) { } catch {
// //
} }
} }
@@ -184,7 +185,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(
@@ -233,16 +234,10 @@ function onClickTime(data: EventData) {
function onUpdateDuration(value: EventData) { function onUpdateDuration(value: EventData) {
emit('onUpdateDuration', value); emit('onUpdateDuration', value);
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onClickInterval = () => {}; const onClickInterval = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onClickHeadResources = () => {}; const onClickHeadResources = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onClickResource = () => {}; const onClickResource = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onResourceExpanded = () => {}; const onResourceExpanded = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onMoved = () => {}; const onMoved = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const onChange = () => {}; const onChange = () => {};
</script> </script>

View File

@@ -7,27 +7,20 @@
round round
icon="menu" icon="menu"
aria-label="Menu" aria-label="Menu"
@click="toggleLeftDrawer" /> @click="$emit('drawer-toggle')" />
<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
:drawer="leftDrawerOpen"
@drawer-toggle="toggleLeftDrawer" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { APP_VERSION } from 'src/version';
import LeftDrawer from 'components/LeftDrawer.vue';
const leftDrawerOpen = ref(false);
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
defineProps({ defineProps({
pageTitle: String, pageTitle: String,
}); });
defineEmits(['drawer-toggle']);
</script> </script>

View File

@@ -1,9 +1,11 @@
<template> <template>
<div v-if="boats"> <div
v-if="boats"
class="row">
<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 col-xs-12 col-sm-6 col-xl-3">
<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"
@@ -51,7 +51,7 @@
scope.timeDurationHeight scope.timeDurationHeight
) )
" "
:id="block.id" :id="block.$id"
@click="selectBlock($event, scope, block)"> @click="selectBlock($event, scope, block)">
{{ boats[scope.columnIndex].name }} {{ boats[scope.columnIndex].name }}
<br /> <br />
@@ -113,7 +113,7 @@ const selectedDate = ref(today());
const { getAvailableIntervals } = useIntervalStore(); const { getAvailableIntervals } = useIntervalStore();
const calendar = ref<QCalendarDay | null>(null); const calendar = ref<QCalendarDay | null>(null);
const now = ref(new Date()); const now = ref(new Date());
let intervalId: string | number | NodeJS.Timeout | undefined; let intervalId: ReturnType<typeof setInterval> | undefined;
onMounted(async () => { onMounted(async () => {
await useBoatStore().fetchBoats(); await useBoatStore().fetchBoats();
@@ -125,8 +125,12 @@ onMounted(async () => {
onUnmounted(() => clearInterval(intervalId)); onUnmounted(() => clearInterval(intervalId));
function handleSwipe({ ...event }) { function handleSwipe({ direction }: { direction: string }) {
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next(); if (direction === 'right') {
calendar.value?.prev();
} else {
calendar.value?.next();
}
} }
function reservationStyles( function reservationStyles(
reservation: Reservation, reservation: Reservation,
@@ -207,7 +211,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

@@ -48,7 +48,6 @@ import {
createNativeLocaleFormatter, createNativeLocaleFormatter,
getEndOfWeek, getEndOfWeek,
getStartOfWeek, getStartOfWeek,
getWeekdaySkips,
parseTimestamp, parseTimestamp,
today, today,
} from '@quasar/quasar-ui-qcalendar'; } from '@quasar/quasar-ui-qcalendar';
@@ -63,10 +62,6 @@ const weekdays = reactive([1, 2, 3, 4, 5, 6, 0]),
dayFormatter = dayFormatterFunc(), dayFormatter = dayFormatterFunc(),
weekdayFormatter = weekdayFormatterFunc(); weekdayFormatter = weekdayFormatterFunc();
const weekdaySkips = computed(() => {
return getWeekdaySkips(weekdays);
});
const parsedStart = computed(() => const parsedStart = computed(() =>
getStartOfWeek( getStartOfWeek(
parseTimestamp(selectedDate.value || today()) as Timestamp, parseTimestamp(selectedDate.value || today()) as Timestamp,
@@ -93,7 +88,7 @@ const days = computed(() => {
parsedStart.value, parsedStart.value,
parsedEnd.value, parsedEnd.value,
today2.value as Timestamp, today2.value as Timestamp,
weekdaySkips.value weekdays
); );
} }
return []; return [];

View File

@@ -5,19 +5,10 @@
<q-item-label caption lines="2">{{ task.description }} </q-item-label> <q-item-label caption lines="2">{{ task.description }} </q-item-label>
<q-item-label caption>Due: {{ task.due_date }}</q-item-label> <q-item-label caption>Due: {{ task.due_date }}</q-item-label>
</q-item-section> </q-item-section>
<q-expansion-item
v-if="task.subtasks && task.subtasks.length"
expand-separator
label="Subtasks"
default-opened
>
<TaskListComponent :tasks="task.subtasks" />
</q-expansion-item>
</q-card> </q-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from 'vue';
import type { Task } from 'src/stores/task'; import type { Task } from 'src/stores/task';
defineProps<{ task: Task }>(); defineProps<{ task: Task }>();

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="q-pa-md" style="max-width: 350px"> <div class="q-pa-md" style="max-width: 350px">
<q-list> <q-list>
<div v-for="task in tasks" :key="task.id"> <div v-for="task in tasks" :key="task.$id">
<TaskCardComponent :task="task" /> <TaskCardComponent :task="task" />
</div> </div>
</q-list> </q-list>

View File

@@ -212,7 +212,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineProps, ref } from 'vue'; import { computed, ref } from 'vue';
import { useTaskStore, Task, SkillTag, TaskTag } from 'src/stores/task'; import { useTaskStore, Task, SkillTag, TaskTag } from 'src/stores/task';
import { QTableProps, date, useQuasar } from 'quasar'; import { QTableProps, date, useQuasar } from 'quasar';
import { Boat, useBoatStore } from 'src/stores/boat'; import { Boat, useBoatStore } from 'src/stores/boat';

View File

@@ -1,4 +1,5 @@
// app global css in SASS form // app global css in SASS form
@import '@quasar/quasar-ui-qcalendar/dist/index.css'
.mobile-card .mobile-card
width: 100% width: 100%
max-width: 450px max-width: 450px

View File

@@ -1,5 +1,22 @@
<template> <template>
<q-layout view="hHh Lpr fFf"> <q-layout view="hHh Lpr fFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer" />
<q-toolbar-title>{{ route.meta.title as string }}</q-toolbar-title>
<q-space />
<div>v{{ APP_VERSION }}</div>
</q-toolbar>
</q-header>
<LeftDrawer
:drawer="leftDrawerOpen"
@drawer-toggle="toggleLeftDrawer" />
<q-page-container> <q-page-container>
<router-view /> <router-view />
</q-page-container> </q-page-container>
@@ -10,10 +27,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import BottomNavComponent from 'src/components/BottomNavComponent.vue'; import BottomNavComponent from 'src/components/BottomNavComponent.vue';
import LeftDrawer from 'src/components/LeftDrawer.vue';
import { APP_VERSION } from 'src/version';
const q = useQuasar(); const q = useQuasar();
const route = useRoute();
const leftDrawerOpen = ref(false);
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
// q.fullscreen.request(); // q.fullscreen.request();
q.addressbarColor.set('#14539a'); q.addressbarColor.set('#14539a');
</script> </script>

View File

@@ -1,5 +1,4 @@
<template> <template>
<ToolbarComponent />
<q-page class="row justify-center"> <q-page class="row justify-center">
<q-img alt="OYS Logo" src="~assets/oysqn_logo.png" fit="scale-down" /> <q-img alt="OYS Logo" src="~assets/oysqn_logo.png" fit="scale-down" />
<q-list class="full-width mobile-only"> <q-list class="full-width mobile-only">
@@ -24,5 +23,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import { enabledLinks } from 'src/router/navlinks.js'; import { enabledLinks } from 'src/router/navlinks.js';
import ToolbarComponent from 'components/ToolbarComponent.vue';
</script> </script>

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,33 @@
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
v-if="!userId"
type="button"
@click="sendMagicLink"
color="secondary"
label="Send Magic Link"
style="width: 300px" />
</div>
<div class="row justify-center q-ma-sm">
<q-btn
type="button"
@click="doTokenLogin"
color="primary"
:label="userId ? 'Login' : 'Send Code'"
style="width: 300px" />
</div>
</q-card-section>
</q-card> </q-card>
</q-page> </q-page>
</q-page-container> </q-page-container>
@@ -74,14 +72,111 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, onMounted } from 'vue';
import { login } from 'boot/appwrite'; import { Dialog, Notify } from 'quasar';
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue'; import { useAuthStore } from 'src/stores/auth';
import { useRouter } from 'vue-router';
import { AppwriteException } from 'appwrite';
const email = ref(''); const email = ref('');
const password = ref(''); const token = ref('');
const userId = ref();
const router = useRouter();
const authStore = useAuthStore();
const doLogin = async () => { onMounted(async () => {
login(email.value, password.value); const query = router.currentRoute.value.query;
if (query.userId && query.secret) {
const notification = Notify.create({
type: 'primary',
position: 'top',
spinner: true,
message: 'Logging you in...',
timeout: 8000,
group: false,
});
try {
await authStore.magicURLLogin(query.userId as string, query.secret as string);
notification({ type: 'positive', message: 'Logged in!', timeout: 2000, spinner: false, icon: 'check_circle' });
router.replace({ name: 'index' });
} catch (error: unknown) {
notification({ type: 'negative', message: 'Magic link login failed.', timeout: 3000, spinner: false });
if (error instanceof AppwriteException) {
Dialog.create({ title: 'Login Error', message: error.message, persistent: true });
}
}
}
});
const sendMagicLink = async () => {
if (!email.value) {
Dialog.create({ message: 'Please enter your e-mail address.' });
return;
}
try {
await authStore.createMagicURLSession(email.value);
Dialog.create({ message: 'Check your e-mail for a magic login link.' });
} catch {
Dialog.create({ message: 'An error occurred. Please ask for help in Discord.' });
}
};
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 {
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,61 +1,66 @@
<template> <template>
<q-layout>
<q-page-container>
<q-page padding> <q-page padding>
<h1>Privacy Policy for bab.toal.ca</h1> <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
information:
</p> </p>
<ul> <ul>
<li>bab.toal.ca needs to perform a contract with you</li> <li>Undock needs to perform a contract with you</li>
<li>You have given bab.toal.ca permission to do so</li> <li>You have given Undock permission to do so</li>
<li> <li>
Processing your personal information is in bab.toal.ca legitimate Processing your personal information is in Undock legitimate
interests interests
</li> </li>
<li>bab.toal.ca needs to comply with the law</li> <li>Undock needs to comply with the law</li>
</ul> </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
rights:
</p> </p>
<ul> <ul>
<li> <li>
The right to access, update or to delete the information we have on you. The right to access, update or to delete the information we have on
you.
</li> </li>
<li>The right of rectification.</li> <li>The right of rectification.</li>
<li>The right to object.</li> <li>The right to object.</li>
@@ -67,97 +72,102 @@
<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'
respective websites.
</p> </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
<a href="/terms-of-service">terms</a>
.
</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

@@ -1,15 +1,44 @@
<template> <template>
<toolbar-component pageTitle="Member Profile" /> <q-page
<q-page padding> padding
<q-list bordered> 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 +46,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>
@@ -34,8 +75,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
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>
<div v-else-if="validResetLink()">
<q-form <q-form
@submit="submitNewPw" @submit="submitNewPw"
v-else-if="validResetLink()"> @keydown.enter.prevent="resetPw">
<q-card-section class="q-ma-sm"> <NewPasswordComponent v-model="newPassword" />
<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>
<q-card-actions> <q-card-actions>
<q-btn <q-btn
type="submit" type="submit"
label="Reset Password" label="Reset Password"
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-form> </q-form>
</div>
<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';
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-layout>
<q-page-container>
<q-page padding> <q-page padding>
<h1>Website Terms and Conditions of Use</h1> <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
display;
</li> </li>
<li> <li>
attempt to reverse engineer any software contained on bab.toal.ca's attempt to reverse engineer any software contained on undock.ca's
Website; Website;
</li> </li>
<li> <li>
remove any copyright or other proprietary notations from the materials; remove any copyright or other proprietary notations from the
or materials; or
</li> </li>
<li> <li>
transferring the materials to another person or "mirror" the materials transferring the materials to another person or "mirror" the
on any other server. materials on any other server.
</li> </li>
</ul> </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

@@ -1,5 +1,7 @@
<template> <template>
<q-page>
<BoatReservationComponent v-model="newReservation" /> <BoatReservationComponent v-model="newReservation" />
</q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -36,7 +36,7 @@
:transparent="interval.user != undefined" :transparent="interval.user != undefined"
:color="interval.user ? 'secondary' : 'primary'" :color="interval.user ? 'secondary' : 'primary'"
:outline="!interval.user" :outline="!interval.user"
:id="interval.id"> :id="interval.$id">
{{ {{
interval.user interval.user
? useAuthStore().getUserNameById(interval.user) ? useAuthStore().getUserNameById(interval.user)
@@ -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
@@ -132,18 +132,22 @@ const createReservationFromInterval = (interval: Interval | Reservation) => {
}; };
function handleSwipe({ ...event }) { function handleSwipe({ ...event }) {
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next(); if (event.direction === 'right') {
calendar.value?.prev();
} else {
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

@@ -1,4 +1,5 @@
<template> <template>
<q-page>
<q-tabs <q-tabs
v-model="tab" v-model="tab"
inline-label inline-label
@@ -22,7 +23,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 +42,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,19 +52,20 @@
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>
</q-tab-panel> </q-tab-panel>
</q-tab-panels> </q-tab-panels>
</q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useReservationStore } from 'src/stores/reservation'; 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

@@ -1,9 +1,7 @@
<template> <template>
<div class="fit row wrap justify-start items-start content-start"> <div class="fit row">
<div class="q-pa-md"> <div class="q-pa-md">
<div <div class="scheduler col-12">
class="scheduler"
style="max-width: 1200px">
<NavigationBar <NavigationBar
@next="onNext" @next="onNext"
@today="onToday" @today="onToday"
@@ -26,7 +24,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,11 +35,9 @@
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') }} -
{{ date.formatDate(block.end, 'HH:mm') }} {{ date.formatDate(block.end, 'HH:mm') }}
@@ -128,7 +126,6 @@
:model-value="template" /> :model-value="template" />
</q-list> </q-list>
</div> </div>
</div>
<q-dialog v-model="alert"> <q-dialog v-model="alert">
<q-card> <q-card>
<q-card-section> <q-card-section>
@@ -153,6 +150,7 @@
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -163,7 +161,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 +206,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),
),
); );
}; };
@@ -235,14 +235,14 @@ function getIntervals(date: Timestamp, boat: Boat) {
function intervalsFromTemplate( function intervalsFromTemplate(
boat: Boat, boat: Boat,
templateId: string, templateId: string,
date: string date: string,
): Interval[] { ): Interval[] {
const template = intervalTemplateStore const template = intervalTemplateStore
.getIntervalTemplates() .getIntervalTemplates()
.value.find((t) => t.$id === templateId); .value.find((t) => t.$id === templateId);
return template return template
? template.timeTuples.map((timeTuple: TimeTuple) => ? template.timeTuples.map((timeTuple: TimeTuple) =>
buildInterval(boat, timeTuple, date) buildInterval(boat, timeTuple, date),
) )
: []; : [];
} }
@@ -279,7 +279,7 @@ function onDrop(
//TODO: Move all overlap checking to the store. This is too messy right now. //TODO: Move all overlap checking to the store. This is too messy right now.
e: DragEvent, e: DragEvent,
type: string, type: string,
scope: { resource: Boat; timestamp: Timestamp } scope: { resource: Boat; timestamp: Timestamp },
) { ) {
if (e.target instanceof HTMLDivElement) if (e.target instanceof HTMLDivElement)
e.target.classList.remove('bg-secondary'); e.target.classList.remove('bg-secondary');
@@ -293,10 +293,10 @@ 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),
) ),
) ),
) )
.flat(1); .flat(1);
if (overlapped.value.length === 0) { if (overlapped.value.length === 0) {

View File

@@ -1,5 +1,7 @@
<template> <template>
<q-page>
<BoatReservationComponent v-model="reservation" /> <BoatReservationComponent v-model="reservation" />
</q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

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

View File

@@ -1,61 +1,66 @@
<template> <template>
<q-layout>
<q-page-container>
<q-page padding> <q-page padding>
<h1>Privacy Policy for bab.toal.ca</h1> <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
information:
</p> </p>
<ul> <ul>
<li>bab.toal.ca needs to perform a contract with you</li> <li>Undock needs to perform a contract with you</li>
<li>You have given bab.toal.ca permission to do so</li> <li>You have given Undock permission to do so</li>
<li> <li>
Processing your personal information is in bab.toal.ca legitimate Processing your personal information is in Undock legitimate
interests interests
</li> </li>
<li>bab.toal.ca needs to comply with the law</li> <li>Undock needs to comply with the law</li>
</ul> </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
rights:
</p> </p>
<ul> <ul>
<li> <li>
The right to access, update or to delete the information we have on you. The right to access, update or to delete the information we have on
you.
</li> </li>
<li>The right of rectification.</li> <li>The right of rectification.</li>
<li>The right to object.</li> <li>The right to object.</li>
@@ -67,97 +72,102 @@
<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'
respective websites.
</p> </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>

2
src/quasar.d.ts vendored
View File

@@ -1,5 +1,3 @@
/* eslint-disable */
// Forces TS to apply `@quasar/app-vite` augmentations of `quasar` package // Forces TS to apply `@quasar/app-vite` augmentations of `quasar` package
// Removing this would break `quasar/wrappers` imports as those typings are declared // Removing this would break `quasar/wrappers` imports as those typings are declared
// into `@quasar/app-vite` // into `@quasar/app-vite`

View File

@@ -1,4 +1,4 @@
import { route } from 'quasar/wrappers'; import { defineRouter } from '#q-app/wrappers';
import { import {
createMemoryHistory, createMemoryHistory,
createRouter, createRouter,
@@ -22,7 +22,7 @@ const publicRoutes = routes
* with the Router instance. * with the Router instance.
*/ */
export default route(function (/* { store, ssrContext } */) { export default defineRouter(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER const createHistory = process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history' : process.env.VUE_ROUTER_MODE === 'history'
@@ -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',
@@ -61,15 +61,6 @@ export const links = <Link[]>[
front_links: false, front_links: false,
enabled: true, enabled: true,
}, },
{
name: 'Manage',
to: '/schedule/manage',
icon: 'edit_calendar',
front_links: false,
enabled: true,
color: 'accent',
requiredRoles: ['Schedule Admins'],
},
], ],
}, },
{ {
@@ -100,6 +91,24 @@ export const links = <Link[]>[
front_links: true, front_links: true,
enabled: false, enabled: false,
}, },
{
name: 'Manage',
icon: 'tune',
enabled: true,
requiredRoles: ['Schedule Admins'],
color: 'negative',
sublinks: [
{
name: 'Schedule',
to: '/schedule/manage',
icon: 'edit_calendar',
front_links: false,
enabled: true,
color: 'accent',
requiredRoles: ['Schedule Admins'],
},
],
},
]; ];
const authStore = useAuthStore(); const authStore = useAuthStore();

View File

@@ -13,16 +13,19 @@ const routes: RouteRecordRaw[] = [
// component: () => import('pages/IndexPage.vue'), // component: () => import('pages/IndexPage.vue'),
component: () => import('src/pages/IndexPage.vue'), component: () => import('src/pages/IndexPage.vue'),
name: 'index', name: 'index',
meta: { title: 'OYS Borrow a Boat' },
}, },
{ {
path: '/boat', path: '/boat',
component: () => import('src/pages/BoatPage.vue'), component: () => import('src/pages/BoatPage.vue'),
name: 'boat', name: 'boat',
meta: { title: 'Boats' },
}, },
{ {
path: '/schedule', path: '/schedule',
component: () => import('pages/schedule/SchedulePageView.vue'), component: () => import('pages/schedule/SchedulePageView.vue'),
name: 'schedule', name: 'schedule',
meta: { title: 'Schedule' },
children: [ children: [
{ {
path: '', path: '',
@@ -64,10 +67,12 @@ const routes: RouteRecordRaw[] = [
path: '/certification', path: '/certification',
component: () => import('src/pages/CertificationPage.vue'), component: () => import('src/pages/CertificationPage.vue'),
name: 'certification', name: 'certification',
meta: { title: 'Certifications' },
}, },
{ {
path: '/task', path: '/task',
name: 'task', name: 'task',
meta: { title: 'Tasks' },
children: [ children: [
{ {
path: '', path: '',
@@ -85,16 +90,19 @@ const routes: RouteRecordRaw[] = [
path: '/checklist', path: '/checklist',
component: () => import('pages/ChecklistPage.vue'), component: () => import('pages/ChecklistPage.vue'),
name: 'checklist', name: 'checklist',
meta: { title: 'Checklist' },
}, },
{ {
path: '/profile', path: '/profile',
component: () => import('src/pages/ProfilePage.vue'), component: () => import('src/pages/ProfilePage.vue'),
name: 'profile', name: 'profile',
meta: { title: 'Member Profile' },
}, },
{ {
path: '/reference', path: '/reference',
component: () => import('src/pages/reference/ReferencePage.vue'), component: () => import('src/pages/reference/ReferencePage.vue'),
name: 'reference', name: 'reference',
meta: { title: 'Reference' },
children: [ children: [
{ {
path: '', path: '',
@@ -103,7 +111,7 @@ const routes: RouteRecordRaw[] = [
name: 'reference-index', name: 'reference-index',
}, },
{ {
path: '/reference/:id/view', path: 'reference/:id/view',
component: () => component: () =>
import('src/pages/reference/ReferenceItemPage.vue'), import('src/pages/reference/ReferenceItemPage.vue'),
}, },
@@ -117,12 +125,12 @@ const routes: RouteRecordRaw[] = [
meta: { requiredRoles: ['admin'] }, meta: { requiredRoles: ['admin'] },
children: [ children: [
{ {
path: '/user', path: 'user',
component: () => import('pages/admin/UserAdminPage.vue'), component: () => import('pages/admin/UserAdminPage.vue'),
name: 'useradmin', name: 'useradmin',
}, },
{ {
path: '/boat', path: 'boat',
component: () => import('pages/admin/BoatAdminPage.vue'), component: () => import('pages/admin/BoatAdminPage.vue'),
name: 'boatadmin', name: 'boatadmin',
}, },
@@ -144,14 +152,6 @@ const routes: RouteRecordRaw[] = [
publicRoute: true, publicRoute: true,
}, },
}, },
{
path: '/login',
component: () => import('pages/LoginPage.vue'),
name: 'login',
meta: {
publicRoute: true,
},
},
{ {
path: '/terms-of-service', path: '/terms-of-service',
component: () => import('pages/TermsOfServicePage.vue'), component: () => import('pages/TermsOfServicePage.vue'),
@@ -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

@@ -1,7 +1,9 @@
import { defineStore } from 'pinia'; 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, 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,26 @@ export const useAuthStore = defineStore('auth', () => {
await init(); await init();
} }
async function googleLogin() { async function createTokenSession(email: string) {
account.createOAuth2Session( return await account.createEmailToken(ID.unique(), email);
OAuthProvider.Google, }
'https://bab.toal.ca/',
'https://bab.toal.ca/#/login' async function createMagicURLSession(email: string) {
return await account.createMagicURLToken(
ID.unique(),
email,
window.location.origin + '/login'
); );
currentUser.value = await account.get(); }
async function tokenLogin(userId: string, token: string) {
await account.createSession(userId, token);
await init();
}
async function magicURLLogin(userId: string, secret: string) {
await account.updateMagicURLSession(userId, secret);
await init();
} }
function getUserNameById(id: string | undefined | null): string { function getUserNameById(id: string | undefined | null): string {
@@ -78,7 +95,12 @@ export const useAuthStore = defineStore('auth', () => {
} }
function logout() { function logout() {
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 {
@@ -86,8 +108,12 @@ export const useAuthStore = defineStore('auth', () => {
getUserNameById, getUserNameById,
hasRequiredRole, hasRequiredRole,
register, register,
updateName,
login, login,
googleLogin, createTokenSession,
createMagicURLSession,
tokenLogin,
magicURLLogin,
logout, logout,
init, init,
}; };

View File

@@ -33,7 +33,7 @@ export const useBoatStore = defineStore('boat', () => {
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collection.boat AppwriteIds.collection.boat
); );
boats.value = response.documents as Boat[]; boats.value = response.documents as unknown as Boat[];
} catch (error) { } catch (error) {
console.error('Failed to fetch boats', error); console.error('Failed to fetch boats', error);
} }

View File

@@ -1,18 +1,5 @@
import { store } from 'quasar/wrappers'; import { defineStore } from '#q-app/wrappers';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { Router } from 'vue-router';
/*
* When adding new properties to stores, you should also
* extend the `PiniaCustomProperties` interface.
* @see https://pinia.vuejs.org/core-concepts/plugins.html#typing-new-store-properties
*/
declare module 'pinia' {
export interface PiniaCustomProperties {
readonly router: Router;
}
}
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
* directly export the Store instantiation; * directly export the Store instantiation;
@@ -22,7 +9,7 @@ declare module 'pinia' {
* with the Store instance. * with the Store instance.
*/ */
export default store((/* { ssrContext } */) => { export default defineStore((/* { ssrContext } */) => {
const pinia = createPinia(); const pinia = createPinia();
// You can add Pinia plugins here // You can add Pinia plugins here

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 unknown as Interval;
if (!payload.$id) return;
if (
response.events.includes('databases.*.collections.*.documents.*.delete')
) {
intervals.value.delete(payload.$id);
} else {
intervals.value.set(payload.$id, payload);
}
}
);
const getIntervals = (date: Timestamp | string, boat?: Boat) => {
const searchDate = typeof date === 'string' ? date : date.date; const 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> {
@@ -76,13 +92,13 @@ 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 unknown 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');
} }
} }
@@ -94,7 +110,7 @@ export const useIntervalStore = defineStore('interval', () => {
ID.unique(), ID.unique(),
interval interval
); );
intervals.value.set(response.$id, response as Interval); intervals.value.set(response.$id, response as unknown as Interval);
} catch (e) { } catch (e) {
console.error('Error creating Interval: ' + e); console.error('Error creating Interval: ' + e);
} }
@@ -108,7 +124,7 @@ export const useIntervalStore = defineStore('interval', () => {
interval.$id, interval.$id,
{ ...interval, $id: undefined } { ...interval, $id: undefined }
); );
intervals.value.set(response.$id, response as Interval); intervals.value.set(response.$id, response as unknown as Interval);
console.info(`Saved Interval: ${interval.$id}`); console.info(`Saved Interval: ${interval.$id}`);
} else { } else {
console.error('Update interval called without an ID'); console.error('Update interval called without an ID');
@@ -140,5 +156,6 @@ export const useIntervalStore = defineStore('interval', () => {
updateInterval, updateInterval,
deleteInterval, deleteInterval,
selectedDate, selectedDate,
intervals,
}; };
}); });

View File

@@ -20,14 +20,13 @@ export const useIntervalTemplateStore = defineStore('intervalTemplate', () => {
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collection.intervalTemplate AppwriteIds.collection.intervalTemplate
); );
intervalTemplates.value = response.documents.map( intervalTemplates.value = response.documents.map((d): IntervalTemplate => {
(d: Models.Document): IntervalTemplate => { const doc = d as unknown as { timeTuple: string[] } & Models.Document;
return { return {
...d, ...doc,
timeTuples: arrayToTimeTuples(d.timeTuple), timeTuples: arrayToTimeTuples(doc.timeTuple),
} as IntervalTemplate; } as unknown as IntervalTemplate;
} });
);
} catch (error) { } catch (error) {
console.error('Failed to fetch timeblock templates', error); console.error('Failed to fetch timeblock templates', error);
} }
@@ -41,7 +40,7 @@ export const useIntervalTemplateStore = defineStore('intervalTemplate', () => {
ID.unique(), ID.unique(),
{ name: template.name, timeTuple: template.timeTuples.flat(2) } { name: template.name, timeTuple: template.timeTuples.flat(2) }
); );
intervalTemplates.value.push(response as IntervalTemplate); intervalTemplates.value.push(response as unknown as IntervalTemplate);
} catch (e) { } catch (e) {
console.error('Error updating IntervalTemplate: ' + e); console.error('Error updating IntervalTemplate: ' + e);
} }
@@ -79,8 +78,10 @@ export const useIntervalTemplateStore = defineStore('intervalTemplate', () => {
? b ? b
: ({ : ({
...response, ...response,
timeTuples: arrayToTimeTuples(response.timeTuple), timeTuples: arrayToTimeTuples(
} as IntervalTemplate) (response as unknown as { timeTuple: string[] }).timeTuple
),
} as unknown as IntervalTemplate)
); );
} catch (e) { } catch (e) {
console.error('Error updating IntervalTemplate: ' + e); console.error('Error updating IntervalTemplate: ' + e);

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

@@ -20,33 +20,33 @@ function getSampleData(): ReferenceEntry[] {
content: `Its hard to imagine that a modern 27 foot sailboat with a classic look, superb 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 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 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 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. 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 Fun and Fast. There are some impressive victories to back this up, but that doesnt
tell the whole story. The J/27 is fun and responsive. Nothing is more exhilarating than 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 popping the J/27s 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 off the wave-tops is easy. And most importantly, this off-wind speed doesnt sacrifice
upwind performance. Going to windward in the J/27 is a dream, it has the solid, balanced 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 "feel" of a traditional keelboat. The J/27 points higher and goes faster than many 30-35
footers! footers!
One-Design Racing. Even more fun is sailing a one-design race around the buoys. The 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 J/27s 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 significant gains. Then off wind, you quickly learn to play gibe angles as the boats
acceleration gains you valuable ground on the competition. The J/27 is remarkably agile 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. 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 All-Day Comfort. Sailing past larger boats is always satisfying... especially when its
effortless and you can\t be written off as being wet and uncomfortable. Design is the effortless and you cant 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 difference. Its 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 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 and secure seating. Harken mainsheet, vang, traveler, and backstay systems; four Barient
winches; a beautiful double spreader, tapered, fractional rig spar by Hall . . . make winches; a beautiful double spreader, tapered, fractional rig spar by Hall . . . make
control and adjustment easy for crew members no matter what the wind. 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 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. family and friends sailing the J/27. Its a fun boat to sail, so everyone becomes involved.
The visibility, when steering with a responsive tiller gives the inexperienced that sense 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. of control not found when spinning a tiny wheel on small cruisers with large trunk cabins.
@@ -57,14 +57,14 @@ function getSampleData(): ReferenceEntry[] {
starboard is a comfortable quarter berth. Enough room below for a family of four or a 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. 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 Durable and Stable. The J/27s 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 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 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 grade, Lloyds 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 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 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 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. light and heavy air...something that doesnt happen with iron keels and box-like hulls.
Strong Class Strict Rules. The J/27 Class Association, owner driven and over 190 boats 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 strong, sail in North American, Midwinter, and Regional championships. A superb J/27 Class
@@ -113,7 +113,7 @@ export const useReferenceStore = defineStore('reference', {
getters: { getters: {
getCategory(state) { getCategory(state) {
(category: string) => { return (category: string) => {
return state.allItems.filter((c) => c.category === category); return state.allItems.filter((c) => c.category === category);
}; };
}, },

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 unknown as Reservation;
if (payload.$id) {
if (
response.events.includes(
'databases.*.collections.*.documents.*.delete'
)
) {
reservations.delete(payload.$id);
userReservations.delete(payload.$id);
} else {
reservations.set(payload.$id, payload);
if (payload.user === authStore.currentUser?.$id)
userReservations.set(payload.$id, payload);
}
}
}
);
// 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 unknown as Reservation)
); );
setDateLoaded(startDate, endDate, 'loaded'); setDateLoaded(startDate, endDate, 'loaded');
} catch (error) { } catch (error) {
@@ -55,7 +77,7 @@ export const useReservationStore = defineStore('reservation', () => {
AppwriteIds.collection.reservation, AppwriteIds.collection.reservation,
id id
); );
return response as Reservation; return response as unknown as Reservation;
} catch (error) { } catch (error) {
console.error('Failed to fetch reservation: ', error); console.error('Failed to fetch reservation: ', error);
} }
@@ -81,10 +103,10 @@ export const useReservationStore = defineStore('reservation', () => {
reservation reservation
); );
} }
reservations.value.set(response.$id, response as Reservation); reservations.set(response.$id, response as unknown as Reservation);
userReservations.value.set(response.$id, response as Reservation); userReservations.set(response.$id, response as unknown as Reservation);
console.info('Reservation booked: ', response); console.info('Reservation booked: ', response);
return response as Reservation; return response as unknown as Reservation;
} catch (e) { } catch (e) {
console.error('Error creating Reservation: ' + e); console.error('Error creating Reservation: ' + e);
throw e; throw 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 unknown 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

@@ -16,7 +16,7 @@ import type {
} from '../schedule.types'; } from '../schedule.types';
export const templateA: IntervalTemplate = { export const templateA: IntervalTemplate = {
id: '1', $id: '1',
name: 'WeekdayBlocks', name: 'WeekdayBlocks',
timeTuples: [ timeTuples: [
['08:00', '12:00'], ['08:00', '12:00'],
@@ -26,7 +26,7 @@ export const templateA: IntervalTemplate = {
}; };
export const templateB: IntervalTemplate = { export const templateB: IntervalTemplate = {
id: '2', $id: '2',
name: 'WeekendBlocks', name: 'WeekendBlocks',
timeTuples: [ timeTuples: [
['07:00', '10:00'], ['07:00', '10:00'],
@@ -47,7 +47,7 @@ export function getSampleIntervals(): Interval[] {
result.push( result.push(
...boats ...boats
.map((b): Interval[] => { .map((b): Interval[] => {
return template.blocks.map((t: TimeTuple): Interval => { return template.timeTuples.map((t: TimeTuple): Interval => {
return { return {
$id: 'id' + Math.random().toString(32).slice(2), $id: 'id' + Math.random().toString(32).slice(2),
resource: b.$id, resource: b.$id,
@@ -135,14 +135,13 @@ export function getSampleReservations(): Reservation[] {
return sampleData.map((entry): Reservation => { return sampleData.map((entry): Reservation => {
const boat = <Boat>boatStore.boats.find((b) => b.$id == entry.boat); const boat = <Boat>boatStore.boats.find((b) => b.$id == entry.boat);
return { return {
id: entry.id, $id: entry.id,
user: entry.user, user: entry.user,
start: date start: date
.adjustDate(now, makeOpts(splitTime(entry.start))) .adjustDate(now, makeOpts(splitTime(entry.start)))
.toISOString(), .toISOString(),
end: date.adjustDate(now, makeOpts(splitTime(entry.end))).toISOString(), end: date.adjustDate(now, makeOpts(splitTime(entry.end))).toISOString(),
resource: boat.$id, resource: boat.$id,
reservationDate: now,
reason: entry.reason, reason: entry.reason,
status: entry.status as StatusTypes, status: entry.status as StatusTypes,
comment: '', comment: '',

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 & {
@@ -23,13 +22,10 @@ export type Interval = Partial<Models.Document> & {
resource: string; resource: string;
start: string; start: string;
end: string; end: string;
user?: string;
}; };
export type IntervalTemplate = Partial<Models.Document> & { export type IntervalTemplate = Partial<Models.Document> & {
name: string; name: string;
timeTuples: TimeTuple[]; timeTuples: TimeTuple[];
}; };
export interface IntervalRecord {
[key: string]: LoadingTypes;
}

View File

@@ -1,10 +0,0 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
store: true;
}
}

View File

@@ -50,7 +50,7 @@ export const useTaskStore = defineStore('tasks', {
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collection.task AppwriteIds.collection.task
); );
this.tasks = response.documents as Task[]; this.tasks = response.documents as unknown as Task[];
} catch (error) { } catch (error) {
console.error('Failed to fetch tasks', error); console.error('Failed to fetch tasks', error);
} }
@@ -62,7 +62,7 @@ export const useTaskStore = defineStore('tasks', {
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collection.taskTags AppwriteIds.collection.taskTags
); );
this.taskTags = response.documents as TaskTag[]; this.taskTags = response.documents as unknown as TaskTag[];
} catch (error) { } catch (error) {
console.error('Failed to fetch task tags', error); console.error('Failed to fetch task tags', error);
} }
@@ -74,7 +74,7 @@ export const useTaskStore = defineStore('tasks', {
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collection.skillTags AppwriteIds.collection.skillTags
); );
this.skillTags = response.documents as SkillTag[]; this.skillTags = response.documents as unknown as SkillTag[];
} catch (error) { } catch (error) {
console.error('Failed to fetch skill tags', error); console.error('Failed to fetch skill tags', error);
} }
@@ -106,7 +106,7 @@ export const useTaskStore = defineStore('tasks', {
ID.unique(), ID.unique(),
newTask newTask
); );
this.tasks.push(response as Task); this.tasks.push(response as unknown as Task);
} catch (error) { } catch (error) {
console.error('Failed to add task:', error); console.error('Failed to add task:', error);
} }
@@ -129,7 +129,7 @@ export const useTaskStore = defineStore('tasks', {
task.$id, task.$id,
newTask newTask
); );
this.tasks.push(response as Task); this.tasks.push(response as unknown as Task);
} catch (error) { } catch (error) {
console.error('Failed to update task:', error); console.error('Failed to update task:', error);
} }

View File

@@ -0,0 +1,490 @@
# Claude Templates — On-Demand Reference
> **Do NOT read this file at session start.** Read it only when you need to write a summary, handoff, decision record, or subagent output. This file is referenced from CLAUDE.md.
---
## Template 1: Source Document Summary
**Use when:** Processing any input document (client brief, research report, requirements doc, existing proposal)
**Write to:** `./docs/summaries/source-[filename].md`
```markdown
# Source Summary: [Original Document Name]
**Processed:** [YYYY-MM-DD]
**Source Path:** [exact file path]
**Archived From:** [original path, if moved to docs/archive/]
**Document Type:** [brief / requirements / research / proposal / transcript / other]
**Confidence:** [high = I understood everything / medium = some interpretation needed / low = significant gaps]
## Exact Numbers & Metrics
<!-- List EVERY specific number, dollar amount, percentage, date, count, measurement.
Do NOT round. Do NOT paraphrase. Copy exactly as stated in source. -->
- [metric]: [exact value] (page/section reference if available)
- [metric]: [exact value]
## Key Facts (Confirmed)
<!-- Only include facts explicitly stated in the document. Tag source. -->
- [fact] — stated in [section/page]
- [fact] — stated in [section/page]
## Requirements & Constraints
<!-- Use IF/THEN/BUT/EXCEPT format to preserve conditional logic -->
- REQUIREMENT: [what is needed]
- CONDITION: [when/if this applies]
- CONSTRAINT: [limitation or exception]
- PRIORITY: [must-have / should-have / nice-to-have / stated by whom]
## Decisions Referenced
<!-- Any decisions mentioned in the document -->
- DECISION: [what was decided]
- RATIONALE: [why, as stated in document]
- ALTERNATIVES MENTIONED: [what else was considered]
- DECIDED BY: [who, if stated]
## Relationships to Other Documents
<!-- How this document connects to other known project documents -->
- SUPPORTS: [other document/decision it reinforces]
- CONTRADICTS: [other document/decision it conflicts with]
- DEPENDS ON: [other document/decision it requires]
- UPDATES: [other document/decision it supersedes]
## Open Questions & Ambiguities
<!-- Things that are NOT resolved in this document -->
- UNCLEAR: [what is ambiguous] — needs clarification from [whom]
- ASSUMED: [interpretation made] — verify with [whom]
- MISSING: [information referenced but not provided]
## Verbatim Quotes Worth Preserving
<!-- 2-5 direct quotes that capture stakeholder language, priorities, or constraints
These are critical for proposals — use the client's own words back to them -->
- "[exact quote]" — [speaker/author], [context]
```
---
## Template 2: Analysis / Research Summary
**Use when:** Completing competitive analysis, market research, technical evaluation
**Write to:** `./docs/summaries/analysis-[topic].md`
```markdown
# Analysis Summary: [Topic]
**Completed:** [YYYY-MM-DD]
**Analysis Type:** [competitive / market / technical / financial / feasibility]
**Sources Used:** [list source paths or URLs]
**Confidence:** [high / medium / low — and WHY this confidence level]
## Core Finding (One Sentence)
[Single sentence: the most important conclusion]
## Evidence Base
<!-- Specific data points supporting the finding. Exact numbers only. -->
| Data Point | Value | Source | Date of Data |
|-----------|-------|--------|-------------|
| [metric] | [exact value] | [source] | [date] |
## Detailed Findings
### Finding 1: [Name]
- WHAT: [the finding]
- SO WHAT: [why it matters for this project]
- EVIDENCE: [specific supporting data]
- CONFIDENCE: [high/medium/low]
### Finding 2: [Name]
[same structure]
## Conditional Conclusions
<!-- Use IF/THEN format -->
- IF [condition], THEN [conclusion], BECAUSE [evidence]
- IF [alternative condition], THEN [different conclusion]
## What This Analysis Does NOT Cover
<!-- Explicit scope boundaries to prevent future sessions from over-interpreting -->
- [topic not addressed and why]
- [data not available]
## Recommended Next Steps
1. [action] — priority [high/medium/low], depends on [what]
2. [action]
```
---
## Template 3: Decision Record
**Use when:** Any significant decision is made during a session
**Write to:** `./docs/summaries/decision-[number]-[topic].md`
```markdown
# Decision Record: [Short Title]
**Decision ID:** DR-[sequential number]
**Date:** [YYYY-MM-DD]
**Status:** CONFIRMED / PROVISIONAL / REQUIRES_VALIDATION
## Decision
[One clear sentence: what was decided]
## Context
[2-3 sentences: what situation prompted this decision]
## Rationale
- CHOSE [option] BECAUSE: [specific reasons with data]
- REJECTED [alternative 1] BECAUSE: [specific reasons]
- REJECTED [alternative 2] BECAUSE: [specific reasons]
## Quantified Impact
- [metric affected]: [expected change with numbers]
- [cost/time/resource implication]: [specific figures]
## Conditions & Constraints
- VALID IF: [conditions under which this decision holds]
- REVISIT IF: [triggers that should cause reconsideration]
- DEPENDS ON: [upstream decisions or facts this relies on]
## Stakeholder Input
- [name/role]: [their stated position, if known]
## Downstream Effects
- AFFECTS: [what other decisions, documents, or deliverables this impacts]
- REQUIRES UPDATE TO: [specific files or deliverables that need revision]
```
---
## Template 4: Session Handoff
**Use when:** A session is ending (context limit approaching OR phase complete)
**Write to:** `./docs/summaries/handoff-[YYYY-MM-DD]-[topic].md`
**LIFECYCLE**: After writing a new handoff, move the PREVIOUS handoff to `docs/archive/handoffs/`.
```markdown
# Session Handoff: [Topic]
**Date:** [YYYY-MM-DD]
**Session Duration:** [approximate]
**Session Focus:** [one sentence]
**Context Usage at Handoff:** [estimated percentage if known]
## What Was Accomplished
<!-- Be specific. Include file paths for every output. -->
1. [task completed] → output at `[file path]`
2. [task completed] → output at `[file path]`
## Exact State of Work in Progress
<!-- If anything is mid-stream, describe exactly where it stopped -->
- [work item]: completed through [specific point], next step is [specific action]
- [work item]: blocked on [specific issue]
## Decisions Made This Session
<!-- Reference decision records if created, otherwise summarize here -->
- DR-[number]: [decision] (see `./docs/summaries/decision-[file]`)
- [Ad-hoc decision]: [what] BECAUSE [why] — STATUS: [confirmed/provisional]
## Key Numbers Generated or Discovered This Session
<!-- Every metric, estimate, or figure produced. Exact values. -->
- [metric]: [value] — [context for where/how this was derived]
## Conditional Logic Established
<!-- Any IF/THEN/BUT/EXCEPT reasoning that future sessions must respect -->
- IF [condition] THEN [approach] BECAUSE [rationale]
## Files Created or Modified
| File Path | Action | Description |
|-----------|--------|-------------|
| `[path]` | Created | [what it contains] |
| `[path]` | Modified | [what changed and why] |
## What the NEXT Session Should Do
<!-- Ordered, specific instructions. The next session starts by reading this. -->
1. **First**: [specific action with file paths]
2. **Then**: [specific action]
3. **Then**: [specific action]
## Open Questions Requiring User Input
<!-- Do NOT proceed on these without explicit user confirmation -->
- [ ] [question] — impacts [what downstream deliverable]
- [ ] [question]
## Assumptions That Need Validation
<!-- Things treated as true this session but not confirmed -->
- ASSUMED: [assumption] — validate by [method/person]
## What NOT to Re-Read
<!-- Prevent the next session from wasting context on already-processed material -->
- `[file path]` — already summarized in `[summary file path]`
## Files to Load Next Session
<!-- Explicit index of what the next session should read. Acts as progressive disclosure index layer. -->
- `[file path]` — needed for [reason]
- `[file path]` — needed for [reason]
```
---
## Template 5: Project Brief (Initial Setup)
**Use when:** Creating the 00-project-brief.md at project start
**Write to:** `./docs/summaries/00-project-brief.md`
```markdown
# Project Brief: [Project Name]
**Created:** [YYYY-MM-DD]
**Last Updated:** [YYYY-MM-DD]
## Client
- **Name:** [client name]
- **Industry:** [industry]
- **Size:** [employee count / revenue if known]
- **Relationship:** [through AutomatonsX / Lagrange Data / direct / other]
- **Key Contacts:** [names and roles if known]
## Engagement
- **Type:** [proposal / workshop / competitive analysis / agent development / hybrid]
- **Scope:** [one paragraph description]
- **Target Deliverable:** [specific output expected]
- **Timeline:** [deadline if known]
- **Budget Context:** [if known — exact figures]
## Input Documents
| Document | Path | Processed? | Summary At |
|----------|------|-----------|------------|
| [name] | `[path]` | Yes/No | `[summary path]` |
## Success Criteria
- [criterion 1]
- [criterion 2]
## Known Constraints
- [constraint 1]
- [constraint 2]
## Project Phase Tracker
| Phase | Status | Summary File | Date |
|-------|--------|-------------|------|
| Discovery | Not Started / In Progress / Complete | `[path]` | |
| Strategy | Not Started / In Progress / Complete | `[path]` | |
| Deliverable Draft | Not Started / In Progress / Complete | `[path]` | |
| Review & Polish | Not Started / In Progress / Complete | `[path]` | |
```
---
## Template 6: Task Definition
**Use when:** Defining a discrete unit of work before starting execution
```markdown
## Task: [name]
**Date:** [YYYY-MM-DD]
**Client:** [if applicable]
**Work Type:** [proposal / workshop / analysis / content / agent development]
### Context Files to Load
- `[file path]` — [why needed]
### Action
[What to produce. Be specific about format, length, and scope.]
### Verify
- [ ] Numbers match source data exactly
- [ ] Open questions marked OPEN
- [ ] Output matches what was requested, not what was assumed
- [ ] Claims backed by specific data
- [ ] Consistent with stored decisions in docs/context/
### Done When
- [ ] Output file exists at `[specific path]`
- [ ] Summary written to `docs/summaries/[specific file]`
```
---
## Subagent Output Contracts
**CRITICAL: When subagents return results to the main agent, unstructured prose causes information loss. These output contracts define the EXACT format subagents must return.**
### Contract for Document Analysis Subagent
```
=== DOCUMENT ANALYSIS OUTPUT ===
SOURCE: [file path]
TYPE: [document type]
CONFIDENCE: [high/medium/low]
NUMBERS:
- [metric]: [exact value]
[repeat for all numbers found]
REQUIREMENTS:
- REQ: [requirement] | CONDITION: [if any] | PRIORITY: [level] | CONSTRAINT: [if any]
[repeat]
DECISIONS_REFERENCED:
- DEC: [what] | WHY: [rationale] | BY: [who]
[repeat]
CONTRADICTIONS:
- [this document says X] CONTRADICTS [other known fact Y]
[repeat or NONE]
OPEN:
- [unresolved item] | NEEDS: [who/what to resolve]
[repeat or NONE]
QUOTES:
- "[verbatim]" — [speaker], [context]
[repeat, max 5]
=== END OUTPUT ===
```
### Contract for Research/Analysis Subagent
```
=== RESEARCH OUTPUT ===
QUERY: [what was researched]
SOURCES: [list]
CONFIDENCE: [high/medium/low] BECAUSE [reason]
CORE_FINDING: [one sentence]
EVIDENCE:
- [data point]: [exact value] | SOURCE: [where] | DATE: [when]
[repeat]
CONCLUSIONS:
- IF [condition] THEN [conclusion] | EVIDENCE: [reference]
[repeat]
GAPS:
- [what was not found or not covered]
[repeat or NONE]
NEXT_STEPS:
- [recommended action] | PRIORITY: [level]
[repeat]
=== END OUTPUT ===
```
### Contract for Review/QA Subagent
```
=== REVIEW OUTPUT ===
REVIEWED: [file path or deliverable name]
AGAINST: [what standard — spec, requirements, style guide]
PASS: [yes/no/partial]
ISSUES:
- SEVERITY: [critical/major/minor] | ITEM: [description] | LOCATION: [where in document] | FIX: [suggested resolution]
[repeat]
MISSING:
- [expected content/section not found] | REQUIRED_BY: [which requirement]
[repeat or NONE]
INCONSISTENCIES:
- [item A says X] BUT [item B says Y] | RESOLUTION: [suggested]
[repeat or NONE]
STRENGTHS:
- [what works well — for positive reinforcement in iteration]
[max 3]
=== END OUTPUT ===
```
---
## Phase-Based Workflow Templates
### Template A: Enterprise Sales Deliverable
```
Phase 1: Discovery & Input Processing
├── Process all client documents → Source Document Summaries
├── Identify gaps in information → flag as OPEN items
├── Create Decision Records for any choices made
├── Write: ./docs/summaries/01-discovery-complete.md (Handoff Template)
├── → Suggest new session for Phase 2
Phase 2: Strategy & Positioning
├── Read summaries only (NOT source documents)
├── Competitive positioning analysis → Analysis Summary
├── Value proposition development
├── ROI framework construction with EXACT numbers
├── Write: ./docs/summaries/02-strategy-complete.md (Handoff Template)
├── → Suggest new session for Phase 3
Phase 3: Deliverable Creation
├── Read strategy summary + project brief only
├── Draft deliverable (proposal / deck / workshop plan)
├── Output to: ./output/deliverables/
├── Write: ./docs/summaries/03-deliverable-draft.md (Handoff Template)
├── → Suggest new session for Phase 4
Phase 4: Review & Polish
├── Read draft deliverable + strategy summary
├── Quality review using Review/QA Output Contract
├── Final edits and formatting
├── Output final version to: ./output/deliverables/
```
### Template B: Agent/Application Development
```
Phase 1: Requirements → Spec
├── Process all input documents → Source Document Summaries
├── Generate structured specification
├── Output: ./output/SPEC.md
├── Write: ./docs/summaries/01-spec-complete.md (Handoff Template)
├── → Suggest new session for Phase 2
Phase 2: Architecture → Schema
├── Read SPEC.md + summaries only
├── Design data model
├── Define agent behaviors and workflows
├── Output: ./output/schemas/data-model.yaml
├── Output: ./output/schemas/agent-definitions.yaml
├── Write: ./docs/summaries/02-architecture-complete.md (Handoff Template)
├── → Suggest new session for Phase 3
Phase 3: Prompts → Integration
├── Read schemas + spec only
├── Write system prompts for each agent
├── Map API integrations and data flows
├── Output: ./output/prompts/[agent-name].md (one per agent)
├── Output: ./output/schemas/integration-map.yaml
├── Write: ./docs/summaries/03-prompts-complete.md (Handoff Template)
├── → Suggest new session for Phase 4
Phase 4: Assembly → Package
├── Read all output files
├── Assemble complete application package
├── Generate deployment/setup instructions
├── Output: ./output/deliverables/[project]-complete-package/
├── QA check against original spec using Review/QA Output Contract
```
### Template C: Hybrid (Sales + Agent Development)
```
Phase 1: Client Discovery → Summaries
Phase 2: Solution Design → Architecture + Schema
Phase 3a: Client-Facing Deliverable (proposal/deck)
Phase 3b: Internal Technical Package (schemas/prompts)
Phase 4: Review both tracks against each other for consistency
```
---
## End of Templates
**Return to your task after reading the template(s) you need. Do not keep this file in active context.**

View File

@@ -1,6 +1,3 @@
{ {
"extends": "@quasar/app-vite/tsconfig-preset", "extends": "./.quasar/tsconfig.json"
"compilerOptions": {
"baseUrl": "."
}
} }

0
v1
View File

18491
yarn.lock

File diff suppressed because it is too large Load Diff