├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .github ├── issue-branch.yml └── workflows │ ├── create-branch.yml │ ├── issue-autolink.yml │ ├── javascript-ci.yml │ └── main.yml ├── .gitignore ├── .prettierignore ├── Firebase ├── .firebaserc ├── .gitignore ├── README.md ├── dummyData │ ├── auth_export │ │ ├── accounts.json │ │ └── config.json │ ├── firebase-export-metadata.json │ ├── firestore_export │ │ ├── all_namespaces │ │ │ └── all_kinds │ │ │ │ ├── all_namespaces_all_kinds.export_metadata │ │ │ │ └── output-0 │ │ └── firestore_export.overall_export_metadata │ └── storage_export │ │ └── buckets.json ├── extensions │ └── firestore-send-email.env ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions │ ├── billingKillswitch.js │ ├── index.js │ ├── package.json │ └── yarn.lock └── storage.rules ├── LICENSE ├── README.md ├── app.vue ├── assets ├── css │ └── main.css └── logos │ ├── ShortLogo.png │ ├── TextlessLogo.png │ ├── TextlessLogoYellow-Black.png │ ├── TextlessLogoYellow.png │ ├── TextlessTransparentLogo.png │ └── TransparentLongLogo.png ├── components ├── Admin │ └── Tournament │ │ ├── DateRounds.vue │ │ ├── DayVenues.vue │ │ └── ExpandBtn.vue ├── Button.vue ├── Chip.vue ├── DeleteDialog.vue ├── Division │ └── DivisionPanel.vue ├── Dropdown.vue ├── FormField.vue ├── Frame.vue ├── HamburgerListItem.vue ├── Header.vue ├── HomePage │ └── LevelButton.vue ├── Leaderboard.vue ├── Loading.vue ├── Modal.vue ├── Multiselect.vue ├── NavBar │ ├── HomeButton.vue │ └── NavBar.vue ├── Notification │ └── index.vue ├── SearchBar.vue ├── SearchSelect.vue ├── Stepper.vue ├── Table.vue ├── Tabs.vue ├── ViewTeams.vue └── admin │ ├── AdminButton.vue │ └── ProfileInfo.vue ├── composables └── useNotification.js ├── docker-compose.yml ├── docker └── emulator │ └── Dockerfile ├── docs ├── docs │ ├── components.md │ ├── images │ │ ├── ShortLogo.png │ │ ├── TextlessLogo.png │ │ ├── TextlessLogoYellow-Black.png │ │ ├── TextlessLogoYellow.png │ │ ├── TextlessTransparentLogo.png │ │ ├── TransparentLongLogo.png │ │ ├── firebaselogo.png │ │ └── nuxtlogo.png │ ├── index.md │ ├── installation-guide.md │ ├── tournament-setup.md │ ├── user-guide.md │ └── using-mkdocs.txt ├── mkdocs.yml └── requirements.txt ├── layouts └── default.vue ├── middleware ├── adjudicator.js ├── admin.js └── team.js ├── misc └── firebaseHelpers.js ├── nuxt.config.js ├── package.json ├── pages ├── adjudicator.vue ├── adjudicator │ ├── [tournamentId] │ │ ├── index.vue │ │ └── scoresheet │ │ │ └── [matchupId].vue │ └── index.vue ├── admin.vue ├── admin │ ├── contacts.vue │ ├── create-admin.vue │ ├── index.vue │ ├── institutions.vue │ ├── signup-requests.vue │ ├── teams.vue │ ├── tournaments │ │ ├── [tournamentId] │ │ │ ├── division │ │ │ │ └── [level].vue │ │ │ ├── fixtures │ │ │ │ ├── [tournamentId].vue │ │ │ │ └── scoresheet │ │ │ │ │ └── [matchupId].vue │ │ │ ├── index.vue │ │ │ ├── leaderboard │ │ │ │ └── index.vue │ │ │ ├── matchups-approve │ │ │ │ ├── index.vue │ │ │ │ └── scoresheet │ │ │ │ │ └── [matchupId].vue │ │ │ └── matchups-upload.vue │ │ └── index.vue │ └── venues.vue ├── coordinator.vue ├── coordinator │ ├── draw.vue │ ├── index.vue │ ├── institutions.vue │ ├── team-registration.vue │ ├── teams │ │ └── [tournamentId].vue │ └── userinformation.vue ├── fixtures │ └── [tournamentId].vue ├── index.vue ├── login.vue ├── resetpassword.vue └── signup.vue ├── plugins ├── 1.firebase.client.js ├── 1.firebase.server.js └── 2.firebase_auth.server.js ├── prettierrc.json ├── server └── api │ ├── create-admin.post.js │ └── send-email.post.js ├── stores ├── adjudicator.js ├── admin.js ├── institutions.js ├── leaderboard.js ├── matchups.js ├── teams.js ├── tournaments.js ├── user.js └── venues.js ├── tailwind.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Refer to README.md to setup .env file 2 | 3 | # refer to https://nuxtjs.org/docs/configuration-glossary/configuration-env/ for more info 4 | # on how nuxt uses these variables. 5 | 6 | # FIREBASE ======================================= 7 | NUXT_PUBLIC_FIREBASE_API_KEY="" 8 | NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN="" 9 | NUXT_PUBLIC_FIREBASE_PROJECT_ID="" 10 | NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET="" 11 | NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="" 12 | NUXT_PUBLIC_FIREBASE_APP_ID="" 13 | NUXT_PUBLIC_FIREBASE_MODE="" 14 | 15 | FIREBASE_SERVICE_ACCOUNT_CONFIG="" 16 | 17 | FIRESTORE_EMULATOR_HOST="localhost:8080" 18 | FIREBASE_AUTH_EMULATOR_HOST="localhost:9099" 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | "standard", 5 | "plugin:vue/vue3-recommended", 6 | "@vue/eslint-config-standard", 7 | "plugin:prettier/recommended", 8 | ], 9 | rules: { 10 | "vue/multi-word-component-names": 0, 11 | }, 12 | overrides: [ 13 | { 14 | files: ["**.js", "**.cjs", "**.mjs", "**.vue"], 15 | }, 16 | ], 17 | globals: { 18 | $fetch: false, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/issue-branch.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/robvanderleek/create-issue-branch#option-2-configure-github-action 2 | 3 | # ex: i4-lower_camel_upper 4 | branchName: 'i${issue.number}-${issue.title,}' 5 | branches: 6 | - label: epic 7 | skip: true -------------------------------------------------------------------------------- /.github/workflows/create-branch.yml: -------------------------------------------------------------------------------- 1 | name: Create Branch from Issue 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | create_issue_branch_job: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Create Issue Branch 12 | uses: robvanderleek/create-issue-branch@main 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/issue-autolink.yml: -------------------------------------------------------------------------------- 1 | name: 'Issue Autolink' 2 | on: 3 | pull_request: 4 | types: [opened] 5 | 6 | jobs: 7 | issue-links: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: tkt-actions/add-issue-links@v1.8.0 11 | with: 12 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 13 | branch-prefix: 'i' 14 | resolve: 'true' -------------------------------------------------------------------------------- /.github/workflows/javascript-ci.yml: -------------------------------------------------------------------------------- 1 | name: Constant Integration for JavaScript 2 | on: 3 | pull_request: 4 | type: [opened, edited] 5 | paths: 6 | - '**.js' 7 | - '**.cjs' 8 | - '**.mjs' 9 | - '**.vue' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 18 | - name: Setup Node with yarn 19 | uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 20 | with: 21 | node-version: 18 22 | cache: 'yarn' 23 | - name: Install Dependencies 24 | run: yarn install 25 | - name: Run ESLint 26 | uses: wearerequired/lint-action@66e2f9c9bf05b7aca19dbb59aebad95da5ddc537 27 | with: 28 | eslint: true 29 | eslint_args: '--ignore-path .gitignore' 30 | neutral_check_on_warning: true 31 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | name: Deploy docs 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout main 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.8' 20 | 21 | - name: Upgrade pip 22 | run: | 23 | # install pip=>20.1 to use "pip cache dir" 24 | python3 -m pip install --upgrade pip 25 | - name: Get pip cache dir 26 | id: pip-cache 27 | run: echo "::set-output name=dir::$(pip cache dir)" 28 | 29 | - name: Cache dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: ${{ steps.pip-cache.outputs.dir }} 33 | key: ${{ runner.os }}-pip-${{ hashFiles('**/docs/requirements.txt') }} 34 | restore-keys: | 35 | ${{ runner.os }}-pip- 36 | - name: Install dependencies 37 | run: python3 -m pip install -r ./docs/requirements.txt 38 | 39 | - run: mkdocs build 40 | working-directory: docs 41 | env: 42 | ENABLE_PDF_EXPORT: 1 43 | 44 | - name: Deploy 45 | uses: peaceiris/actions-gh-pages@v3 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | publish_dir: ./docs/site 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./data 2 | .env 3 | .env.*.local 4 | 5 | .DS_Store 6 | 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # Development Environment 148 | .vscode/ 149 | vetur.config.js 150 | jsconfig.json 151 | 152 | # Docker Persistence layer 153 | data/docker/* 154 | data/wais/db* 155 | 156 | node_modules 157 | *.log* 158 | .nuxt 159 | .nitro 160 | .cache 161 | .output 162 | .env 163 | dist 164 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ### 2 | # Place your Prettier ignore content here 3 | 4 | ### 5 | # .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506 6 | 7 | # Created by .ignore support plugin (hsz.mobi) 8 | ### Node template 9 | # Logs 10 | /logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # parcel-bundler cache (https://parceljs.org/) 69 | .cache 70 | 71 | # next.js build output 72 | .next 73 | 74 | # nuxt.js build output 75 | .nuxt 76 | 77 | # Nuxt generate 78 | dist 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless 85 | 86 | # IDE / Editor 87 | .idea 88 | 89 | # Service worker 90 | sw.* 91 | 92 | # macOS 93 | .DS_Store 94 | 95 | # Vim swap files 96 | *.swp -------------------------------------------------------------------------------- /Firebase/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "wadl-7e1f7" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Firebase/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firestore Data 14 | # Firebase config 15 | 16 | # Uncomment this if you'd like others to create their own Firebase project. 17 | # For a team working on the same Firebase project(s), it is recommended to leave 18 | # it commented so all members can deploy to the same project(s) in .firebaserc. 19 | # .firebaserc 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (http://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | -------------------------------------------------------------------------------- /Firebase/README.md: -------------------------------------------------------------------------------- 1 | # Firebase Instructions 2 | 3 | --- 4 | 5 | This document will include instructions on all things firebase in the Western Australian Debating Legue!! 6 | 7 | ## Firebase Steps 8 | 9 | --- 10 | 11 | This section will include steps on how to setup firebase for the WADL project 12 | 13 | **Note**: after running most of the yarn commands that involve using the emulators, please run: 14 | 15 | > yarn emu:stop 16 | 17 | If you want to start the emulators just by themselves to see if they work, run: 18 | 19 | > yarn firebase 20 | 21 | If you want to run the wadl project with the emulators, run: 22 | 23 | > yarn dev:emu 24 | 25 | To run the project with the cloud service, run: 26 | 27 | > yarn dev 28 | 29 | If you want to kill the ports that the emulators use, run: 30 | 31 | > yarn emu:stop 32 | 33 | If you want to use the most up to date rules for emulation, please check the wadl firebase project, or use the draft rules in the Firebase/firestore.rules file 34 | 35 | The emulators are now linked to a pinia store that will allow for easy access of data in different pages, this will also allow for a fully functioning login & register page when linked to routes. 36 | 37 | To access the data that is inputted into these forms, if using the emulators, go to: 38 | 39 | > http://127.0.0.1:4000/ 40 | 41 | ## Firebase Emulators 42 | 43 | --- 44 | 45 | This project will inlcude the emulators for 46 | 47 | 1. Firestore 48 | 2. Authentication 49 | 3. Functions 50 | 4. Storage 51 | 52 | The project will get the use of extensions added in later, when it becomes more 53 | definitive the extenions we need, these can be installed by running the command 54 | 55 | > firebase ext:install 56 | 57 | The way that the emulators have been set up is to be in the base wadl directory, before running anything make sure to run: 58 | 59 | > yarn install 60 | 61 | Then make sure that there is a variable in the .env file that triggers the dev mode, this will be: 62 | 63 | > NUXT_PUBLIC_FIREBASE_MODE="dev" 64 | 65 | Now that all the required packages have been installed, you can run: 66 | 67 | > yarn firebase 68 | 69 | This will provide you with a gui of what is going on like your using the online version, to access this please follow the link that comes up in your terminal. If the link doesn't show in the terminal, this is the default link: 70 | 71 | > localhost:4000 72 | 73 | Now you can run the basic yarn command to start the project: 74 | 75 | > yarn dev 76 | 77 | I have added in the basic Auth and Firestore emualtors, and will decide later if any more need to be added 78 | 79 | ## Firebase Killswitch 80 | 81 | --- 82 | 83 | When the spending limit is reached, it will automatically downgrade the application to the free tier. 84 | 85 | ## Installation Guide 86 | 87 | 1. Create a budget in the `Budgets & alerts` tab in [Billing](https://console.cloud.google.com/billing). 88 | 89 | 1. Enable `Connect a Pub/Sub topic to this budget`. 90 | 91 | 1. Create a `Cloud Pub/Sub topic` for the Firebase project and name it `billing`. 92 | 93 | 1. Enable [Cloud Billing API](https://console.developers.google.com/apis/api/cloudbilling.googleapis.com). 94 | 95 | 1. Rename the project id in `.firebaserc` and `billingKillswitch.js`. 96 | 97 | 1. Set up Firebase CLI and login: 98 | 99 | ```console 100 | yarn global add firebase-tools 101 | firebase login 102 | ``` 103 | 104 | If `firebase` is not found, try: 105 | 106 | ```console 107 | npm install -g firebase-tools 108 | firebase login 109 | ``` 110 | 111 | 1. Check if CLI is installed correctly: 112 | 113 | ```console 114 | firebase projects:list 115 | ``` 116 | 117 | 1. Navigate to the `functions` folder and deploy functions to Firebase: 118 | 119 | ```console 120 | cd functions 121 | yarn 122 | yarn deploy:functions 123 | ``` 124 | 125 | 1. Give billing administrative privilege to **__@appspot.gserviceaccount.com** in [Billing](https://console.cloud.google.com/billing) under `Account management`. 126 | 127 | ### Optional - [Retrying Event-Driven Functions](https://cloud.google.com/functions/docs/bestpractices/retries) 128 | 129 | Event-Driven Functions may [fail](https://cloud.google.com/functions/docs/bestpractices/retries#why_event-driven_functions_fail_to_complete) and the event will be discarded so enabling `Retry on failure` will cuase the event to be retried for up to multiple days (defualt: 7 days) until the function successfully completes. 130 | 131 | Warning: The functuion may stuck in a retry loop if there is a bug in the code or some permanent error. 132 | 133 | 1. Activating `Retry on failure` Using the [Cloud Console](https://cloud.google.com/functions/docs/bestpractices/retries#using_the). 134 | 135 | 1. Go to the [Cloud Functions Overview page](https://console.cloud.google.com/functions/list) in the Cloud Platform Console. 136 | 137 | 1. Click `Create` function. Alternatively, click an existing function to go to its details page and click `Edit`. 138 | 139 | 1. Fill in the required fields for your function. 140 | 141 | 1. Ensure the `Trigger` field is set to an event-based trigger type, such as `Cloud Pub/Sub` or `Cloud Storage`. 142 | 143 | 1. Expand the advanced settings by clicking `More`. 144 | 145 | 1. Check or uncheck the box labeled `Retry on failure`. 146 | 147 | 1. Activating `Retry on failure` in Firebase Cloud Function [Programmatically](https://stackoverflow.com/questions/55606808/activate-retry-in-firebase-cloud-function-programmatically). 148 | 149 | 1. Activating `Retry on failure` Using [gcloud CLI](https://dev.to/danielsc/firebase-function-retries-with-pubsub-3jf9). 150 | 151 | ### Testing 152 | 153 | 1. Go to [Pub/Sub](https://console.cloud.google.com/cloudpubsub/topic/detail) and select the Firebase project. 154 | 155 | 1. Navigate to the `MESSAGES` tab, then click on the `PUBLISH MESSAGE` button. 156 | 157 | 1. Paste the code below into `Message body` text box and hit `PUBLISH`: 158 | 159 | ```json 160 | { 161 | "budgetDisplayName": "name-of-budget", 162 | "alertThresholdExceeded": 1.0, 163 | "costAmount": 100.01, 164 | "costIntervalStart": "2019-01-01T00:00:00Z", 165 | "budgetAmount": 0.2, 166 | "budgetAmountType": "SPECIFIED_AMOUNT", 167 | "currencyCode": "AUD" 168 | } 169 | ``` 170 | 171 | 1. An email should be sent from Firebase informing you that the project has been downgraded to the free tier. 172 | 173 | ## Credits 174 | 175 | [How to Stop Runaway Bills on Google Cloud Platform](https://www.youtube.com/watch?v=KiTg8RPpGG4) 176 | -------------------------------------------------------------------------------- /Firebase/dummyData/auth_export/config.json: -------------------------------------------------------------------------------- 1 | {"signIn":{"allowDuplicateEmails":false}} -------------------------------------------------------------------------------- /Firebase/dummyData/firebase-export-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "11.24.0", 3 | "firestore": { 4 | "version": "1.16.0", 5 | "path": "firestore_export", 6 | "metadata_file": "firestore_export/firestore_export.overall_export_metadata" 7 | }, 8 | "auth": { 9 | "version": "11.24.0", 10 | "path": "auth_export" 11 | }, 12 | "storage": { 13 | "version": "11.24.0", 14 | "path": "storage_export" 15 | } 16 | } -------------------------------------------------------------------------------- /Firebase/dummyData/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/Firebase/dummyData/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata -------------------------------------------------------------------------------- /Firebase/dummyData/firestore_export/all_namespaces/all_kinds/output-0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/Firebase/dummyData/firestore_export/all_namespaces/all_kinds/output-0 -------------------------------------------------------------------------------- /Firebase/dummyData/firestore_export/firestore_export.overall_export_metadata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/Firebase/dummyData/firestore_export/firestore_export.overall_export_metadata -------------------------------------------------------------------------------- /Firebase/dummyData/storage_export/buckets.json: -------------------------------------------------------------------------------- 1 | { 2 | "buckets": [ 3 | { 4 | "id": "wadl-7e1f7.appspot.com" 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /Firebase/extensions/firestore-send-email.env: -------------------------------------------------------------------------------- 1 | DEFAULT_FROM=wadlexample@gmail.com 2 | LOCATION=australia-southeast1 3 | MAIL_COLLECTION=mail 4 | SMTP_CONNECTION_URI=smtps://wadlexample@gmail.com:bqwpbojjccbpzcop@smtp.gmail.com:465 5 | TEMPLATES_COLLECTION=templates -------------------------------------------------------------------------------- /Firebase/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "emulators": { 7 | "auth": { 8 | "port": 9099, 9 | "host": "0.0.0.0" 10 | }, 11 | "functions": { 12 | "port": 5001, 13 | "host": "0.0.0.0" 14 | }, 15 | "firestore": { 16 | "port": 8080, 17 | "host": "0.0.0.0" 18 | }, 19 | "pubsub": { 20 | "port": 8085, 21 | "host": "0.0.0.0" 22 | }, 23 | "storage": { 24 | "port": 9199, 25 | "host": "0.0.0.0" 26 | }, 27 | "ui": { 28 | "enabled": true, 29 | "host": "0.0.0.0" 30 | }, 31 | "singleProjectMode": true 32 | }, 33 | "functions": [ 34 | { 35 | "source": "functions", 36 | "codebase": "default", 37 | "ignore": [ 38 | "node_modules", 39 | ".git", 40 | "firebase-debug.log", 41 | "firebase-debug.*.log" 42 | ] 43 | } 44 | ], 45 | "storage": { 46 | "rules": "storage.rules" 47 | }, 48 | "extensions": { 49 | "firestore-send-email": "firebase/firestore-send-email@0.1.21" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Firebase/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /Firebase/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /users/{userId} { 5 | function isAdmin(){ 6 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin' 7 | } 8 | function allowSignup(){ 9 | return request.auth.token.email != null 10 | } 11 | function isOwner(){ 12 | return request.auth.uid == userId 13 | } 14 | function isAdjudicator(){ 15 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "Adjudicator" 16 | } 17 | function isTeamCoordinator(){ 18 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "Team Coordinator" 19 | } 20 | function isAdjudicatorCoordinator(){ 21 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "Adjudicator Coordinator" 22 | } 23 | function isNotUpdatingProtectedFields(resource){ 24 | return !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role', 'requesting']) 25 | } 26 | allow create: if allowSignup(); 27 | allow read: if isOwner() || isAdjudicator(); 28 | allow update: if isOwner() && isNotUpdatingProtectedFields(resource); 29 | allow create, read, update, delete: if isAdmin(); 30 | } 31 | match /venues/{venueId} { 32 | function isAdmin(){ 33 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin' 34 | } 35 | allow create, read, update, delete: if isAdmin(); 36 | allow read: if true; 37 | } 38 | match /tournaments/{tournamentId}/levels/{levelId} { 39 | function isTeamCoord() { 40 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Team Coordinator' 41 | } 42 | allow update, read: if isTeamCoord(); 43 | } 44 | match /tournaments/{document=**} { 45 | function isAdmin(){ 46 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin' 47 | } 48 | allow create, read, write, update, delete: if isAdmin(); 49 | allow read: if true; 50 | } 51 | match /tournaments/{tournamentId}/levels/{levelId}/divisions/{divisionId}/matchups/{matchupId} { 52 | function isAdjudicatorCoordinator() { 53 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Adjudicator Coordinator' 54 | } 55 | allow update, read: if isAdjudicatorCoordinator(); 56 | } 57 | 58 | match /institutions/{institutionsId} { 59 | function isAdjudicator(){ 60 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Adjudicator' 61 | 62 | } 63 | function isTeamCoordinator(){ 64 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Team Coordinator' 65 | 66 | } 67 | function isAdjudicatorCoordinator(){ 68 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Adjudicator Coordinator' 69 | 70 | } 71 | function isAdmin(){ 72 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin' 73 | } 74 | allow create, update, read: if isAdjudicator() || isTeamCoordinator() || isAdjudicatorCoordinator() || isAdmin(); 75 | allow delete: if isAdmin(); 76 | } 77 | match /teams/{teamId} { 78 | function isAdmin(){ 79 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin' 80 | } 81 | allow create, read, update, delete: if isAdmin(); 82 | allow create, read, update: if true; 83 | } 84 | match /matchups/{matchupID} { 85 | function isAdmin(){ 86 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin' 87 | } 88 | function isAdjudicator(){ 89 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "Adjudicator" 90 | } 91 | allow create, read, update, delete: if isAdmin(); 92 | allow read, update: if isAdjudicator(); 93 | allow read: if true; 94 | } 95 | match /leaderboard/{leaderboardId} { 96 | function isAdmin(){ 97 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin' 98 | } 99 | allow create, read, update, delete: if isAdmin(); 100 | allow read: if true; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Firebase/functions/billingKillswitch.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const functions = require("firebase-functions"); 3 | const { CloudBillingClient } = require("@google-cloud/billing"); 4 | // import * as functions from "firebase-functions"; 5 | // import {CloudBillingClient} from "@google-cloud/billing"; 6 | 7 | const PROJECT_ID = "wadl-7e1f7"; 8 | const PROJECT_NAME = `projects/${PROJECT_ID}`; 9 | const billing = new CloudBillingClient(); 10 | 11 | /** 12 | * Determine whether billing is enabled for a project 13 | * @param {string} projectName Name of project to check if billing is enabled 14 | * @return {bool} Whether project has billing enabled or not 15 | */ 16 | const _isBillingEnabled = async (projectName) => { 17 | try { 18 | const [res] = await billing.getProjectBillingInfo({ name: projectName }); 19 | console.log("isBillingEnabled:", res.billingEnabled); 20 | return res.billingEnabled; 21 | } catch (e) { 22 | console.log( 23 | "Unable to determine if billing is enabled on specified project,", 24 | "assuming billing is enabled" 25 | ); 26 | return true; 27 | } 28 | }; 29 | 30 | /** 31 | * Disable billing for a project by removing its billing account 32 | * @param {string} projectName Name of project disable billing on 33 | * @return {Promise} Text containing response from disabling billing 34 | */ 35 | const _disableBillingForProject = async (projectName) => { 36 | console.log("Disabling billing"); 37 | const [res] = await billing.updateProjectBillingInfo({ 38 | name: projectName, 39 | projectBillingInfo: { billingAccountName: "" }, // Disable billing 40 | }); 41 | console.log("Billing disabled"); 42 | return `Billing disabled: ${JSON.stringify(res)}`; 43 | }; 44 | 45 | exports.stopBilling = functions 46 | .region("australia-southeast1") 47 | .pubsub.topic("billing") 48 | .onPublish(async (pubsubEvent) => { 49 | const pubsubData = JSON.parse( 50 | Buffer.from(pubsubEvent.data, "base64").toString() 51 | ); 52 | console.log("pubsubEvent.data", pubsubEvent.data); 53 | console.log("pubsubData", pubsubData); 54 | console.log("pubsubData.costAmount", pubsubData.costAmount); 55 | console.log("pubsubData.budgetAmount", pubsubData.budgetAmount); 56 | 57 | if (pubsubData.costAmount <= pubsubData.budgetAmount) { 58 | console.log("No action necessary."); 59 | return `No action necessary. (Current cost: ${pubsubData.costAmount})`; 60 | } 61 | const billingEnabled = await _isBillingEnabled(PROJECT_NAME); 62 | if (billingEnabled) { 63 | return _disableBillingForProject(PROJECT_NAME); 64 | } else { 65 | console.log("Billing already disabled"); 66 | return "Billing already disabled"; 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /Firebase/functions/index.js: -------------------------------------------------------------------------------- 1 | const killswitch = require("./billingKillswitch"); 2 | exports.killswitch = killswitch; 3 | -------------------------------------------------------------------------------- /Firebase/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase emulators:start --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "16" 13 | }, 14 | "main": "index.js", 15 | "dependencies": { 16 | "firebase-admin": "^10.0.2", 17 | "firebase-functions": "^4.2.1" 18 | }, 19 | "devDependencies": { 20 | "firebase-functions-test": "^0.2.0" 21 | }, 22 | "private": true 23 | } 24 | -------------------------------------------------------------------------------- /Firebase/storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /{allPaths=**} { 5 | allow read, write: if false; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Coders for Causes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Western Australian Debating League 2 | A progressive web-application to assist the Western Australia Debating League with their seasonal organisation needs. 3 | 4 | # Nuxt 3 Minimal Starter 5 | 6 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 7 | 8 | ## Setup 9 | 10 | ### Local .env file 11 | Download **'env'**, pinned in discord wadl-backend channel. Rename to **'.env'** and place in root directory. 12 | Alternatively: 13 | * Create file '.env' in root directory, and copy content of '.env.example' into it 14 | * Copy environment values from the cfc wadl-backend discord channel into their corresponding values in .env 15 | * Set NUXT_PUBLIC_FIREBASE_MODE to "dev" 16 | > If you do not have access to wadl-backend, message an admin of the project. 17 | 18 | ## Local Server setup 19 | Make sure to install the dependencies: 20 | 21 | ```bash 22 | # yarn 23 | yarn install 24 | ``` 25 | ## Development Server 26 | 27 | Start the development server on http://localhost:3000 28 | 29 | ```bash 30 | yarn dev 31 | ``` 32 | ## Production 33 | 34 | Build the application for production: 35 | 36 | ```bash 37 | yarn run build 38 | ``` 39 | 40 | Locally preview production build: 41 | 42 | ```bash 43 | yarn run preview 44 | ``` 45 | ## Firebase Setup 46 | Refer to the README located in the src/Firebase folder 47 | 48 | ## Using Docker 49 | Run 50 | 51 | ``` 52 | yarn dev:docker 53 | ``` 54 | 55 | Open another terminal and run 56 | 57 | ``` 58 | docker exec -it wadl_firebase /bin/bash 59 | ``` 60 | to go into the container 61 | 62 | Once inside the container, run (make sure you have an account with Firebase and is granted permission for the project before doing this) 63 | 64 | ``` 65 | firebase login 66 | ``` 67 | and follow the link to authenticate your account 68 | 69 | Finally, run 70 | 71 | ``` 72 | firebase emulators:start --import=dummyData 73 | ``` 74 | inside the container 75 | 76 | ## More info 77 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 78 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .heading-brandon { 7 | font-family: theme("fontFamily.brandon"); 8 | letter-spacing: theme("letterSpacing.15"); 9 | font-weight: bold; 10 | } 11 | 12 | .heading-lato { 13 | font-family: theme("fontFamily.lato"); 14 | } 15 | 16 | .heading-futura { 17 | font-family: theme("fontFamily.futura"); 18 | font-weight: bold; 19 | } 20 | 21 | .heading-jost { 22 | font-family: theme("fontFamily.jost"); 23 | } 24 | 25 | .heading-montserrat { 26 | font-family: theme("fontFamily.montserrat"); 27 | } 28 | 29 | .body-brandon { 30 | font-family: theme("fontFamily.brandon"); 31 | letter-spacing: theme("letterSpacing.12"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/logos/ShortLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/assets/logos/ShortLogo.png -------------------------------------------------------------------------------- /assets/logos/TextlessLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/assets/logos/TextlessLogo.png -------------------------------------------------------------------------------- /assets/logos/TextlessLogoYellow-Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/assets/logos/TextlessLogoYellow-Black.png -------------------------------------------------------------------------------- /assets/logos/TextlessLogoYellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/assets/logos/TextlessLogoYellow.png -------------------------------------------------------------------------------- /assets/logos/TextlessTransparentLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/assets/logos/TextlessTransparentLogo.png -------------------------------------------------------------------------------- /assets/logos/TransparentLongLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/assets/logos/TransparentLongLogo.png -------------------------------------------------------------------------------- /components/Admin/Tournament/DateRounds.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 98 | -------------------------------------------------------------------------------- /components/Admin/Tournament/DayVenues.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 77 | -------------------------------------------------------------------------------- /components/Admin/Tournament/ExpandBtn.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | -------------------------------------------------------------------------------- /components/Button.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 51 | -------------------------------------------------------------------------------- /components/Chip.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 73 | -------------------------------------------------------------------------------- /components/DeleteDialog.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 41 | -------------------------------------------------------------------------------- /components/Division/DivisionPanel.vue: -------------------------------------------------------------------------------- 1 | 53 | 171 | -------------------------------------------------------------------------------- /components/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 48 | 116 | 123 | -------------------------------------------------------------------------------- /components/FormField.vue: -------------------------------------------------------------------------------- 1 | 25 | 42 | -------------------------------------------------------------------------------- /components/Frame.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 81 | -------------------------------------------------------------------------------- /components/HamburgerListItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /components/Header.vue: -------------------------------------------------------------------------------- 1 | 14 | 30 | -------------------------------------------------------------------------------- /components/HomePage/LevelButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /components/Leaderboard.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 46 | -------------------------------------------------------------------------------- /components/Loading.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /components/Modal.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 36 | 51 | -------------------------------------------------------------------------------- /components/Multiselect.vue: -------------------------------------------------------------------------------- 1 | 54 | 113 | 120 | -------------------------------------------------------------------------------- /components/NavBar/HomeButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /components/NavBar/NavBar.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 122 | -------------------------------------------------------------------------------- /components/Notification/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 54 | 55 | 70 | -------------------------------------------------------------------------------- /components/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 17 | 25 | -------------------------------------------------------------------------------- /components/SearchSelect.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 75 | -------------------------------------------------------------------------------- /components/Stepper.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /components/Table.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 123 | -------------------------------------------------------------------------------- /components/Tabs.vue: -------------------------------------------------------------------------------- 1 | 26 | 41 | -------------------------------------------------------------------------------- /components/ViewTeams.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | -------------------------------------------------------------------------------- /components/admin/AdminButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /components/admin/ProfileInfo.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /composables/useNotification.js: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { handleError } from "../misc/firebaseHelpers"; 3 | 4 | class NotificationModalHelper { 5 | #isVisible = ref(false); 6 | get isVisible() { 7 | return this.#isVisible.value; 8 | } 9 | 10 | #isSuccess = ref(false); 11 | get isSuccess() { 12 | return this.#isSuccess.value; 13 | } 14 | 15 | #message = ref(""); 16 | get message() { 17 | return this.#message.value; 18 | } 19 | 20 | dismiss() { 21 | this.#isVisible.value = false; 22 | this.#isSuccess.value = false; 23 | this.#message.value = ""; 24 | } 25 | 26 | notifyError(error) { 27 | this.#isVisible.value = false; 28 | this.#message.value = handleError(error); 29 | this.#isSuccess.value = false; 30 | this.#isVisible.value = true; 31 | } 32 | 33 | notifySuccess(message) { 34 | this.#isVisible.value = false; 35 | this.#message.value = message; 36 | this.#isSuccess.value = true; 37 | this.#isVisible.value = true; 38 | } 39 | } 40 | 41 | export default function () { 42 | return new NotificationModalHelper(); 43 | } 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | emulator: 5 | build: 6 | context: . 7 | dockerfile: docker/emulator/Dockerfile 8 | container_name: wadl_firebase 9 | restart: unless-stopped 10 | ports: 11 | - 9099:9099 12 | - 5001:5001 13 | - 8080:8080 14 | - 8085:8085 15 | - 9199:9199 16 | - 4000:4000 17 | - 9005:9005 18 | volumes: 19 | - ./Firebase:/app 20 | command: sleep 9999999 21 | -------------------------------------------------------------------------------- /docker/emulator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # java dependencies 4 | RUN apt-get update && \ 5 | apt-get install -y openjdk-11-jre-headless && \ 6 | apt-get clean; 7 | 8 | # firebase cli 9 | RUN yarn global add firebase-tools 10 | 11 | # firebase emulators 12 | RUN firebase setup:emulators:database 13 | RUN firebase setup:emulators:firestore 14 | RUN firebase setup:emulators:pubsub 15 | RUN firebase setup:emulators:storage 16 | RUN firebase setup:emulators:ui 17 | 18 | WORKDIR /app 19 | COPY /Firebase ./ 20 | 21 | WORKDIR /app/functions 22 | RUN npm install 23 | 24 | WORKDIR /app 25 | -------------------------------------------------------------------------------- /docs/docs/images/ShortLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/ShortLogo.png -------------------------------------------------------------------------------- /docs/docs/images/TextlessLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/TextlessLogo.png -------------------------------------------------------------------------------- /docs/docs/images/TextlessLogoYellow-Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/TextlessLogoYellow-Black.png -------------------------------------------------------------------------------- /docs/docs/images/TextlessLogoYellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/TextlessLogoYellow.png -------------------------------------------------------------------------------- /docs/docs/images/TextlessTransparentLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/TextlessTransparentLogo.png -------------------------------------------------------------------------------- /docs/docs/images/TransparentLongLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/TransparentLongLogo.png -------------------------------------------------------------------------------- /docs/docs/images/firebaselogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/firebaselogo.png -------------------------------------------------------------------------------- /docs/docs/images/nuxtlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codersforcauses/wadl/8f1115fbd4e219badf312a13df08f001747c9b34/docs/docs/images/nuxtlogo.png -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | ## Tech Stack 2 | 3 | #### Backend 4 | 5 | ![Firebase](images/firebaselogo.png) 6 | 7 | #### Frontend 8 | 9 | Nuxt 3, Vue 3 10 | ![Nuxt](images/nuxtlogo.png) 11 | -------------------------------------------------------------------------------- /docs/docs/installation-guide.md: -------------------------------------------------------------------------------- 1 | ## Run Documentation 2 | 3 | `mkdocs serve` 4 | 5 | ## Run Emulator and Development Server 6 | 7 | Requires java SDK and firebase-tools installed globally 8 | 9 | #### Mac Setup 10 | 11 | ``` 12 | yarn dev:emu 13 | ``` 14 | 15 | #### Windows Setup 16 | 17 | ``` 18 | yarn firebase 19 | ``` 20 | 21 | and in a separate terminal 22 | 23 | ``` 24 | yarn dev 25 | ``` 26 | 27 | OR 28 | 29 | ``` 30 | yarn dev:docker 31 | ``` 32 | 33 | Requires docker and you will need to authenticate your firebase account while the container briefly is put into sleep mode 34 | -------------------------------------------------------------------------------- /docs/docs/tournament-setup.md: -------------------------------------------------------------------------------- 1 | ## Create Tournament 2 | 3 | To create a new tournament, go to the admin dashboard and find the tournaments section. You'll need to create a new tournament before opening registrations, where you can input the following details: 4 | 5 | - Tournament Name 6 | - Tournament Short Name 7 | - Levels (Novice, Junior, Senior) 8 | - Number of Non-preliminary Rounds 9 | 10 | When a tournament is created it will automatically set registrations to opened. 11 | 12 | --- 13 | 14 | ## Setup Venues 15 | 16 | Venues must be added on `admin/venues` so that they can be added to the `manage tournament` page 17 | 18 | --- 19 | 20 | ## Manage Tournament 21 | 22 | To access a list of tournaments, go to the admin dashboard and navigate to the tournaments. Click on the tournament row you're interested in to view its management dashboard. Here, you can set up your round dates, venues, and divisions, as well as view all the relevant details related to the tournament. 23 | 24 | #### Viewing Registered Teams 25 | 26 | You will be able to view all registered teams for the a level by clicking `teams` 27 | 28 | #### Managing Level Divisions 29 | 30 | You will be navigated to a `Manage Divisions` page where you can add new divisions to a level and assign the teams based on venue preferences. This could be done after adding venues to the tournament 31 | 32 | #### View Level Draw 33 | 34 | `Coming soon` - A page that displays a list of debates after fixtures have been generated. 35 | 36 | --- 37 | 38 | ### Adding Venues 39 | 40 | Adding venues to your tournament is crucial in order to properly assign teams to divisions based on their venue preferences. To add venues, simply click the plus icon and choose the week and day for which you want to add a venue. You can select multiple venues for that specific time slot. Keep in mind that if the week and/or day is different, you will have to create a new venue. 41 | 42 | --- 43 | 44 | ### Adding Round Dates 45 | 46 | To add dates for a round, simply click on the plus icon and type the specific round you wish to add dates for. Each round should be added separately, and you can specify the following dates for each round: 47 | 48 | - Tuesday Week 1 49 | - Tuesday Week 2 50 | - Wednesday Week 1 51 | - Wednesday Week 2 52 | 53 | --- 54 | 55 | ### Tournament Status 56 | 57 | Clicking on `Previous Stage` or `Next Stage` will change the status. 58 | 59 | - `Open` - Registration is opened and teams can register 60 | - `Closed` - Registration is closed and teams cannot be registered 61 | - `Running` - Fixtures have been generated and is ready for the public to view 62 | - `Complete` - Tournament has been completed and no longer displays to the public 63 | 64 | --- 65 | 66 | ## Features Coming Soon 67 | 68 | - Generate Fixtures 69 | - Advance the current round 70 | - Release Results for the current round 71 | -------------------------------------------------------------------------------- /docs/docs/user-guide.md: -------------------------------------------------------------------------------- 1 | The WADL Application is currently `in development` and the released version is in beta stage, which may result in unexpected bugs. If you encounter any issues while using the application, please don't hesitate to reach out to the WADL staff for assistance. Your feedback is also highly appreciated as we work to improve the application. Over the next few months, new features will be added to enhance your experience. 2 | 3 | [View the WADL Website](https://wadl.vercel.app) 4 | 5 | ## Creating a Team Coordinator Account 6 | 7 | To create a user account, simply follow the registration process. Once you have completed the necessary information, your account will be pending approval by an administrator. Please allow up to `1-3 business days` for approval. Note that you will not receive any notification once your account has been approved. This feature is under development. 8 | 9 | --- 10 | 11 | ## Setting Up Your Institution 12 | 13 | Before registering your teams for a tournament, you'll need to create a `School Profile`. If there are multiple team coordinators, this profile may have already been created. You can select your school profile from the school name dropdown menu, or create a new profile by typing the school name and filling in the rest of the information. To link your account with the school profile, make sure to submit any changes you make. 14 | 15 | Information required: 16 | 17 | 1. `School Name`: A name that represents your school 18 | 2. `School Name Abbreviation`: A short name that represents your school 19 | 3. `School Email`: A contact email for WADL staff 20 | 4. `School Phone Number`: A phone number for WADL staff 21 | 22 | Once your school profile has been created, you'll have access to the `Team Registration` button in the navigation dropdown menu and the `Teams` page. 23 | 24 | --- 25 | 26 | ## Team Registration 27 | 28 | It is advised that you only use one team coordinator to register your teams in order to prevent duplications. Upon completion, all team coordinators associated with the same institution will be able to view the registered teams on the `Teams` page. 29 | 30 | Additionally, you'll have the option to edit preferences for each team individually and even add more teams after your initial registration, providing you with a flexible and organized system for managing your teams. 31 | 32 | Information required: 33 | 34 | 1. `Tournament Name`: A list of tournament that are currently open for registration will be displayed in a dropdown menu 35 | 2. `Level`: Select the levels you want to participate in (Novice, Junior, Senior) 36 | 3. `Number of Teams`: Indicate the total number of teams for each level 37 | 4. `Week Preference`: Specify which week you would prefer to compete in (Week 1, Week 2, Either) for each level. 38 | 5. `Tuesday Allocation`: Choose the number of teams you would like to compete on a Tuesday for each level 39 | 6. `Wednesday Allocation`: Choose the number of teams you would like to compete on a Wednesday for each level. Note: You can allocate teams for both Tuesday and Wednesday. 40 | 7. `Venue Preferences`: Provide your top 3 venue preferences or indicate if you have no preference 41 | 8. `Notes`: Any additional notes you might like to include for WADL staff 42 | 43 | --- 44 | 45 | ## Features Coming Soon 46 | 47 | 1. Change personal information and password 48 | 2. View the division that has been assigned to all your teams 49 | 3. Ability to remove a team from registration 50 | 4. View team debate matchups 51 | 5. Ability to postpone debate and forfeit 52 | 6. Email notifications on account approval and possibly other situations 53 | -------------------------------------------------------------------------------- /docs/docs/using-mkdocs.txt: -------------------------------------------------------------------------------- 1 | For full documentation visit: 2 | 3 | - mkdocs.org (mkdocs.org) for the generic MkDocs 4 | - PyMdown Extensions (https://facelessuser.github.io/pymdown-extensions/) for the different extensions that are installed 5 | - MkDocs Material (https://squidfunk.github.io/mkdocs-material/) for the customisation of the web server documentation. 6 | 7 | 8 | Installation 9 | $ pip install -r requirements.txt - for Documentation requirements 10 | 11 | 12 | Commands 13 | $ mkdocs build - Build the documentation site. 14 | $ mkdocs serve - Start the live-reloading docs server. Very helpful when you want to take a look at the docs before deploying. 15 | 16 | $ mkdocs -h - Print help message and exit. 17 | $ mkdocs gh-deploy - Deploy in github pages -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # This configuration can be changed from this https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ 2 | # I will be putting comments on certain places where necessary 3 | 4 | site_name: WADL Project Documentation 5 | repo_url: https://github.com/codersforcauses/wadl/ 6 | 7 | # This uri refers to the github path to edit, change this if you branch is different in name 8 | edit_uri: edit/main/docs/docs/ 9 | theme: 10 | name: material 11 | palette: 12 | - media: "(prefers-color-scheme: light)" # below here is a palette for light mode 13 | primary: yellow 14 | scheme: default 15 | toggle: 16 | icon: material/toggle-switch-off-outline 17 | name: Switch to dark mode 18 | - media: "(prefers-color-scheme: dark)" # below here is a palette for dark mode 19 | primary: yellow 20 | scheme: slate 21 | toggle: 22 | icon: material/toggle-switch 23 | name: Switch to light mode 24 | 25 | logo: images/TextlessTransparentLogo.png 26 | favicon: images/TextlessTransparentLogo.png 27 | features: # Refer to https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/ 28 | - navigation.instant 29 | # - navigation.tabs 30 | plugins: 31 | - search 32 | - mermaid2: 33 | version: 8.8.2 34 | arguments: 35 | theme: white # change this as you see fit - ["white", "dark"] 36 | 37 | # These are markdown extensions I have included that makes the documentation look nicer 38 | markdown_extensions: 39 | - admonition 40 | - attr_list 41 | - pymdownx.arithmatex: 42 | generic: true 43 | - pymdownx.details 44 | - pymdownx.smartsymbols 45 | - pymdownx.highlight: 46 | use_pygments: true 47 | linenums: true 48 | - pymdownx.tabbed 49 | - footnotes 50 | - pymdownx.critic 51 | - attr_list 52 | - def_list 53 | - pymdownx.tasklist 54 | - pymdownx.keys 55 | - pymdownx.mark 56 | - pymdownx.emoji: 57 | emoji_index: !!python/name:materialx.emoji.twemoji 58 | emoji_generator: !!python/name:materialx.emoji.to_svg 59 | - toc: 60 | permalink: true 61 | toc_depth: 4 62 | - codehilite 63 | - pymdownx.snippets: 64 | base_path: docs 65 | - pymdownx.superfences: 66 | custom_fences: 67 | - name: mermaid 68 | class: mermaid 69 | format: 70 | !!python/name:mermaid2.fence_mermaid # This is the extra javascript included in the documentation 71 | 72 | 73 | extra_javascript: 74 | - https://unpkg.com/mermaid@8.8.2/dist/mermaid.min.js 75 | - https://polyfill.io/v3/polyfill.min.js?features=es6 76 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 77 | 78 | # This is where you adjust the hierarchy if the documentation 79 | # You can erase this if you want. If you erase this, Mkdocs will alphabetically sort your documentation 80 | nav: 81 | - Home: index.md 82 | - Installation Guide: installation-guide.md 83 | - Tournament Setup: tournament-setup.md 84 | - Team Coordinator Guide: user-guide.md 85 | - Components: components.md 86 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material 2 | mkdocs-mermaid2-plugin -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /middleware/adjudicator.js: -------------------------------------------------------------------------------- 1 | import { useUserStore } from "../stores/user"; 2 | import { defineNuxtRouteMiddleware, navigateTo } from "#imports"; 3 | 4 | export default defineNuxtRouteMiddleware(async (_to, _from) => { 5 | if (process.server) { 6 | const userStore = await useUserStore(); 7 | if (userStore.role !== "Ajudicator") { 8 | return navigateTo({ path: "/" }); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /middleware/admin.js: -------------------------------------------------------------------------------- 1 | import { useUserStore } from "../stores/user"; 2 | import { defineNuxtRouteMiddleware, navigateTo } from "#imports"; 3 | 4 | export default defineNuxtRouteMiddleware(async (_to, _from) => { 5 | if (process.server) { 6 | const userStore = await useUserStore(); 7 | if (userStore.role !== "Admin") { 8 | return navigateTo({ path: "/" }); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /middleware/team.js: -------------------------------------------------------------------------------- 1 | import { useUserStore } from "../stores/user"; 2 | import { defineNuxtRouteMiddleware, navigateTo } from "#imports"; 3 | 4 | export default defineNuxtRouteMiddleware(async (_to, _from) => { 5 | if (process.server) { 6 | const userStore = await useUserStore(); 7 | if (userStore.role !== "Team Coordinator") { 8 | return navigateTo({ path: "/" }); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /misc/firebaseHelpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts the `code` attribute of a Firebase error into a human-readable message. 3 | * @param {String} code The `code` property of an FirebaseError. 4 | * @returns {String} A human readable error message. 5 | */ 6 | function errorCodeToMessage(code) { 7 | switch (code) { 8 | case "auth/user-not-found": 9 | return "Account not found, try again with a new account"; 10 | case "auth/email-already-in-use": 11 | return "E-mail already in use"; 12 | case "auth/email-already-exists": 13 | return "E-mail already exists"; 14 | case "auth/network-request-failed": 15 | return "Network Failed, Please try again"; 16 | case "auth/wrong-password": 17 | return "Incorrect Password or Email"; 18 | default: 19 | return "Encountered an error"; 20 | } 21 | } 22 | 23 | /** 24 | * Converts a firebase or non-firebase error into a human readable message for the end-user. 25 | * @param {Error | import("firebase-admin").FirebaseError} error the error. 26 | * @returns {String} the error as a human readable message. 27 | */ 28 | export function handleError(error) { 29 | if (error.code) { 30 | return errorCodeToMessage(error.code); 31 | } else if (error.message) { 32 | return error.message; 33 | } else if (error === "tie-occured") { 34 | return "A tie occurred, please resolve the tie before submitting."; 35 | } else { 36 | return "An error occurred."; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt/config"; 2 | 3 | // https://nuxt.com/docs/api/configuration/nuxt-config 4 | export default defineNuxtConfig({ 5 | build: { 6 | transpile: ["@heroicons/vue"], 7 | }, 8 | modules: ["@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt"], 9 | css: ["~/assets/css/main.css"], 10 | postcss: { 11 | plugins: { 12 | tailwindcss: {}, 13 | autoprefixer: {}, 14 | }, 15 | }, 16 | app: { 17 | head: { 18 | link: [ 19 | { 20 | rel: "stylesheet", 21 | href: "https://fonts.googleapis.com/css2?family=Jost&family=Lato&family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap&family=Carter+One&display=swap", 22 | }, 23 | ], 24 | }, 25 | }, 26 | runtimeConfig: { 27 | // note: nuxt automatically converts .env vals to camelcase. 28 | public: { 29 | firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY || "", 30 | firebaseAuthDomain: process.env.NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN || "", 31 | firebaseProjectId: process.env.NUXT_PUBLIC_FIREBASE_PROJECT_ID || "", 32 | firebaseStorageBucket: 33 | process.env.NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET || "", 34 | firebaseMessagingSenderId: 35 | process.env.NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || "", 36 | firebaseAppId: process.env.NUXT_PUBLIC_FIREBASE_APP_ID || "", 37 | firebaseMode: process.env.NUXT_PUBLIC_FIREBASE_MODE || "", 38 | }, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "dev:emu": "yarn firebase & (sleep 15 && yarn dev)", 7 | "dev:emu-w": "yarn concurrently \"yarn firebase-w\" \"yarn dev\"", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "lint": "eslint --ignore-path .gitignore .", 12 | "lint:fix": "eslint --ignore-path .gitignore --fix .", 13 | "firebase": "yarn emu:stop & cd Firebase && firebase emulators:start --import=dummyData", 14 | "firebase-w": "yarn emu:stop-w & cd Firebase && firebase emulators:start --import=dummyData", 15 | "emu:stop": "lsof -ti :9099 -ti :5001 -ti :8080 -ti :8085 -ti :9199 | xargs kill", 16 | "emu:stop-w": "yarn kill-port 9099 5001 8080 8085 9199", 17 | "dev:docker": "docker-compose up -d && yarn dev" 18 | }, 19 | "devDependencies": { 20 | "@pinia-plugin-persistedstate/nuxt": "^1.0.0", 21 | "@vue/eslint-config-standard": "^8.0.1", 22 | "autoprefixer": "^10.4.13", 23 | "concurrently": "^7.6.0", 24 | "eslint": "^8.0.1", 25 | "eslint-config-prettier": "^8.5.0", 26 | "eslint-config-standard": "^17.0.0", 27 | "eslint-plugin-import": "^2.25.2", 28 | "eslint-plugin-n": "^15.0.0", 29 | "eslint-plugin-prettier": "^4.2.1", 30 | "eslint-plugin-promise": "^6.0.0", 31 | "eslint-plugin-vue": "^9.8.0", 32 | "kill-port": "^2.0.1", 33 | "nuxt": "3.0.0", 34 | "postcss": "^8.4.19", 35 | "prettier": "^2.8.0", 36 | "tailwindcss": "^3.2.4" 37 | }, 38 | "dependencies": { 39 | "@google-cloud/billing": "^3.1.3", 40 | "@heroicons/vue": "^2.0.13", 41 | "@pinia/nuxt": "^0.4.6", 42 | "@vueuse/components": "^9.10.0", 43 | "@vueuse/core": "^9.10.0", 44 | "firebase": "^9.14.0", 45 | "firebase-admin": "^11.5.0", 46 | "firebase-functions": "^4.2.1", 47 | "papaparse": "^5.4.1", 48 | "pinia": "^2.0.27", 49 | "uuid": "^9.0.0", 50 | "vue": "^3.2.45" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pages/adjudicator.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /pages/adjudicator/[tournamentId]/index.vue: -------------------------------------------------------------------------------- 1 | 163 | 164 | 207 | -------------------------------------------------------------------------------- /pages/adjudicator/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | -------------------------------------------------------------------------------- /pages/admin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /pages/admin/contacts.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 87 | -------------------------------------------------------------------------------- /pages/admin/create-admin.vue: -------------------------------------------------------------------------------- 1 | 46 | 100 | -------------------------------------------------------------------------------- /pages/admin/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 106 | -------------------------------------------------------------------------------- /pages/admin/institutions.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 230 | -------------------------------------------------------------------------------- /pages/admin/signup-requests.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 150 | -------------------------------------------------------------------------------- /pages/admin/teams.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 91 | -------------------------------------------------------------------------------- /pages/admin/tournaments/[tournamentId]/leaderboard/index.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 104 | -------------------------------------------------------------------------------- /pages/admin/tournaments/[tournamentId]/matchups-upload.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 126 | -------------------------------------------------------------------------------- /pages/coordinator.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /pages/coordinator/draw.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /pages/coordinator/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 93 | -------------------------------------------------------------------------------- /pages/coordinator/institutions.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 161 | -------------------------------------------------------------------------------- /pages/coordinator/teams/[tournamentId].vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 237 | -------------------------------------------------------------------------------- /pages/coordinator/userinformation.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 160 | -------------------------------------------------------------------------------- /pages/fixtures/[tournamentId].vue: -------------------------------------------------------------------------------- 1 | 154 | 155 | 189 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 39 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 95 | -------------------------------------------------------------------------------- /pages/resetpassword.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 70 | -------------------------------------------------------------------------------- /pages/signup.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 177 | -------------------------------------------------------------------------------- /plugins/1.firebase.client.js: -------------------------------------------------------------------------------- 1 | import { 2 | getAuth, 3 | connectAuthEmulator, 4 | onAuthStateChanged, 5 | } from "firebase/auth"; 6 | import { defineNuxtPlugin, useRuntimeConfig, useCookie } from "#imports"; 7 | import { useUserStore } from "../stores/user"; 8 | import { connectFirestoreEmulator, getFirestore } from "firebase/firestore"; 9 | import { initializeApp } from "firebase/app"; 10 | import { connectFunctionsEmulator, getFunctions } from "firebase/functions"; 11 | 12 | export default defineNuxtPlugin(async (nuxtApp) => { 13 | const config = useRuntimeConfig(); 14 | const firebaseConfig = { 15 | apiKey: config.firebaseApiKey, 16 | authDomain: config.firebaseAuthDomain, 17 | projectId: config.firebaseProjectId, 18 | storageBucket: config.firebaseStorageBucket, 19 | messagingSenderId: config.firebaseMessagingSenderId, 20 | appId: config.firebaseAppId, 21 | mode: config.firebaseMode, 22 | }; 23 | 24 | const app = initializeApp(firebaseConfig, "client"); 25 | const firestore = getFirestore(app); 26 | const auth = getAuth(app); 27 | const functions = getFunctions(app); 28 | 29 | if (config.firebaseMode === "dev") { 30 | connectFirestoreEmulator(firestore, "localhost", 8080); 31 | connectAuthEmulator(auth, "http://localhost:9099"); 32 | connectFunctionsEmulator(functions, "localhost", 5001); 33 | } 34 | 35 | // AUTH FUNCTIONS 36 | 37 | const userStore = await useUserStore(); 38 | 39 | auth.onIdTokenChanged(async (user) => { 40 | // on sign-in, sign-out, and token refresh. 41 | const tokenCookie = await useCookie("auth-token", { 42 | default: () => { 43 | return ""; 44 | }, 45 | watch: true, // keeps cookie sync'ed 46 | maxAge: 3600, // firebase cookies expire in an hour. 47 | }); 48 | 49 | if (user) { 50 | tokenCookie.value = await user.getIdToken(true); 51 | } else { 52 | // logged out. 53 | tokenCookie.value = ""; 54 | userStore.clearStore(); 55 | } 56 | }); 57 | 58 | onAuthStateChanged(auth, (user) => { 59 | // on log in or log out. 60 | if (user) { 61 | userStore.setUser(user); 62 | } else { 63 | userStore.clearStore(); 64 | } 65 | }); 66 | 67 | return { 68 | provide: { 69 | clientFirestore: firestore, 70 | clientAuth: auth, 71 | }, 72 | }; 73 | }); 74 | -------------------------------------------------------------------------------- /plugins/1.firebase.server.js: -------------------------------------------------------------------------------- 1 | import { getAuth } from "firebase-admin/auth"; 2 | import { defineNuxtPlugin } from "#imports"; 3 | import admin from "firebase-admin"; 4 | import { getFirestore } from "firebase-admin/firestore"; 5 | import { getApp, getApps, initializeApp } from "firebase-admin/app"; 6 | 7 | export default defineNuxtPlugin(async (nuxtApp) => { 8 | const serviceAccountCredentials = admin.credential.cert( 9 | JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_CONFIG) 10 | ); 11 | const initializedApps = getApps(); 12 | const app = initializedApps.find((app) => app.name === "server") 13 | ? getApp("server") 14 | : initializeApp({ credential: serviceAccountCredentials }, "server"); 15 | const firestore = getFirestore(app); 16 | const auth = getAuth(app); 17 | 18 | return { 19 | provide: { 20 | serverFirestore: firestore, 21 | serverAuth: auth, 22 | }, 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /plugins/2.firebase_auth.server.js: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin, useNuxtApp, useCookie } from "#imports"; 2 | import { useUserStore } from "../stores/user"; 3 | 4 | export default defineNuxtPlugin(async (nuxtApp) => { 5 | const { $serverAuth } = useNuxtApp(); 6 | 7 | const userStore = await useUserStore(); 8 | 9 | const token = await useCookie(`auth-token`); 10 | if (token.value) { 11 | try { 12 | const result = await $serverAuth.verifyIdToken(token.value); 13 | await userStore.setUser(result); 14 | } catch { 15 | await userStore.clearStore(); 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /server/api/create-admin.post.js: -------------------------------------------------------------------------------- 1 | import { getApp, getApps, initializeApp } from "firebase-admin/app"; 2 | import admin from "firebase-admin"; 3 | import { getAuth } from "firebase-admin/auth"; 4 | import { getFirestore } from "firebase-admin/firestore"; 5 | import { defineEventHandler, readBody, createError } from "#imports"; 6 | 7 | export default defineEventHandler(async (event) => { 8 | const { adminToken, newUser } = await readBody(event); 9 | 10 | if (!adminToken || !newUser) { 11 | throw createError({ 12 | statusCode: 400, 13 | }); 14 | } 15 | 16 | const serviceAccountCredentials = admin.credential.cert( 17 | JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_CONFIG) 18 | ); 19 | 20 | const initializedApps = getApps(); 21 | const app = initializedApps.find((app) => app.name === "server") 22 | ? getApp("server") 23 | : initializeApp({ credential: serviceAccountCredentials }, "server"); 24 | 25 | const auth = getAuth(app); 26 | const firestore = getFirestore(app); 27 | 28 | try { 29 | const claims = await auth.verifyIdToken(adminToken); 30 | const adminUser = await firestore.runTransaction(async (transaction) => { 31 | const snap = await transaction.get( 32 | firestore.collection("users").doc(claims.uid) 33 | ); 34 | 35 | return snap.data(); 36 | }); 37 | 38 | if (adminUser.role !== "Admin") { 39 | throw createError({ 40 | statusCode: 401, 41 | statusMessage: "Unauthorized", 42 | }); 43 | } 44 | 45 | const authUser = await auth.createUser({ 46 | email: newUser.email, 47 | password: newUser.password, 48 | }); 49 | 50 | await firestore.runTransaction(async (transaction) => { 51 | transaction.set(firestore.collection("users").doc(authUser.uid), { 52 | role: "Admin", 53 | requesting: false, 54 | firstName: newUser.firstName, 55 | lastName: newUser.lastName, 56 | phoneNumber: newUser.phoneNumber, 57 | email: newUser.email, 58 | }); 59 | }); 60 | return true; 61 | } catch (err) { 62 | throw createError({ 63 | statusCode: 400, 64 | statusMessage: err, 65 | }); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /server/api/send-email.post.js: -------------------------------------------------------------------------------- 1 | import { getApp, getApps, initializeApp } from "firebase-admin/app"; 2 | import admin from "firebase-admin"; 3 | import { getAuth } from "firebase-admin/auth"; 4 | import { getFirestore } from "firebase-admin/firestore"; 5 | import { defineEventHandler, readBody, createError } from "#imports"; 6 | 7 | export default defineEventHandler(async (event) => { 8 | const { userInfo, emailStructure } = await readBody(event); 9 | if (!userInfo || !emailStructure) { 10 | throw createError({ 11 | statusCode: 400, 12 | }); 13 | } 14 | 15 | const serviceAccountCredentials = admin.credential.cert( 16 | JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_CONFIG) 17 | ); 18 | 19 | const initializedApps = getApps(); 20 | const app = initializedApps.find((app) => app.name === "server") 21 | ? getApp("server") 22 | : initializeApp({ credential: serviceAccountCredentials }, "server"); 23 | 24 | const auth = getAuth(app); 25 | const firestore = getFirestore(app); 26 | 27 | try { 28 | if (emailStructure.name === "resetPassword") { 29 | const resetLink = await auth.generatePasswordResetLink( 30 | emailStructure.data.userEmail.email 31 | ); 32 | await firestore 33 | .collection("mail") 34 | .add({ 35 | to: [userInfo.email], 36 | message: {}, 37 | template: { 38 | name: emailStructure.name, 39 | data: { 40 | link: resetLink, 41 | }, 42 | }, 43 | }) 44 | .then(() => console.log("Queued email for delivery!")); 45 | return true; 46 | } 47 | if (emailStructure.name === "approveUser") { 48 | await firestore 49 | .collection("mail") 50 | .add({ 51 | to: [userInfo.email], 52 | message: {}, 53 | template: { 54 | name: emailStructure.name, 55 | data: { 56 | name: emailStructure.data.name, 57 | role: emailStructure.data.role, 58 | }, 59 | }, 60 | }) 61 | .then(() => console.log("Queued email for delivery!")); 62 | return true; 63 | } 64 | } catch (err) { 65 | throw createError({ 66 | statusCode: 400, 67 | statusMessage: err, 68 | }); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /stores/adjudicator.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useNuxtApp } from "#imports"; 3 | import { collection, where, getDocs, query } from "firebase/firestore"; 4 | 5 | export const useAdjudicatorStore = defineStore("adjudicator", { 6 | state: () => { 7 | return { 8 | adjudicators: [], 9 | }; 10 | }, 11 | getters: { 12 | getAdjudicators() { 13 | return this.adjudicators; 14 | }, 15 | }, 16 | actions: { 17 | async fetchAdjudicators() { 18 | const { $clientFirestore } = useNuxtApp(); 19 | const ref = collection($clientFirestore, "users"); 20 | const q = query(ref, where("role", "==", "Adjudicator")); 21 | const querySnapshot = await getDocs(q); 22 | 23 | querySnapshot.forEach((doc) => { 24 | const fullName = doc.data().firstName + " " + doc.data().lastName; 25 | this.adjudicators.push(fullName); 26 | }); 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /stores/admin.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useNuxtApp } from "#imports"; 3 | import { 4 | collection, 5 | updateDoc, 6 | doc, 7 | deleteDoc, 8 | getDocs, 9 | } from "firebase/firestore"; 10 | 11 | export const useAdminStore = defineStore("admin", { 12 | state() { 13 | return { 14 | users: [], 15 | }; 16 | }, 17 | 18 | getters: { 19 | getRequestingUsers() { 20 | return this.users.filter((user) => user.requesting === true); 21 | }, 22 | getApprovedUsers() { 23 | return this.users.filter((user) => user.requesting === false); 24 | }, 25 | }, 26 | 27 | actions: { 28 | async createAdmin(user) { 29 | const { $clientAuth } = useNuxtApp(); 30 | const adminToken = await $clientAuth.currentUser.getIdToken(); 31 | 32 | // @es-lint ignore 33 | await $fetch("/api/create-admin", { 34 | method: "post", 35 | body: { 36 | adminToken, 37 | newUser: user, 38 | }, 39 | }); 40 | }, 41 | 42 | async fetchUsers() { 43 | const { $clientFirestore } = useNuxtApp(); 44 | const snap = await getDocs(collection($clientFirestore, "users")); 45 | this.users = snap.docs.map((doc) => { 46 | const user = doc.data(); 47 | 48 | return { 49 | email: user.email, 50 | id: doc.id, 51 | firstName: user.firstName, 52 | lastName: user.lastName, 53 | phoneNumber: user.phoneNumber, 54 | institutions: user.institutions, 55 | requesting: user.requesting, 56 | role: user.role, 57 | school: user.school, 58 | }; 59 | }); 60 | }, 61 | 62 | async acceptUser(user) { 63 | const { $clientFirestore } = useNuxtApp(); 64 | const template = { 65 | name: "approveUser", 66 | data: { 67 | name: user.firstName, 68 | role: user.role, 69 | }, 70 | }; 71 | await updateDoc(doc($clientFirestore, "users", user.id), { 72 | requesting: false, 73 | role: user.role, 74 | }); 75 | this.users = this.users.filter((u) => u.id !== user.id); 76 | await $fetch("/api/send-email", { 77 | method: "post", 78 | body: { 79 | userInfo: user, 80 | emailStructure: template, 81 | }, 82 | }); 83 | }, 84 | 85 | async denyUser(user) { 86 | const { $clientFirestore } = useNuxtApp(); 87 | await deleteDoc(doc($clientFirestore, "users", user.id), { 88 | requesting: null, 89 | role: user.role, 90 | }); 91 | this.users = this.users.filter((u) => u.id !== user.id); 92 | }, 93 | async clearStore() { 94 | this.searchTerm = ""; 95 | this.users = []; 96 | this.adjudicators = []; 97 | }, 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /stores/leaderboard.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useNuxtApp } from "#imports"; 3 | import { getDoc, doc, updateDoc } from "firebase/firestore"; 4 | 5 | export const useLeaderboardStore = defineStore("leaderboard", { 6 | state: () => { 7 | return { 8 | novice: [], 9 | senior: [], 10 | junior: [], 11 | }; 12 | }, 13 | getters: {}, 14 | actions: { 15 | async getLeaderboard(torniID) { 16 | this.novice.pop(); 17 | this.senior.pop(); 18 | this.junior.pop(); 19 | // 20 | const { $clientFirestore } = useNuxtApp(); 21 | if (!$clientFirestore) return; 22 | const ref = doc($clientFirestore, "leaderboard", torniID); 23 | const querySnapshot = await getDoc(ref); 24 | this.junior.push(querySnapshot.data().junior); 25 | this.senior.push(querySnapshot.data().senior); 26 | this.novice.push(querySnapshot.data().novice); 27 | }, 28 | async updateLeaderboard(matchup, tournamentID) { 29 | const { $clientFirestore } = useNuxtApp(); 30 | if (!$clientFirestore) return; 31 | const ref = doc($clientFirestore, "leaderboard", tournamentID); 32 | const win = 2; 33 | const loss = 1; 34 | const level = matchup.level; 35 | const affirmativeTeam = matchup.affirmativeTeam; 36 | const negativeTeam = matchup.negativeTeam; 37 | const division = matchup.division; 38 | const scoresheet = matchup.scoresheet; 39 | const affirmativeTeamTotal = scoresheet.affirmativeTeam.total; 40 | const negativeTeamTotal = scoresheet.negativeTeam.total; 41 | let winningTeam = null; 42 | let losingTeam = null; 43 | if (affirmativeTeamTotal > negativeTeamTotal) { 44 | console.log("Affirmative Team Wins", affirmativeTeam); 45 | winningTeam = affirmativeTeam; 46 | losingTeam = negativeTeam; 47 | } else { 48 | console.log("Negative Team Wins", negativeTeam); 49 | winningTeam = negativeTeam; 50 | losingTeam = affirmativeTeam; 51 | } 52 | this[level][0][division - 1].forEach((team) => { 53 | if (team.name === winningTeam) { 54 | team.points += win; 55 | } else if (team.name === losingTeam) { 56 | team.points += loss; 57 | } 58 | }); 59 | await updateDoc(ref, { 60 | junior: this.junior[0], 61 | senior: this.senior[0], 62 | novice: this.novice[0], 63 | }); 64 | }, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /stores/teams.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useNuxtApp } from "#imports"; 3 | import { 4 | collection, 5 | getDocs, 6 | query, 7 | where, 8 | writeBatch, 9 | doc, 10 | } from "firebase/firestore"; 11 | 12 | export const useTeamStore = defineStore("team", { 13 | state: () => { 14 | return { 15 | teams: [], 16 | divisions: new Map(), 17 | allocatedTeams: new Map(), 18 | unallocatedTeams: new Map(), 19 | }; 20 | }, 21 | getters: {}, 22 | actions: { 23 | async getTeams() { 24 | const { $clientFirestore } = useNuxtApp(); 25 | if (!$clientFirestore) return; 26 | const ref = collection($clientFirestore, "teams"); 27 | const querySnapshot = await getDocs(ref); 28 | querySnapshot.forEach((doc) => { 29 | const team = { 30 | id: doc.id, 31 | name: doc.data().name, 32 | level: doc.data().level, 33 | division: doc.data().division, 34 | timeslot: doc.data().timeslot, 35 | hasVenuePreference: doc.data().hasVenuePreference, 36 | venuePreference: doc.data().venuePreference, 37 | weekPreference: doc.data().weekPreference, 38 | allocatedTue: doc.data().allocatedTue, 39 | allocatedWed: doc.data().allocatedWed, 40 | notes: doc.data().notes, 41 | }; 42 | this.teams.push(team); 43 | }); 44 | }, 45 | async getTeamsbyTournament(id) { 46 | this.teams = []; 47 | const { $clientFirestore } = useNuxtApp(); 48 | if (!$clientFirestore) return; 49 | const ref = collection($clientFirestore, "teams"); 50 | const q = query(ref, where("tournamentId", "==", id)); 51 | const querySnapshot = await getDocs(q); 52 | querySnapshot.forEach((doc) => { 53 | const team = { 54 | id: doc.id, 55 | tournamentId: doc.data().tournamentId, 56 | institutionId: doc.data().institutionId, 57 | name: doc.data().name, 58 | level: doc.data().level, 59 | division: doc.data().division, 60 | timeslot: doc.data().timeslot, 61 | hasVenuePreference: doc.data().hasVenuePreference, 62 | venuePreference: doc.data().venuePreference, 63 | weekPreference: doc.data().weekPreference, 64 | allocatedTue: doc.data().allocatedTue, 65 | allocatedWed: doc.data().allocatedWed, 66 | notes: doc.data().notes, 67 | }; 68 | this.teams.push(team); 69 | }); 70 | }, 71 | sortTeamDivisionAllocation(level) { 72 | this.allocatedTeams = new Map(); 73 | this.unallocatedTeams = new Map(); 74 | this.getTeamsByLevel(level).forEach((team) => { 75 | if (team.division) { 76 | this.allocatedTeams.set(team.id, team); 77 | } else { 78 | this.unallocatedTeams.set(team.id, team); 79 | } 80 | }); 81 | }, 82 | getTeamsByLevel(level) { 83 | return this.teams.filter((t) => t.level === level); 84 | }, 85 | getNumberTeams(level) { 86 | return this.teams.filter((t) => t.level === level).length; 87 | }, 88 | async updateTeamDivision() { 89 | const { $clientFirestore } = useNuxtApp(); 90 | const batch = writeBatch($clientFirestore); 91 | this.allocatedTeams.forEach((team) => { 92 | const ref = doc(collection($clientFirestore, "teams"), team.id); 93 | batch.set(ref, team); 94 | }); 95 | await batch.commit(); 96 | }, 97 | }, 98 | }); 99 | -------------------------------------------------------------------------------- /stores/tournaments.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useNuxtApp } from "#imports"; 3 | import { 4 | collection, 5 | getDocs, 6 | addDoc, 7 | setDoc, 8 | doc, 9 | updateDoc, 10 | deleteDoc, 11 | } from "firebase/firestore"; 12 | 13 | export const useTournamentStore = defineStore("tournament", { 14 | state: () => { 15 | return { 16 | tournaments: [], 17 | filteredTournaments: [], 18 | hasTournaments: false, 19 | currentTournament: null, 20 | divisions: [], 21 | }; 22 | }, 23 | getters: { 24 | getRunning() { 25 | return this.tournaments.filter( 26 | (tournament) => tournament.status === "Running" 27 | ); 28 | }, 29 | getOpen() { 30 | return this.tournaments.filter( 31 | (tournament) => tournament.status === "Open" 32 | ); 33 | }, 34 | }, 35 | actions: { 36 | async getTournaments() { 37 | this.clearStore(); 38 | const { $clientFirestore } = useNuxtApp(); 39 | if (!$clientFirestore) return; 40 | const ref = collection($clientFirestore, "tournaments"); 41 | const querySnapshot = await getDocs(ref); 42 | querySnapshot.forEach((doc) => { 43 | const tournament = { 44 | id: doc.id, 45 | currentRound: doc.data().currentRound, 46 | levels: doc.data().levels, 47 | name: doc.data().name, 48 | numRounds: doc.data().numRounds, 49 | roundDates: doc.data().roundDates, 50 | shortName: doc.data().shortName, 51 | status: doc.data().status, 52 | venues: doc.data().venues, 53 | }; 54 | this.tournaments.push(tournament); 55 | }); 56 | }, 57 | async clearStore() { 58 | this.tournaments = []; 59 | this.filteredTournaments = []; 60 | this.hasTournaments = false; 61 | }, 62 | async createTournament(tournament) { 63 | const { $clientFirestore } = useNuxtApp(); 64 | const t = await addDoc(collection($clientFirestore, "tournaments"), { 65 | levels: tournament.levels, 66 | name: tournament.name, 67 | numRounds: tournament.numRounds, 68 | shortName: tournament.shortName, 69 | status: tournament.status, 70 | }); 71 | 72 | tournament.id = t.id; 73 | 74 | this.tournaments.push({ 75 | ...tournament, 76 | }); 77 | }, 78 | 79 | async editTournament(tournament) { 80 | const { $clientFirestore } = useNuxtApp(); 81 | await setDoc(doc($clientFirestore, "tournaments", tournament.id), { 82 | currentRound: tournament.currentRound, 83 | levels: tournament.levels, 84 | name: tournament.name, 85 | numRounds: tournament.numRounds, 86 | shortName: tournament.shortName, 87 | status: tournament.status, 88 | venues: tournament.venues, 89 | roundDates: tournament.roundDates, 90 | }); 91 | 92 | this.tournaments.forEach((t) => { 93 | if (t.id === tournament.id) { 94 | Object.assign(t, tournament); 95 | } 96 | }); 97 | }, 98 | getTournament(id) { 99 | this.currentTournament = []; 100 | this.currentTournament = this.tournaments.find((t) => t.id === id); 101 | if (!this.currentTournament.roundDates) { 102 | this.currentTournament.roundDates = []; 103 | } 104 | if (!this.currentTournament.venues) { 105 | this.currentTournament.venues = []; 106 | } 107 | }, 108 | getTournamentDivisionsByLevel(level) { 109 | const levels = this.currentTournament.levels.find( 110 | (l) => l.level === level 111 | ); 112 | this.divisions = levels.divisions; 113 | }, 114 | updateDivision(newVenue, division) { 115 | const divisionIndex = this.divisions.findIndex( 116 | (div) => div.division === division 117 | ); 118 | this.divisions[divisionIndex].venue = { ...newVenue }; 119 | }, 120 | async updateDivisionVenue(level) { 121 | const { $clientFirestore } = useNuxtApp(); 122 | const tournamentRef = doc( 123 | $clientFirestore, 124 | "tournaments", 125 | this.currentTournament.id 126 | ); 127 | 128 | const levels = this.currentTournament.levels; 129 | const index = levels.findIndex((l) => l.level === level); 130 | 131 | this.currentTournament.levels[index].divisions = this.divisions; 132 | await updateDoc(tournamentRef, this.currentTournament); 133 | }, 134 | async deleteTournament(id) { 135 | const { $clientFirestore } = useNuxtApp(); 136 | const ref = doc($clientFirestore, "tournaments", id); 137 | await deleteDoc(ref); 138 | const index = this.tournaments.findIndex((t) => { 139 | return id === t.id; 140 | }); 141 | this.tournaments.splice(index, 1); 142 | }, 143 | async filterTournaments(tournaments) { 144 | tournaments.forEach((id) => { 145 | const index = this.tournaments.findIndex((t) => { 146 | return id === t.id; 147 | }); 148 | this.filteredTournaments.push(this.tournaments[index]); 149 | }); 150 | this.hasTournaments = true; 151 | }, 152 | }, 153 | }); 154 | -------------------------------------------------------------------------------- /stores/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { doc, setDoc, getDoc, updateDoc } from "firebase/firestore"; 3 | import { 4 | signInWithEmailAndPassword, 5 | createUserWithEmailAndPassword, 6 | signOut, 7 | updatePassword, 8 | reauthenticateWithCredential, 9 | EmailAuthProvider, 10 | } from "firebase/auth"; 11 | import { useNuxtApp } from "#imports"; 12 | 13 | export const useUserStore = defineStore("user", { 14 | state() { 15 | return { 16 | auth: null, 17 | firstName: null, 18 | lastName: null, 19 | phoneNumber: null, 20 | email: null, 21 | role: null, 22 | requesting: null, 23 | token: null, 24 | institution: "", 25 | school: null, 26 | }; 27 | }, 28 | 29 | // persist only on client for now. 30 | persist: { 31 | key: "pinia-store", 32 | debug: true, 33 | persist: true, 34 | // eslint-disable-next-line no-undef 35 | storage: persistedState.localStorage, 36 | }, 37 | 38 | getters: {}, 39 | 40 | actions: { 41 | async registerUser(user) { 42 | const { $clientFirestore, $clientAuth } = useNuxtApp(); 43 | const authUser = await createUserWithEmailAndPassword( 44 | $clientAuth, 45 | user.email, 46 | user.password 47 | ); 48 | 49 | await setDoc(doc($clientFirestore, "users", authUser.user.uid), { 50 | role: user.role, 51 | requesting: true, 52 | firstName: user.firstName, 53 | lastName: user.lastName, 54 | phoneNumber: user.phoneNumber, 55 | email: user.email, 56 | school: user.school, 57 | }); 58 | }, 59 | 60 | async loginUser(user) { 61 | const { $clientAuth } = useNuxtApp(); 62 | await signInWithEmailAndPassword($clientAuth, user.email, user.password); 63 | }, 64 | 65 | async setUser(user) { 66 | if (user !== null) { 67 | let userDoc; 68 | if (!process.client) { 69 | const { $serverFirestore } = useNuxtApp(); 70 | userDoc = await $serverFirestore 71 | .collection("users") 72 | .doc(user.uid) 73 | .get(); 74 | } else { 75 | const { $clientFirestore } = useNuxtApp(); 76 | const docRef = doc($clientFirestore, "users", user.uid); 77 | userDoc = await getDoc(docRef); 78 | } 79 | 80 | if (!userDoc) { 81 | throw new Error("Could not find user document"); 82 | } 83 | 84 | const userInfo = userDoc.data(); 85 | this.auth = user; 86 | this.firstName = userInfo.firstName; 87 | this.lastName = userInfo.lastName; 88 | this.phoneNumber = userInfo.phoneNumber; 89 | this.email = userInfo.email; 90 | this.role = userInfo.requesting ? "" : userInfo.role; 91 | this.requesting = userInfo.requesting; 92 | this.institution = userInfo.institution; 93 | this.school = userInfo.school; 94 | } 95 | }, 96 | async resetPassword(email) { 97 | const template = { 98 | name: "resetPassword", 99 | data: { 100 | userEmail: email, 101 | }, 102 | }; 103 | await $fetch("/api/send-email", { 104 | method: "post", 105 | body: { 106 | userInfo: email, 107 | emailStructure: template, 108 | }, 109 | }); 110 | }, 111 | async updateUser(user) { 112 | const { $clientFirestore } = useNuxtApp(); 113 | const ref = doc($clientFirestore, "users", this.auth.uid); 114 | await updateDoc(ref, { 115 | firstName: user.firstName, 116 | lastName: user.lastName, 117 | phoneNumber: user.phoneNumber, 118 | }); 119 | }, 120 | async updateuserPassword(password) { 121 | const { $clientAuth } = useNuxtApp(); 122 | const cred = EmailAuthProvider.credential( 123 | this.email, 124 | password.currentPassword 125 | ); 126 | await reauthenticateWithCredential($clientAuth.currentUser, cred); 127 | await updatePassword($clientAuth.currentUser, password.password); 128 | }, 129 | async clearStore() { 130 | if (process.client) { 131 | const { $clientAuth } = useNuxtApp(); 132 | if ($clientAuth.currentUser) { 133 | try { 134 | await signOut($clientAuth); 135 | } catch {} 136 | } 137 | } 138 | 139 | this.auth = null; 140 | this.firstName = null; 141 | this.lastName = null; 142 | this.email = null; 143 | this.phoneNumber = null; 144 | this.role = null; 145 | this.requesting = null; 146 | this.institution = null; 147 | }, 148 | }, 149 | }); 150 | -------------------------------------------------------------------------------- /stores/venues.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useNuxtApp } from "#imports"; 3 | import { 4 | collection, 5 | getDocs, 6 | addDoc, 7 | setDoc, 8 | doc, 9 | deleteDoc, 10 | } from "firebase/firestore"; 11 | 12 | export const useVenueStore = defineStore("venue", { 13 | state: () => { 14 | return { 15 | venues: [], 16 | filteredVenues: [], 17 | tournamentVenues: [], 18 | }; 19 | }, 20 | getters: {}, 21 | actions: { 22 | async createVenue(venue) { 23 | const { $clientFirestore } = useNuxtApp(); 24 | if (!$clientFirestore) return; 25 | const newVenue = await addDoc(collection($clientFirestore, "venues"), { 26 | ...venue, 27 | }); 28 | this.venues.push({ ...venue, id: newVenue.id }); 29 | }, 30 | async editVenue(venue) { 31 | const { $clientFirestore } = useNuxtApp(); 32 | await setDoc(doc($clientFirestore, "venues", venue.id), { 33 | ...venue, 34 | }); 35 | this.venues.forEach((v) => { 36 | if (v.id === venue.id) { 37 | Object.assign(v, venue); 38 | } 39 | }); 40 | }, 41 | async getVenues() { 42 | this.$reset(); 43 | const { $clientFirestore } = useNuxtApp(); 44 | if (!$clientFirestore) return; 45 | const ref = collection($clientFirestore, "venues"); 46 | const querySnapshot = await getDocs(ref); 47 | querySnapshot.forEach((doc) => { 48 | this.venues.push({ id: doc.id, ...doc.data() }); 49 | }); 50 | }, 51 | async deleteVenue(id) { 52 | const { $clientFirestore } = useNuxtApp(); 53 | const ref = doc($clientFirestore, "venues", id); 54 | await deleteDoc(ref); 55 | const index = this.venues.findIndex((t) => { 56 | return id === t.id; 57 | }); 58 | this.venues.splice(index, 1); 59 | }, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./components/**/*.vue", 5 | "./layouts/**/*.vue", 6 | "./pages/**/*.vue", 7 | "./plugins/**/*.js", 8 | "./nuxt.config.js", 9 | "./app.vue", 10 | ], 11 | theme: { 12 | fontFamily: { 13 | brandon: ["brandon-grotesque", "sans-serif"], 14 | lato: ["Lato", "sans-serif"], 15 | futura: ["Futura", "sans-serif"], 16 | jost: ["Jost", "sans-serif"], 17 | montserrat: ["Montserrat", "sans-serif"], 18 | carterone: ["Carter One", "cursive"], 19 | }, 20 | letterSpacing: { 21 | 12: "0.12em", 22 | 15: "0.15em", 23 | }, 24 | extend: { 25 | colors: { 26 | black: "#000000", 27 | white: "#FFFFFF", 28 | gold: "#FFD700", 29 | "light-gold": "#FFEB87", 30 | "lighter-grey": "#F2F2F2", 31 | "light-grey": "#A8A8A8", 32 | "mid-grey": "#8A8A8A", 33 | "dark-grey": "#474747", 34 | "light-orange-gold": "#FFBD4A", 35 | "light-yellow": "#F9EFBF", 36 | "light-red": "hsl(0, 79%, 72%, 20%)", 37 | "dark-red": "#C74B4B", 38 | "light-green": "#3EBD93", 39 | "danger-red": "#D63C3C", 40 | }, 41 | }, 42 | }, 43 | plugins: [], 44 | }; 45 | --------------------------------------------------------------------------------