Compare commits
43 Commits
475ba45248
...
v0.7.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
148b8ff49d
|
|||
|
c4113f63a4
|
|||
|
6274e4936d
|
|||
|
e1259688a4
|
|||
|
e2a4dd851d
|
|||
|
2a61cc105f
|
|||
|
d6f58ddabd
|
|||
|
a1d9e147f9
|
|||
|
92bfc7bafa
|
|||
|
6f61edd659
|
|||
|
ea4e848e57
|
|||
|
c08fa6c2d8
|
|||
|
01aae9e8ff
|
|||
|
70c6837858
|
|||
|
6167a713dd
|
|||
|
ab6b909fba
|
|||
|
9fdab2acc9
|
|||
|
68c242ae81
|
|||
|
cb3c1ab05f
|
|||
|
02dae967a2
|
|||
|
77ae081031
|
|||
|
aed60cc0d5
|
|||
|
278c7309b7
|
|||
|
a11b2a0568
|
|||
|
ff8e54449a
|
|||
|
64a59e856f
|
|||
|
5e8c5a1631
|
|||
|
e97949cab3
|
|||
|
b7a3608e67
|
|||
|
bbb544c029
|
|||
|
da42f6ed22
|
|||
|
8016e20451
|
|||
|
64ee8f4fea
|
|||
|
17e8d7dc37
|
|||
|
a409b0a5c7
|
|||
|
6ec4a1e025
|
|||
|
d063b0cf0d
|
|||
|
643d74e29d
|
|||
|
1526a10630
|
|||
|
fc035106d0
|
|||
|
8ae855838b
|
|||
|
9bd10b56d9
|
|||
|
1a78f82c5e
|
8
.claude/commands/handoff.md
Normal file
8
.claude/commands/handoff.md
Normal 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.
|
||||||
13
.claude/commands/process-doc.md
Normal file
13
.claude/commands/process-doc.md
Normal 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.
|
||||||
13
.claude/commands/status.md
Normal file
13
.claude/commands/status.md
Normal 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)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
VITE_APPWRITE_API_ENDPOINT='https://appwrite.oys.undock.ca/v1'
|
|
||||||
VITE_APPWRITE_API_PROJECT='bab'
|
|
||||||
@@ -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" }'
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -34,4 +34,10 @@ yarn-error.log*
|
|||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
# local .env files
|
# local .env files
|
||||||
.env.local*
|
.env*
|
||||||
|
|
||||||
|
# version file
|
||||||
|
src/version.js
|
||||||
|
VERSION
|
||||||
|
release-*.gz
|
||||||
|
CHANGELOG.md
|
||||||
|
|||||||
27
.releaserc.json
Normal file
27
.releaserc.json
Normal 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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
51
CLAUDE.md
Normal file
51
CLAUDE.md
Normal 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.
|
||||||
16
docs/context/archive-rules.md
Normal file
16
docs/context/archive-rules.md
Normal 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.
|
||||||
23
docs/context/processing-protocol.md
Normal file
23
docs/context/processing-protocol.md
Normal 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.
|
||||||
18
docs/context/subagent-rules.md
Normal file
18
docs/context/subagent-rules.md
Normal 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.
|
||||||
22
generate-version.js
Normal file
22
generate-version.js
Normal 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.js');
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
14
package.json
14
package.json
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "oys_bab",
|
"name": "oys_bab",
|
||||||
"version": "0.6.1",
|
"version": "0.0.0",
|
||||||
"description": "Manage a Borrow a Boat program for a Yacht Club",
|
"description": "Manage a Borrow a Boat program for a Yacht Club",
|
||||||
"productName": "OYS Borrow a Boat",
|
"productName": "OYS Borrow a Boat",
|
||||||
"author": "Patrick Toal <ptoal@takeflight.ca>",
|
"author": "Patrick Toal <ptoal@takeflight.ca>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"generate-version": "node generate-version.js",
|
||||||
"lint": "eslint --ext .js,.ts,.vue ./",
|
"lint": "eslint --ext .js,.ts,.vue ./",
|
||||||
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||||
"test": "echo \"No test specified\" && exit 0",
|
"test": "echo \"No test specified\" && exit 0",
|
||||||
"dev": "quasar dev",
|
"dev": "npm run generate-version && quasar dev -m pwa",
|
||||||
"build": "quasar build"
|
"build": "npm run generate-version && quasar build -m pwa"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.11",
|
"@quasar/extras": "^1.16.11",
|
||||||
@@ -26,6 +27,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.9.1",
|
"@quasar/app-vite": "^1.9.1",
|
||||||
|
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/exec": "^6.0.3",
|
||||||
|
"@semantic-release/github": "^10.0.6",
|
||||||
|
"@semantic-release/npm": "^12.0.1",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^12.20.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
@@ -34,8 +40,10 @@
|
|||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
|
"git-commit-info": "^2.0.2",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"quasar": "^2.16.0",
|
"quasar": "^2.16.0",
|
||||||
|
"semantic-release": "^24.0.0",
|
||||||
"typescript": "~5.3.0",
|
"typescript": "~5.3.0",
|
||||||
"vite-plugin-checker": "^0.6.4",
|
"vite-plugin-checker": "^0.6.4",
|
||||||
"vue-tsc": "^1.8.22",
|
"vue-tsc": "^1.8.22",
|
||||||
|
|||||||
@@ -9,9 +9,8 @@
|
|||||||
// 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');
|
const { configure } = require('quasar/wrappers');
|
||||||
const packageJson = require('./package.json');
|
|
||||||
|
|
||||||
module.exports = configure(function (/* ctx */) {
|
module.exports = configure(function () {
|
||||||
return {
|
return {
|
||||||
eslint: {
|
eslint: {
|
||||||
// fix: true,
|
// fix: true,
|
||||||
@@ -49,9 +48,6 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||||
build: {
|
build: {
|
||||||
env: {
|
|
||||||
VUE_APP_VERSION: packageJson.version,
|
|
||||||
},
|
|
||||||
target: {
|
target: {
|
||||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
node: 'node16',
|
node: 'node16',
|
||||||
@@ -105,12 +101,19 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
},
|
},
|
||||||
'/function': {
|
'/api/v1/realtime': {
|
||||||
target: 'https://6640382951eacb568371.f.appwrite.toal.ca/',
|
target: 'wss://apidev.bab.toal.ca',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/function/, ''),
|
ws: true,
|
||||||
},
|
},
|
||||||
|
// '/function': {
|
||||||
|
// target: 'https://6640382951eacb568371.f.appwrite.toal.ca/',
|
||||||
|
// changeOrigin: true,
|
||||||
|
// secure: false,
|
||||||
|
// rewrite: (path) => path.replace(/^\/function/, ''),
|
||||||
|
// },
|
||||||
},
|
},
|
||||||
// For reverse-proxying via haproxy
|
// For reverse-proxying via haproxy
|
||||||
// hmr: {
|
// hmr: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -14,39 +14,41 @@ import type { Router } from 'vue-router';
|
|||||||
|
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
|
|
||||||
// let VITE_APPWRITE_API_ENDPOINT, VITE_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 (
|
client.setEndpoint(API_ENDPOINT).setProject(API_PROJECT);
|
||||||
// process.env.VITE_APPWRITE_API_ENDPOINT &&
|
|
||||||
// process.env.VITE_APPWRITE_API_PROJECT
|
|
||||||
// ) {
|
|
||||||
const VITE_APPWRITE_API_ENDPOINT = import.meta.env.VITE_APPWRITE_API_ENDPOINT;
|
|
||||||
const VITE_APPWRITE_API_PROJECT = import.meta.env.VITE_APPWRITE_API_PROJECT;
|
|
||||||
// }
|
|
||||||
// } else if (process.env.DEV) {
|
|
||||||
// VITE_APPWRITE_API_ENDPOINT = 'http://localhost:4000/api/v1';
|
|
||||||
// VITE_APPWRITE_API_PROJECT = '65ede55a213134f2b688';
|
|
||||||
// } else {
|
|
||||||
// VITE_APPWRITE_API_ENDPOINT = 'https://appwrite.oys.undock.ca/v1';
|
|
||||||
// VITE_APPWRITE_API_PROJECT = 'bab';
|
|
||||||
// }
|
|
||||||
if (VITE_APPWRITE_API_ENDPOINT && VITE_APPWRITE_API_PROJECT) {
|
|
||||||
client
|
|
||||||
.setEndpoint(VITE_APPWRITE_API_ENDPOINT)
|
|
||||||
.setProject(VITE_APPWRITE_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'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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>{};
|
||||||
? {
|
|
||||||
|
console.log(API_ENDPOINT);
|
||||||
|
if (
|
||||||
|
API_ENDPOINT === 'https://apidev.bab.toal.ca/v1' ||
|
||||||
|
API_ENDPOINT === 'http://localhost:4000/api/v1'
|
||||||
|
) {
|
||||||
|
AppwriteIds = {
|
||||||
databaseId: '65ee1cbf9c2493faf15f',
|
databaseId: '65ee1cbf9c2493faf15f',
|
||||||
collection: {
|
collection: {
|
||||||
boat: 'boat',
|
boat: 'boat',
|
||||||
@@ -60,8 +62,9 @@ const AppwriteIds = process.env.DEV
|
|||||||
function: {
|
function: {
|
||||||
userinfo: 'userinfo',
|
userinfo: 'userinfo',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
: {
|
} else if (API_ENDPOINT === 'https://appwrite.oys.undock.ca/v1') {
|
||||||
|
AppwriteIds = {
|
||||||
databaseId: 'bab_prod',
|
databaseId: 'bab_prod',
|
||||||
collection: {
|
collection: {
|
||||||
boat: 'boat',
|
boat: 'boat',
|
||||||
@@ -76,6 +79,7 @@ const AppwriteIds = process.env.DEV
|
|||||||
userinfo: '664038294b5473ef0c8d',
|
userinfo: '664038294b5473ef0c8d',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const account = new Account(client);
|
const account = new Account(client);
|
||||||
const databases = new Databases(client);
|
const databases = new Databases(client);
|
||||||
@@ -132,6 +136,7 @@ async function login(email: string, password: string) {
|
|||||||
});
|
});
|
||||||
appRouter.replace({ name: 'index' });
|
appRouter.replace({ name: 'index' });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
console.log(error);
|
||||||
if (error instanceof AppwriteException) {
|
if (error instanceof AppwriteException) {
|
||||||
if (error.type === 'user_session_already_exists') {
|
if (error.type === 'user_session_already_exists') {
|
||||||
appRouter.replace({ name: 'index' });
|
appRouter.replace({ name: 'index' });
|
||||||
@@ -159,7 +164,7 @@ async function login(email: string, password: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resetPassword(email: string) {
|
async function resetPassword(email: string) {
|
||||||
await account.createRecovery(email, pwresetUrl);
|
await account.createRecovery(email, window.location.origin + '/pwreset');
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
19
src/components/DiscordOauthComponent.vue
Normal file
19
src/components/DiscordOauthComponent.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<q-btn
|
||||||
|
@click="auth.discordLogin()"
|
||||||
|
style="width: 300px">
|
||||||
|
<q-avatar
|
||||||
|
left
|
||||||
|
size="sm"
|
||||||
|
class="q-ma-xs">
|
||||||
|
<img
|
||||||
|
src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/636e0a6a49cf127bf92de1e2_icon_clyde_blurple_RGB.png" />
|
||||||
|
</q-avatar>
|
||||||
|
Login with Discord
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
const auth = useAuthStore();
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="auth.googleLogin()">Login with Google</div>
|
<q-btn
|
||||||
|
@click="auth.googleLogin()"
|
||||||
|
style="width: 300px">
|
||||||
|
<q-avatar
|
||||||
|
left
|
||||||
|
class="q-ma-xs"
|
||||||
|
size="sm">
|
||||||
|
<img
|
||||||
|
src="https://lh3.googleusercontent.com/COxitqgJr1sJnIDe8-jiKhxDx1FrYbtRHKJ9z_hELisAlapwE9LUPh6fcXIfb5vwpbMl4xl9H9TRFPc5NOO8Sb3VSgIBrfRYvW6cUA" />
|
||||||
|
</q-avatar>
|
||||||
|
<div>Login with Google</div>
|
||||||
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
62
src/components/NewPasswordComponent.vue
Normal file
62
src/components/NewPasswordComponent.vue
Normal 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>
|
||||||
@@ -184,7 +184,7 @@ function getEvents(scope: ResourceIntervalScope) {
|
|||||||
scope.resource.$id
|
scope.resource.$id
|
||||||
);
|
);
|
||||||
|
|
||||||
return resourceEvents.map((event) => {
|
return resourceEvents.value.map((event) => {
|
||||||
return {
|
return {
|
||||||
left: scope.timeStartPosX(parsed(event.start)),
|
left: scope.timeStartPosX(parsed(event.start)),
|
||||||
width: scope.timeDurationWidth(
|
width: scope.timeDurationWidth(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<q-toolbar-title>{{ pageTitle }}</q-toolbar-title>
|
<q-toolbar-title>{{ pageTitle }}</q-toolbar-title>
|
||||||
<q-space />
|
<q-space />
|
||||||
<div>v{{ VERSION }}</div>
|
<div>v{{ APP_VERSION }}</div>
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
<LeftDrawer
|
<LeftDrawer
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import LeftDrawer from 'components/LeftDrawer.vue';
|
import LeftDrawer from 'components/LeftDrawer.vue';
|
||||||
|
import { APP_VERSION } from 'src/version';
|
||||||
const VERSION = process.env.VUE_APP_VERSION;
|
|
||||||
|
|
||||||
const leftDrawerOpen = ref(false);
|
const leftDrawerOpen = ref(false);
|
||||||
function toggleLeftDrawer() {
|
function toggleLeftDrawer() {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
v-for="block in getAvailableIntervals(
|
v-for="block in getAvailableIntervals(
|
||||||
scope.timestamp,
|
scope.timestamp,
|
||||||
boats[scope.columnIndex]
|
boats[scope.columnIndex]
|
||||||
)"
|
).value"
|
||||||
:key="block.$id">
|
:key="block.$id">
|
||||||
<div
|
<div
|
||||||
class="timeblock"
|
class="timeblock"
|
||||||
@@ -207,7 +207,7 @@ function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
|
|||||||
const boatReservations = computed((): Record<string, Reservation[]> => {
|
const boatReservations = computed((): Record<string, Reservation[]> => {
|
||||||
return reservationStore
|
return reservationStore
|
||||||
.getReservationsByDate(selectedDate.value)
|
.getReservationsByDate(selectedDate.value)
|
||||||
.reduce((result, reservation) => {
|
.value.reduce((result, reservation) => {
|
||||||
if (!result[reservation.resource]) result[reservation.resource] = [];
|
if (!result[reservation.resource]) result[reservation.resource] = [];
|
||||||
result[reservation.resource].push(reservation);
|
result[reservation.resource].push(reservation);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-layout view="hHh Lpr fFf">
|
<q-layout view="hHh Lpr fFf">
|
||||||
|
<ToolbarComponent :pageTitle="route.meta.title as string" />
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<router-view />
|
<router-view />
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
@@ -11,9 +12,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
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 ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
|
const route = useRoute();
|
||||||
// q.fullscreen.request();
|
// q.fullscreen.request();
|
||||||
q.addressbarColor.set('#14539a');
|
q.addressbarColor.set('#14539a');
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="col text-h6">Log in</div>
|
<div class="col text-h6">Log in</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-form>
|
<q-form @keydown.enter.prevent="doTokenLogin">
|
||||||
<q-card-section class="q-gutter-md">
|
<q-card-section class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
@@ -23,35 +23,37 @@
|
|||||||
color="darkblue"
|
color="darkblue"
|
||||||
filled></q-input>
|
filled></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="password"
|
v-if="userId"
|
||||||
label="Password"
|
v-model="token"
|
||||||
type="password"
|
label="6-digit code"
|
||||||
|
type="number"
|
||||||
color="darkblue"
|
color="darkblue"
|
||||||
filled></q-input>
|
filled></q-input>
|
||||||
<q-card-actions>
|
|
||||||
<q-btn
|
|
||||||
type="button"
|
|
||||||
@click="doLogin"
|
|
||||||
label="Login"
|
|
||||||
color="primary"></q-btn>
|
|
||||||
<q-space />
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
color="secondary"
|
|
||||||
to="/pwreset">
|
|
||||||
Reset password
|
|
||||||
</q-btn>
|
|
||||||
<!-- <q-btn
|
|
||||||
type="button"
|
|
||||||
@click="register"
|
|
||||||
color="secondary"
|
|
||||||
label="Register"
|
|
||||||
flat
|
|
||||||
></q-btn> -->
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-form>
|
</q-form>
|
||||||
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
|
<q-card-section class="q-pa-none">
|
||||||
|
<div class="row justify-center q-ma-sm">
|
||||||
|
<q-btn
|
||||||
|
type="button"
|
||||||
|
@click="doTokenLogin"
|
||||||
|
color="primary"
|
||||||
|
label="Login with E-mail"
|
||||||
|
style="width: 300px" />
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center q-ma-sm">
|
||||||
|
<GoogleOauthComponent />
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center q-ma-sm">
|
||||||
|
<DiscordOauthComponent />
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="secondary"
|
||||||
|
to="/pwreset"
|
||||||
|
label="Forgot Password?" />
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
@@ -75,13 +77,77 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { login } from 'boot/appwrite';
|
import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
|
||||||
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
|
import DiscordOauthComponent from 'src/components/DiscordOauthComponent.vue';
|
||||||
|
import { Dialog, Notify } from 'quasar';
|
||||||
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { AppwriteException } from 'appwrite';
|
||||||
|
import { APP_VERSION } from 'src/version.js';
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const password = ref('');
|
const token = ref('');
|
||||||
|
const userId = ref();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const doLogin = async () => {
|
console.log('version:' + APP_VERSION);
|
||||||
login(email.value, password.value);
|
|
||||||
|
const doTokenLogin = async () => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
if (!userId.value) {
|
||||||
|
try {
|
||||||
|
const sessionToken = await authStore.createTokenSession(email.value);
|
||||||
|
userId.value = sessionToken.userId;
|
||||||
|
Dialog.create({ message: 'Check your e-mail for your login code.' });
|
||||||
|
} catch (e) {
|
||||||
|
Dialog.create({
|
||||||
|
message: 'An error occurred. Please ask for help in Discord',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const notification = Notify.create({
|
||||||
|
type: 'primary',
|
||||||
|
position: 'top',
|
||||||
|
spinner: true,
|
||||||
|
message: 'Logging you in...',
|
||||||
|
timeout: 8000,
|
||||||
|
group: false,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await authStore.tokenLogin(userId.value, token.value);
|
||||||
|
notification({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Logged in!',
|
||||||
|
timeout: 2000,
|
||||||
|
spinner: false,
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
router.replace({ name: 'index' });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AppwriteException) {
|
||||||
|
if (error.type === 'user_session_already_exists') {
|
||||||
|
useRouter().replace({ name: 'index' });
|
||||||
|
notification({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Already Logged in!',
|
||||||
|
timeout: 2000,
|
||||||
|
spinner: false,
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Dialog.create({
|
||||||
|
title: 'Login Error!',
|
||||||
|
message: error.message,
|
||||||
|
persistent: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
notification({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Login failed.',
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
87
src/pages/SignupPage.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<q-layout>
|
||||||
|
<q-page-container>
|
||||||
|
<q-page class="flex bg-image flex-center">
|
||||||
|
<q-card
|
||||||
|
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }">
|
||||||
|
<q-card-section>
|
||||||
|
<q-img
|
||||||
|
fit="scale-down"
|
||||||
|
src="~assets/oysqn_logo.png" />
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-center q-pt-sm">
|
||||||
|
<div class="col text-h6">Sign Up</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-form>
|
||||||
|
<q-card-section class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
v-model="email"
|
||||||
|
label="E-Mail"
|
||||||
|
type="email"
|
||||||
|
color="darkblue"
|
||||||
|
:rules="['email']"
|
||||||
|
filled></q-input>
|
||||||
|
<NewPasswordComponent v-model="password" />
|
||||||
|
<q-card-actions>
|
||||||
|
<q-space />
|
||||||
|
<q-btn
|
||||||
|
type="button"
|
||||||
|
@click="doRegister"
|
||||||
|
label="Sign Up"
|
||||||
|
color="primary"></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card-section>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-image {
|
||||||
|
background-image: url('/src/assets/oys_lighthouse.jpg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position-x: center;
|
||||||
|
background-size: cover;
|
||||||
|
/* background-image: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#ed232a 0%,
|
||||||
|
#ffffff 75%,
|
||||||
|
#14539a 100%
|
||||||
|
); */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
import NewPasswordComponent from 'src/components/NewPasswordComponent.vue';
|
||||||
|
import { Dialog } from 'quasar';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { APP_VERSION } from 'src/version.js';
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
console.log('version:' + APP_VERSION);
|
||||||
|
|
||||||
|
const doRegister = async () => {
|
||||||
|
if (email.value && password.value) {
|
||||||
|
try {
|
||||||
|
await useAuthStore().register(email.value, password.value);
|
||||||
|
Dialog.create({
|
||||||
|
message: 'Account Created! Now log-in with your e-mail / password.',
|
||||||
|
}).onOk(() => router.replace('/login'));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
Dialog.create({
|
||||||
|
message: 'An error occurred. Please ask for support in Discord',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const currentUser = useAuthStore().currentUser;
|
|||||||
|
|
||||||
const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => {
|
const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => {
|
||||||
return getAvailableIntervals(timestamp, boat)
|
return getAvailableIntervals(timestamp, boat)
|
||||||
.concat(boatReservationEvents(timestamp, boat))
|
.value.concat(boatReservationEvents(timestamp, boat))
|
||||||
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
||||||
};
|
};
|
||||||
// Method declarations
|
// Method declarations
|
||||||
@@ -134,16 +134,16 @@ const createReservationFromInterval = (interval: Interval | Reservation) => {
|
|||||||
function handleSwipe({ ...event }) {
|
function handleSwipe({ ...event }) {
|
||||||
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
|
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
|
||||||
}
|
}
|
||||||
function boatReservationEvents(
|
const boatReservationEvents = (
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
resource: Boat | undefined
|
resource: Boat | undefined
|
||||||
) {
|
): Reservation[] => {
|
||||||
if (!resource) return [];
|
if (!resource) return [] as Reservation[];
|
||||||
return reservationStore.getReservationsByDate(
|
return reservationStore.getReservationsByDate(
|
||||||
getDate(timestamp),
|
getDate(timestamp),
|
||||||
(resource as Boat).$id
|
(resource as Boat).$id
|
||||||
);
|
).value;
|
||||||
}
|
};
|
||||||
function onToday() {
|
function onToday() {
|
||||||
calendar.value.moveToToday();
|
calendar.value.moveToToday();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +35,8 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
">
|
">
|
||||||
<template
|
<template
|
||||||
v-for="block in sortedIntervals(
|
v-for="block in sortedIntervals(scope.timestamp, scope.resource)
|
||||||
scope.timestamp,
|
.value"
|
||||||
scope.resource
|
|
||||||
)"
|
|
||||||
:key="block.id">
|
:key="block.id">
|
||||||
<q-chip class="cursor-pointer">
|
<q-chip class="cursor-pointer">
|
||||||
{{ date.formatDate(block.start, 'HH:mm') }} -
|
{{ date.formatDate(block.start, 'HH:mm') }} -
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { defineStore } from 'pinia';
|
|||||||
import { ID, account, functions, teams } from 'boot/appwrite';
|
import { ID, account, functions, teams } from 'boot/appwrite';
|
||||||
import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite';
|
import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useBoatStore } from './boat';
|
||||||
|
import { useReservationStore } from './reservation';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const currentUser = ref<Models.User<Models.Preferences> | null>(null);
|
const currentUser = ref<Models.User<Models.Preferences> | null>(null);
|
||||||
@@ -14,6 +16,8 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
try {
|
try {
|
||||||
currentUser.value = await account.get();
|
currentUser.value = await account.get();
|
||||||
currentUserTeams.value = await teams.list();
|
currentUserTeams.value = await teams.list();
|
||||||
|
await useBoatStore().fetchBoats();
|
||||||
|
await useReservationStore().fetchUserReservations();
|
||||||
} catch {
|
} catch {
|
||||||
currentUser.value = null;
|
currentUser.value = null;
|
||||||
currentUserTeams.value = null;
|
currentUserTeams.value = null;
|
||||||
@@ -41,13 +45,31 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
await init();
|
await init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createTokenSession(email: string) {
|
||||||
|
return await account.createEmailToken(ID.unique(), email);
|
||||||
|
}
|
||||||
|
|
||||||
async function googleLogin() {
|
async function googleLogin() {
|
||||||
account.createOAuth2Session(
|
await account.createOAuth2Session(
|
||||||
OAuthProvider.Google,
|
OAuthProvider.Google,
|
||||||
'https://bab.toal.ca/',
|
'https://oys.undock.ca',
|
||||||
'https://bab.toal.ca/#/login'
|
'https://oys.undock.ca/login'
|
||||||
);
|
);
|
||||||
currentUser.value = await account.get();
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discordLogin() {
|
||||||
|
await account.createOAuth2Session(
|
||||||
|
OAuthProvider.Discord,
|
||||||
|
'https://oys.undock.ca',
|
||||||
|
'https://oys.undock.ca/login'
|
||||||
|
);
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tokenLogin(userId: string, token: string) {
|
||||||
|
await account.createSession(userId, token);
|
||||||
|
await init();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserNameById(id: string | undefined | null): string {
|
function getUserNameById(id: string | undefined | null): string {
|
||||||
@@ -78,7 +100,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 +113,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
getUserNameById,
|
getUserNameById,
|
||||||
hasRequiredRole,
|
hasRequiredRole,
|
||||||
register,
|
register,
|
||||||
|
updateName,
|
||||||
login,
|
login,
|
||||||
googleLogin,
|
googleLogin,
|
||||||
|
discordLogin,
|
||||||
|
createTokenSession,
|
||||||
|
tokenLogin,
|
||||||
logout,
|
logout,
|
||||||
init,
|
init,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,25 +2,44 @@ import { defineStore } from 'pinia';
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { Boat } from './boat';
|
import { Boat } from './boat';
|
||||||
import { Timestamp, today } from '@quasar/quasar-ui-qcalendar';
|
import { Timestamp, today } from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { Interval } from './schedule.types';
|
||||||
import { Interval, IntervalRecord } from './schedule.types';
|
|
||||||
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
import { ID, Query } from 'appwrite';
|
import { ID, Query } from 'appwrite';
|
||||||
import { useReservationStore } from './reservation';
|
import { useReservationStore } from './reservation';
|
||||||
|
import { LoadingTypes } from 'src/utils/misc';
|
||||||
|
import { useRealtimeStore } from './realtime';
|
||||||
|
|
||||||
export const useIntervalStore = defineStore('interval', () => {
|
export const useIntervalStore = defineStore('interval', () => {
|
||||||
// TODO: Implement functions to dynamically pull this data.
|
const intervals = ref(new Map<string, Interval>()); // Intervals by DocID
|
||||||
const intervals = ref<Map<string, Interval>>(new Map());
|
const dateStatus = ref(new Map<string, LoadingTypes>()); // State of load by date
|
||||||
const intervalDates = ref<IntervalRecord>({});
|
|
||||||
const reservationStore = useReservationStore();
|
|
||||||
const selectedDate = ref<string>(today());
|
const selectedDate = ref<string>(today());
|
||||||
|
|
||||||
const getIntervals = (date: Timestamp | string, boat?: Boat): Interval[] => {
|
const reservationStore = useReservationStore();
|
||||||
|
|
||||||
|
const realtimeStore = useRealtimeStore();
|
||||||
|
|
||||||
|
realtimeStore.register(
|
||||||
|
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.interval}.documents`,
|
||||||
|
(response) => {
|
||||||
|
const payload = response.payload as Interval;
|
||||||
|
if (!payload.$id) return;
|
||||||
|
if (
|
||||||
|
response.events.includes('databases.*.collections.*.documents.*.delete')
|
||||||
|
) {
|
||||||
|
intervals.value.delete(payload.$id);
|
||||||
|
} else {
|
||||||
|
intervals.value.set(payload.$id, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getIntervals = (date: Timestamp | string, boat?: Boat) => {
|
||||||
const searchDate = typeof date === 'string' ? date : date.date;
|
const searchDate = typeof date === 'string' ? date : date.date;
|
||||||
const dayStart = new Date(searchDate + 'T00:00');
|
const dayStart = new Date(searchDate + 'T00:00');
|
||||||
const dayEnd = new Date(searchDate + 'T23:59');
|
const dayEnd = new Date(searchDate + 'T23:59');
|
||||||
if (!intervalDates.value[searchDate]) {
|
if (dateStatus.value.get(searchDate) === undefined) {
|
||||||
intervalDates.value[searchDate] = 'pending';
|
dateStatus.value.set(searchDate, 'pending');
|
||||||
fetchIntervals(searchDate);
|
fetchIntervals(searchDate);
|
||||||
}
|
}
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
@@ -32,22 +51,19 @@ export const useIntervalStore = defineStore('interval', () => {
|
|||||||
const matchesBoat = boat ? boat.$id === interval.resource : true;
|
const matchesBoat = boat ? boat.$id === interval.resource : true;
|
||||||
return isWithinDay && matchesBoat;
|
return isWithinDay && matchesBoat;
|
||||||
});
|
});
|
||||||
}).value;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvailableIntervals = (
|
const getAvailableIntervals = (date: Timestamp | string, boat?: Boat) => {
|
||||||
date: Timestamp | string,
|
return computed(() =>
|
||||||
boat?: Boat
|
getIntervals(date, boat).value.filter((interval) => {
|
||||||
): Interval[] => {
|
|
||||||
return computed(() => {
|
|
||||||
return getIntervals(date, boat).filter((interval) => {
|
|
||||||
return !reservationStore.isResourceTimeOverlapped(
|
return !reservationStore.isResourceTimeOverlapped(
|
||||||
interval.resource,
|
interval.resource,
|
||||||
new Date(interval.start),
|
new Date(interval.start),
|
||||||
new Date(interval.end)
|
new Date(interval.end)
|
||||||
);
|
);
|
||||||
});
|
})
|
||||||
}).value;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchInterval(id: string): Promise<Interval> {
|
async function fetchInterval(id: string): Promise<Interval> {
|
||||||
@@ -78,11 +94,11 @@ export const useIntervalStore = defineStore('interval', () => {
|
|||||||
response.documents.forEach((d) =>
|
response.documents.forEach((d) =>
|
||||||
intervals.value.set(d.$id, d as Interval)
|
intervals.value.set(d.$id, d as Interval)
|
||||||
);
|
);
|
||||||
intervalDates.value[dateString] = 'loaded';
|
dateStatus.value.set(dateString, 'loaded');
|
||||||
console.info(`Loaded ${response.documents.length} intervals from server`);
|
console.info(`Loaded ${response.documents.length} intervals from server`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch intervals', error);
|
console.error('Failed to fetch intervals', error);
|
||||||
intervalDates.value[dateString] = 'error';
|
dateStatus.value.set(dateString, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,5 +156,6 @@ export const useIntervalStore = defineStore('interval', () => {
|
|||||||
updateInterval,
|
updateInterval,
|
||||||
deleteInterval,
|
deleteInterval,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
|
intervals,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
21
src/stores/realtime.ts
Normal file
21
src/stores/realtime.ts
Normal 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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { Reservation } from './schedule.types';
|
import type { Reservation } from './schedule.types';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { ComputedRef, computed, reactive } from 'vue';
|
||||||
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
import { ID, Query } from 'appwrite';
|
import { ID, Query } from 'appwrite';
|
||||||
import { date, useQuasar } from 'quasar';
|
import { date, useQuasar } from 'quasar';
|
||||||
@@ -8,15 +8,37 @@ import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
|
|||||||
import { LoadingTypes } from 'src/utils/misc';
|
import { LoadingTypes } from 'src/utils/misc';
|
||||||
import { useAuthStore } from './auth';
|
import { useAuthStore } from './auth';
|
||||||
import { isPast } from 'src/utils/schedule';
|
import { isPast } from 'src/utils/schedule';
|
||||||
|
import { useRealtimeStore } from './realtime';
|
||||||
|
|
||||||
export const useReservationStore = defineStore('reservation', () => {
|
export const useReservationStore = defineStore('reservation', () => {
|
||||||
const reservations = ref<Map<string, Reservation>>(new Map());
|
const reservations = reactive<Map<string, Reservation>>(new Map());
|
||||||
const datesLoaded = ref<Record<string, LoadingTypes>>({});
|
const datesLoaded = reactive<Record<string, LoadingTypes>>({});
|
||||||
const userReservations = ref<Map<string, Reservation>>(new Map());
|
const userReservations = reactive<Map<string, Reservation>>(new Map());
|
||||||
// TODO: Come up with a better way of storing reservations by date & reservations for user
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
const realtimeStore = useRealtimeStore();
|
||||||
|
|
||||||
|
realtimeStore.register(
|
||||||
|
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.reservation}.documents`,
|
||||||
|
(response) => {
|
||||||
|
const payload = response.payload as Reservation;
|
||||||
|
if (payload.$id) {
|
||||||
|
if (
|
||||||
|
response.events.includes(
|
||||||
|
'databases.*.collections.*.documents.*.delete'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
reservations.delete(payload.$id);
|
||||||
|
userReservations.delete(payload.$id);
|
||||||
|
} else {
|
||||||
|
reservations.set(payload.$id, payload);
|
||||||
|
if (payload.user === authStore.currentUser?.$id)
|
||||||
|
userReservations.set(payload.$id, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
// Fetch reservations for a specific date range
|
// Fetch reservations for a specific date range
|
||||||
const fetchReservationsForDateRange = async (
|
const fetchReservationsForDateRange = async (
|
||||||
start: string = today(),
|
start: string = today(),
|
||||||
@@ -40,7 +62,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
response.documents.forEach((d) =>
|
response.documents.forEach((d) =>
|
||||||
reservations.value.set(d.$id, d as Reservation)
|
reservations.set(d.$id, d as Reservation)
|
||||||
);
|
);
|
||||||
setDateLoaded(startDate, endDate, 'loaded');
|
setDateLoaded(startDate, endDate, 'loaded');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -81,8 +103,8 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
reservation
|
reservation
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
reservations.value.set(response.$id, response as Reservation);
|
reservations.set(response.$id, response as Reservation);
|
||||||
userReservations.value.set(response.$id, response as Reservation);
|
userReservations.set(response.$id, response as Reservation);
|
||||||
console.info('Reservation booked: ', response);
|
console.info('Reservation booked: ', response);
|
||||||
return response as Reservation;
|
return response as Reservation;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -95,14 +117,8 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
reservation: string | Reservation | null | undefined
|
reservation: string | Reservation | null | undefined
|
||||||
) => {
|
) => {
|
||||||
if (!reservation) return false;
|
if (!reservation) return false;
|
||||||
let id;
|
const id = typeof reservation === 'string' ? reservation : reservation.$id;
|
||||||
if (typeof reservation === 'string') {
|
if (!id) return false;
|
||||||
id = reservation;
|
|
||||||
} else if ('$id' in reservation && typeof reservation.$id === 'string') {
|
|
||||||
id = reservation.$id;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = $q.notify({
|
const status = $q.notify({
|
||||||
color: 'secondary',
|
color: 'secondary',
|
||||||
@@ -120,8 +136,8 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
AppwriteIds.collection.reservation,
|
AppwriteIds.collection.reservation,
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
reservations.value.delete(id);
|
reservations.delete(id);
|
||||||
userReservations.value.delete(id);
|
userReservations.delete(id);
|
||||||
console.info(`Deleted reservation: ${id}`);
|
console.info(`Deleted reservation: ${id}`);
|
||||||
status({
|
status({
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
@@ -146,7 +162,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
if (start > end) return [];
|
if (start > end) return [];
|
||||||
let curDate = start;
|
let curDate = start;
|
||||||
while (curDate < end) {
|
while (curDate < end) {
|
||||||
datesLoaded.value[(parseDate(curDate) as Timestamp).date] = state;
|
datesLoaded[(parseDate(curDate) as Timestamp).date] = state;
|
||||||
curDate = date.addToDate(curDate, { days: 1 });
|
curDate = date.addToDate(curDate, { days: 1 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -157,8 +173,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
const unloaded = [];
|
const unloaded = [];
|
||||||
while (curDate < end) {
|
while (curDate < end) {
|
||||||
const parsedDate = (parseDate(curDate) as Timestamp).date;
|
const parsedDate = (parseDate(curDate) as Timestamp).date;
|
||||||
if (datesLoaded.value[parsedDate] === undefined)
|
if (datesLoaded[parsedDate] === undefined) unloaded.push(parsedDate);
|
||||||
unloaded.push(parsedDate);
|
|
||||||
curDate = date.addToDate(curDate, { days: 1 });
|
curDate = date.addToDate(curDate, { days: 1 });
|
||||||
}
|
}
|
||||||
return unloaded;
|
return unloaded;
|
||||||
@@ -168,15 +183,15 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
const getReservationsByDate = (
|
const getReservationsByDate = (
|
||||||
searchDate: string,
|
searchDate: string,
|
||||||
boat?: string
|
boat?: string
|
||||||
): Reservation[] => {
|
): ComputedRef<Reservation[]> => {
|
||||||
if (!datesLoaded.value[searchDate]) {
|
if (!datesLoaded[searchDate]) {
|
||||||
fetchReservationsForDateRange(searchDate);
|
fetchReservationsForDateRange(searchDate);
|
||||||
}
|
}
|
||||||
const dayStart = new Date(searchDate + 'T00:00');
|
const dayStart = new Date(searchDate + 'T00:00');
|
||||||
const dayEnd = new Date(searchDate + 'T23:59');
|
const dayEnd = new Date(searchDate + 'T23:59');
|
||||||
|
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
return Array.from(reservations.value.values()).filter((reservation) => {
|
return Array.from(reservations.values()).filter((reservation) => {
|
||||||
const reservationStart = new Date(reservation.start);
|
const reservationStart = new Date(reservation.start);
|
||||||
const reservationEnd = new Date(reservation.end);
|
const reservationEnd = new Date(reservation.end);
|
||||||
|
|
||||||
@@ -185,7 +200,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
const matchesBoat = boat ? boat === reservation.resource : true;
|
const matchesBoat = boat ? boat === reservation.resource : true;
|
||||||
return isWithinDay && matchesBoat;
|
return isWithinDay && matchesBoat;
|
||||||
});
|
});
|
||||||
}).value;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get conflicting reservations for a resource within a time range
|
// Get conflicting reservations for a resource within a time range
|
||||||
@@ -194,7 +209,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
start: Date,
|
start: Date,
|
||||||
end: Date
|
end: Date
|
||||||
): Reservation[] => {
|
): Reservation[] => {
|
||||||
return Array.from(reservations.value.values()).filter(
|
return Array.from(reservations.values()).filter(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
entry.resource === resource &&
|
entry.resource === resource &&
|
||||||
new Date(entry.start) < end &&
|
new Date(entry.start) < end &&
|
||||||
@@ -229,7 +244,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
[Query.equal('user', authStore.currentUser.$id)]
|
[Query.equal('user', authStore.currentUser.$id)]
|
||||||
);
|
);
|
||||||
response.documents.forEach((d) =>
|
response.documents.forEach((d) =>
|
||||||
userReservations.value.set(d.$id, d as Reservation)
|
userReservations.set(d.$id, d as Reservation)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch reservations for user: ', error);
|
console.error('Failed to fetch reservations for user: ', error);
|
||||||
@@ -237,7 +252,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sortedUserReservations = computed((): Reservation[] =>
|
const sortedUserReservations = computed((): Reservation[] =>
|
||||||
[...userReservations.value?.values()].sort(
|
[...userReservations.values()].sort(
|
||||||
(a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()
|
(a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -252,27 +267,6 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
return sortedUserReservations.value?.filter((b) => isPast(b.end));
|
return sortedUserReservations.value?.filter((b) => isPast(b.end));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure reactivity for computed properties when Map is modified
|
|
||||||
watch(
|
|
||||||
reservations,
|
|
||||||
() => {
|
|
||||||
sortedUserReservations.value;
|
|
||||||
futureUserReservations.value;
|
|
||||||
pastUserReservations.value;
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
userReservations,
|
|
||||||
() => {
|
|
||||||
sortedUserReservations.value;
|
|
||||||
futureUserReservations.value;
|
|
||||||
pastUserReservations.value;
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getReservationsByDate,
|
getReservationsByDate,
|
||||||
getReservationById,
|
getReservationById,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Models } from 'appwrite';
|
import { Models } from 'appwrite';
|
||||||
import { LoadingTypes } from 'src/utils/misc';
|
|
||||||
|
|
||||||
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
||||||
export type Reservation = Interval & {
|
export type Reservation = Interval & {
|
||||||
@@ -29,7 +28,3 @@ export type IntervalTemplate = Partial<Models.Document> & {
|
|||||||
name: string;
|
name: string;
|
||||||
timeTuples: TimeTuple[];
|
timeTuples: TimeTuple[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IntervalRecord {
|
|
||||||
[key: string]: LoadingTypes;
|
|
||||||
}
|
|
||||||
|
|||||||
1
src/version.js
Normal file
1
src/version.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const APP_VERSION = '0.0.0';
|
||||||
490
templates/claude-templates.md
Normal file
490
templates/claude-templates.md
Normal 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.**
|
||||||
Reference in New Issue
Block a user