5 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 1m14s
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
49 changed files with 14440 additions and 8991 deletions

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

@@ -5,6 +5,7 @@ on:
branches: branches:
- main - main
- alpha - alpha
- devel
jobs: jobs:
build: build:
@@ -17,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

11
.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*
@@ -37,7 +43,10 @@ yarn-error.log*
.env* .env*
# version file # version file
src/version.js src/version.ts
VERSION VERSION
release-*.gz release-*.gz
CHANGELOG.md CHANGELOG.md
# Quasar cruft
/quasar.config.*.temporary.compiled*

View File

@@ -3,6 +3,7 @@
"main", "main",
"next", "next",
{ "name": "beta", "prerelease": true }, { "name": "beta", "prerelease": true },
{ "name": "devel", "prerelease": true },
{ "name": "alpha", "prerelease": true } { "name": "alpha", "prerelease": true }
], ],
"plugins": [ "plugins": [

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

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,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,
);

View File

@@ -9,7 +9,7 @@ try {
// Create version content // Create version content
const versionContent = `export const APP_VERSION = '${version}';\n`; const versionContent = `export const APP_VERSION = '${version}';\n`;
const versionTxtFilePath = path.resolve(__dirname, './VERSION'); const versionTxtFilePath = path.resolve(__dirname, './VERSION');
const versionFilePath = path.resolve(__dirname, 'src/version.js'); const versionFilePath = path.resolve(__dirname, 'src/version.ts');
// Write version to TXT file // Write version to TXT file
fs.writeFileSync(versionTxtFilePath, version, 'utf8'); fs.writeFileSync(versionTxtFilePath, version, 'utf8');

View File

@@ -5,60 +5,64 @@
"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": {
"generate-version": "node generate-version.js", "generate-version": "node generate-version.cjs",
"lint": "eslint --ext .js,.ts,.vue ./", "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": "npm run generate-version && quasar dev -m pwa", "dev": "yarn generate-version && quasar dev -m pwa",
"build": "npm run generate-version && quasar build -m pwa" "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",
"@quasar/app-vite": "^2.4.1",
"@saithodev/semantic-release-gitea": "^2.1.0", "@saithodev/semantic-release-gitea": "^2.1.0",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3", "@semantic-release/exec": "^7.1.0",
"@semantic-release/github": "^10.0.6", "@semantic-release/github": "^12.0.6",
"@semantic-release/npm": "^12.0.1", "@semantic-release/npm": "^13.1.5",
"@types/node": "^12.20.21", "@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^5.10.0", "autoprefixer": "^10.4.27",
"@typescript-eslint/parser": "^5.10.0", "dotenv": "^17.3.1",
"autoprefixer": "^10.4.2", "eslint": "^9",
"dotenv": "^16.3.1", "eslint-config-prettier": "^10.1.8",
"eslint": "^8.10.0", "eslint-plugin-vue": "^9",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^9.0.0",
"git-commit-info": "^2.0.2", "git-commit-info": "^2.0.2",
"prettier": "^2.5.1", "globals": "^17.4.0",
"quasar": "^2.16.0", "prettier": "^3.8.1",
"semantic-release": "^24.0.0", "quasar": "^2.18.6",
"typescript": "~5.3.0", "semantic-release": "^25.0.3",
"vite-plugin-checker": "^0.6.4", "typescript": "^5.9.3",
"vue-tsc": "^1.8.22", "typescript-eslint": "^8.57.0",
"workbox-build": "^7.0.0", "vite-plugin-checker": "^0.12.0",
"workbox-cacheable-response": "^7.0.0", "vue-eslint-parser": "^10.4.0",
"workbox-core": "^7.0.0", "vue-tsc": "^3.2.5",
"workbox-expiration": "^7.0.0", "workbox-build": "^7.4.0",
"workbox-precaching": "^7.0.0", "workbox-cacheable-response": "^7.4.0",
"workbox-routing": "^7.0.0", "workbox-core": "^7.4.0",
"workbox-strategies": "^7.0.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"
} }

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 () { 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,
@@ -79,7 +63,8 @@ module.exports = configure(function () {
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 },
@@ -96,13 +81,13 @@ module.exports = configure(function () {
// 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/, ''),
}, },
'/api/v1/realtime': { '/api/v1/realtime': {
target: 'wss://apidev.bab.toal.ca', target: 'wss://appwrite.toal.ca',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''),
secure: false, secure: false,
@@ -125,9 +110,7 @@ module.exports = configure(function () {
// 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
@@ -189,7 +172,7 @@ module.exports = configure(function () {
// 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',
@@ -240,8 +223,6 @@ module.exports = configure(function () {
// 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,4 +1,4 @@
import { boot } from 'quasar/wrappers'; import { defineBoot } from '#q-app/wrappers';
import { import {
Client, Client,
Account, Account,
@@ -21,7 +21,7 @@ if (API_ENDPOINT && API_PROJECT) {
client.setEndpoint(API_ENDPOINT).setProject(API_PROJECT); client.setEndpoint(API_ENDPOINT).setProject(API_PROJECT);
} else { } else {
console.error( console.error(
'Must configure VITE_APPWRITE_API_ENDPOINT and VITE_APPWRITE_API_PROJECT' 'Must configure VITE_APPWRITE_API_ENDPOINT and VITE_APPWRITE_API_PROJECT',
); );
} }
@@ -43,13 +43,8 @@ type AppwriteIDConfig = {
let AppwriteIds = <AppwriteIDConfig>{}; let AppwriteIds = <AppwriteIDConfig>{};
console.log(API_ENDPOINT);
if (
API_ENDPOINT === 'https://apidev.bab.toal.ca/v1' ||
API_ENDPOINT === 'http://localhost:4000/api/v1'
) {
AppwriteIds = { AppwriteIds = {
databaseId: '65ee1cbf9c2493faf15f', databaseId: 'bab_prod',
collection: { collection: {
boat: 'boat', boat: 'boat',
reservation: 'reservation', reservation: 'reservation',
@@ -63,23 +58,6 @@ if (
userinfo: 'userinfo', userinfo: 'userinfo',
}, },
}; };
} else if (API_ENDPOINT === 'https://appwrite.oys.undock.ca/v1') {
AppwriteIds = {
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);
const databases = new Databases(client); const databases = new Databases(client);
@@ -88,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();

View File

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

View File

@@ -1,19 +0,0 @@
<template>
<q-btn
@click="auth.googleLogin()"
style="width: 300px">
<q-avatar
left
class="q-ma-xs"
size="sm">
<img
src="https://lh3.googleusercontent.com/COxitqgJr1sJnIDe8-jiKhxDx1FrYbtRHKJ9z_hELisAlapwE9LUPh6fcXIfb5vwpbMl4xl9H9TRFPc5NOO8Sb3VSgIBrfRYvW6cUA" />
</q-avatar>
<div>Login with Google</div>
</q-btn>
</template>
<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

@@ -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 {
// //
} }
} }
@@ -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,28 +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>v{{ APP_VERSION }}</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 LeftDrawer from 'components/LeftDrawer.vue';
import { APP_VERSION } from 'src/version'; import { APP_VERSION } from 'src/version';
const leftDrawerOpen = ref(false);
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
defineProps({ defineProps({
pageTitle: String, pageTitle: String,
}); });
defineEmits(['drawer-toggle']);
</script> </script>

View File

@@ -4,7 +4,7 @@
class="row"> class="row">
<q-card <q-card
v-for="boat in boats" v-for="boat in boats"
:key="boat.id" :key="boat.$id"
class="q-ma-sm col-xs-12 col-sm-6 col-xl-3"> class="q-ma-sm col-xs-12 col-sm-6 col-xl-3">
<q-card-section> <q-card-section>
<q-img <q-img

View File

@@ -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,

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,6 +1,22 @@
<template> <template>
<q-layout view="hHh Lpr fFf"> <q-layout view="hHh Lpr fFf">
<ToolbarComponent :pageTitle="route.meta.title as string" /> <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>
@@ -11,13 +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 { useRoute } from 'vue-router';
import BottomNavComponent from 'src/components/BottomNavComponent.vue'; import BottomNavComponent from 'src/components/BottomNavComponent.vue';
import ToolbarComponent from 'src/components/ToolbarComponent.vue'; import LeftDrawer from 'src/components/LeftDrawer.vue';
import { APP_VERSION } from 'src/version';
const q = useQuasar(); const q = useQuasar();
const route = useRoute(); 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

@@ -34,24 +34,20 @@
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<div class="row justify-center q-ma-sm"> <div class="row justify-center q-ma-sm">
<q-btn <q-btn
v-if="!userId"
type="button" type="button"
@click="doTokenLogin" @click="sendMagicLink"
color="primary" color="secondary"
label="Login with E-mail" label="Send Magic Link"
style="width: 300px" /> style="width: 300px" />
</div> </div>
<div class="row justify-center q-ma-sm"> <div class="row justify-center q-ma-sm">
<GoogleOauthComponent />
</div>
<div class="row justify-center q-ma-sm">
<DiscordOauthComponent />
</div>
<div class="row justify-center">
<q-btn <q-btn
flat type="button"
color="secondary" @click="doTokenLogin"
to="/pwreset" color="primary"
label="Forgot Password?" /> :label="userId ? 'Login' : 'Send Code'"
style="width: 300px" />
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -76,21 +72,54 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, onMounted } from 'vue';
import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
import DiscordOauthComponent from 'src/components/DiscordOauthComponent.vue';
import { Dialog, Notify } from 'quasar'; import { Dialog, Notify } from 'quasar';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { AppwriteException } from 'appwrite'; import { AppwriteException } from 'appwrite';
import { APP_VERSION } from 'src/version.js';
const email = ref(''); const email = ref('');
const token = ref(''); const token = ref('');
const userId = ref(); const userId = ref();
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore();
console.log('version:' + APP_VERSION); onMounted(async () => {
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 doTokenLogin = async () => {
const authStore = useAuthStore(); const authStore = useAuthStore();
@@ -99,7 +128,7 @@ const doTokenLogin = async () => {
const sessionToken = await authStore.createTokenSession(email.value); const sessionToken = await authStore.createTokenSession(email.value);
userId.value = sessionToken.userId; userId.value = sessionToken.userId;
Dialog.create({ message: 'Check your e-mail for your login code.' }); Dialog.create({ message: 'Check your e-mail for your login code.' });
} catch (e) { } catch {
Dialog.create({ Dialog.create({
message: 'An error occurred. Please ask for help in Discord', message: 'An error occurred. Please ask for help in Discord',
}); });

View File

@@ -61,7 +61,7 @@ import { useAuthStore } from 'src/stores/auth';
import NewPasswordComponent from 'src/components/NewPasswordComponent.vue'; import NewPasswordComponent from 'src/components/NewPasswordComponent.vue';
import { Dialog } from 'quasar'; import { Dialog } from 'quasar';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { APP_VERSION } from 'src/version.js'; import { APP_VERSION } from 'src/version';
const email = ref(''); const email = ref('');
const password = ref(''); const password = ref('');

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)
@@ -132,7 +132,11 @@ 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();
}
} }
const boatReservationEvents = ( const boatReservationEvents = (
timestamp: Timestamp, timestamp: Timestamp,

View File

@@ -37,7 +37,7 @@
<template <template
v-for="block in sortedIntervals(scope.timestamp, scope.resource) v-for="block in sortedIntervals(scope.timestamp, scope.resource)
.value" .value"
: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') }}

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'

View File

@@ -1,6 +1,6 @@
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 { useBoatStore } from './boat';
import { useReservationStore } from './reservation'; import { useReservationStore } from './reservation';
@@ -49,22 +49,12 @@ export const useAuthStore = defineStore('auth', () => {
return await account.createEmailToken(ID.unique(), email); return await account.createEmailToken(ID.unique(), email);
} }
async function googleLogin() { async function createMagicURLSession(email: string) {
await account.createOAuth2Session( return await account.createMagicURLToken(
OAuthProvider.Google, ID.unique(),
'https://oys.undock.ca', email,
'https://oys.undock.ca/login' window.location.origin + '/login'
); );
await init();
}
async function discordLogin() {
await account.createOAuth2Session(
OAuthProvider.Discord,
'https://oys.undock.ca',
'https://oys.undock.ca/login'
);
await init();
} }
async function tokenLogin(userId: string, token: string) { async function tokenLogin(userId: string, token: string) {
@@ -72,6 +62,11 @@ export const useAuthStore = defineStore('auth', () => {
await init(); 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 {
if (!id) return 'No User'; if (!id) return 'No User';
try { try {
@@ -115,10 +110,10 @@ export const useAuthStore = defineStore('auth', () => {
register, register,
updateName, updateName,
login, login,
googleLogin,
discordLogin,
createTokenSession, createTokenSession,
createMagicURLSession,
tokenLogin, 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

@@ -22,7 +22,7 @@ export const useIntervalStore = defineStore('interval', () => {
realtimeStore.register( realtimeStore.register(
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.interval}.documents`, `databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.interval}.documents`,
(response) => { (response) => {
const payload = response.payload as Interval; const payload = response.payload as unknown as Interval;
if (!payload.$id) return; if (!payload.$id) return;
if ( if (
response.events.includes('databases.*.collections.*.documents.*.delete') response.events.includes('databases.*.collections.*.documents.*.delete')
@@ -92,7 +92,7 @@ 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)
); );
dateStatus.value.set(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`);
@@ -110,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);
} }
@@ -124,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');

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);

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

View File

@@ -22,7 +22,7 @@ export const useReservationStore = defineStore('reservation', () => {
realtimeStore.register( realtimeStore.register(
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.reservation}.documents`, `databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.reservation}.documents`,
(response) => { (response) => {
const payload = response.payload as Reservation; const payload = response.payload as unknown as Reservation;
if (payload.$id) { if (payload.$id) {
if ( if (
response.events.includes( response.events.includes(
@@ -62,7 +62,7 @@ export const useReservationStore = defineStore('reservation', () => {
); );
response.documents.forEach((d) => response.documents.forEach((d) =>
reservations.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) {
@@ -77,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);
} }
@@ -103,10 +103,10 @@ export const useReservationStore = defineStore('reservation', () => {
reservation reservation
); );
} }
reservations.set(response.$id, response as Reservation); reservations.set(response.$id, response as unknown as Reservation);
userReservations.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;
@@ -244,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.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);

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

@@ -22,6 +22,7 @@ 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> & {

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

@@ -1 +0,0 @@
export const APP_VERSION = '0.0.0';

View File

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

21458
yarn.lock

File diff suppressed because it is too large Load Diff