Compare commits

..

No commits in common. "rust-rewrite" and "main" have entirely different histories.

210 changed files with 25897 additions and 12550 deletions

3
.envrc
View File

@ -1,3 +0,0 @@
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
use devenv

35
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,35 @@
name: Build and Deploy
on: [push]
jobs:
Deploy:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v3
- name: Use Nodejs
uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
working-directory: ./frontend
- run: npm run build
working-directory: ./frontend
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
registry: gitea.xeovalyte.dev
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
- run: docker buildx build -t gitea.xeovalyte.dev/xeovalyte/wrbapp:latest-arm --load --platform=linux/arm64 .
- run: docker push gitea.xeovalyte.dev/xeovalyte/wrbapp:latest-arm
- run: docker buildx build -t gitea.xeovalyte.dev/xeovalyte/wrbapp:latest --load --platform=linux/amd64 .
- run: docker push gitea.xeovalyte.dev/xeovalyte/wrbapp:latest

23
.gitignore vendored
View File

@ -1,23 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/dist/
/static/
/.dioxus/
database/
# These are backup files generated by rustfmt
**/*.rs.bk
# Node
node_modules
# Devenv
.devenv*
devenv.local.nix
# Direnv
.direnv
# Pre commit
.pre-commit-config.yaml

BIN
Assets/addtohomescreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
Assets/done.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
Assets/mainview.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
Assets/options.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

5927
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +0,0 @@
[package]
name = "wrbapp"
version = "0.1.0"
authors = ["xeovalyte <me+gitea@xeovalyte.dev>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0.197", features = ["derive"] }
dioxus = { version = "0.5", features = ["fullstack", "router"] }
web-sys = { version = "0.3.70", features = ["Window", "Location"] }
tokio = { version = "1.38", features = ["macros", "rt-multi-thread"], optional = true }
axum = { version = "0.7", optional = true }
axum-extra = { version = "0.9", features = ["cookie"], optional = true }
time = { version = "0.3", optional = true }
once_cell = { version = "1.19", optional = true }
surrealdb = { version = "2.0", optional = true }
thiserror = { version = "1.0" }
csv = { version = "1.3", optional = true }
# Debug
tracing = "0.1"
dioxus-logger = "0.5"
manganis = "0.2"
[features]
default = []
server = [ "dioxus/axum", "tokio", "axum", "axum-extra", "time", "once_cell", "surrealdb", "csv" ]
web = ["dioxus/web"]

View File

@ -1,45 +0,0 @@
[application]
# App (Project) Name
name = "wrbapp"
# Dioxus App Default Platform
# desktop, web
default_platform = "web"
# `build` & `serve` dist path
out_dir = "dist"
# resource (assets) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "wrbapp"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "assets"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
style = []

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:18
WORKDIR /usr/src/app
COPY ./frontend/.output .
EXPOSE 3000
CMD [ "node", "server/index.mjs" ]

View File

@ -1,14 +0,0 @@
1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
2. Install the tailwind css cli: https://tailwindcss.com/docs/installation
3. Run the following command in the root of the project to start the tailwind CSS compiler:
```bash
npx tailwindcss -i ./input.css -o ./assets/tailwind.css --watch
```
Launch the Dioxus Fullstack app:
```bash
dx serve --platform fullstack
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,153 +0,0 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1728113618,
"owner": "cachix",
"repo": "devenv",
"rev": "a8495abab31ce52cd45d343caa760046c0c7ee74",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1728196311,
"owner": "nix-community",
"repo": "fenix",
"rev": "26971356e387b5ec0578f52be1bbd82ecf6dbad4",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1728067476,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6e6b3dd395c3b1eb9be9f2d096383a8d05add030",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1728067476,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6e6b3dd395c3b1eb9be9f2d096383a8d05add030",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1728092656,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "1211305a5b237771e13fcca0c51e60ad47326a9a",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"fenix": "fenix",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1728064742,
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "5982d9c420d0dc90739171829f0d2e9c80d98979",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,47 +0,0 @@
{ pkgs, lib, config, inputs, ... }:
{
# https://devenv.sh/basics/
env.GREET = "devenv";
env.LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
env.BINDGEN_EXTRA_CLANG_ARGS = "-I ${pkgs.libclang.lib}/lib/clang/17/include";
# https://devenv.sh/packages/
packages = [ pkgs.openssl pkgs.git pkgs.tailwindcss pkgs.watchexec pkgs.libclang pkgs.clang ];
# https://devenv.sh/scripts/
scripts.hello.exec = "echo hello from $GREET";
enterShell = ''
hello
git --version
'';
# https://devenv.sh/tests/
enterTest = ''
echo "Running tests"
git --version | grep "2.42.0"
'';
# https://devenv.sh/services/
# services.postgres.enable = true;
# https://devenv.sh/languages/
languages.rust.enable = true;
languages.rust.channel = "stable";
languages.rust.targets = [ "wasm32-unknown-unknown" ];
languages.javascript.enable = true;
languages.javascript.npm.enable = true;
languages.javascript.npm.install.enable = true;
# https://devenv.sh/pre-commit-hooks/
# pre-commit.hooks.shellcheck.enable = true;
# https://devenv.sh/processes/
processes.tailwind.exec = "watchexec -e rs ${lib.getExe pkgs.tailwindcss} -i input.css -o assets/tailwind.css";
processes.dioxus.exec = "dx serve";
processes.surrealdb.exec = "docker compose up";
# See full reference at https://devenv.sh/reference/options/
}

View File

@ -1,8 +0,0 @@
inputs:
nixpkgs:
url: github:NixOS/nixpkgs/nixos-24.05
fenix:
url: github:nix-community/fenix
inputs:
nixpkgs:
follows: nixpkgs

View File

@ -1,19 +0,0 @@
services:
surrealdb:
image: surrealdb/surrealdb:latest
container_name: surrealdb-wrbapp
ports:
- 8000:8000
volumes:
- ./database:/data
user: "${UID}:${GID}"
entrypoint:
- /surreal
- start
- --user
- "root"
- --pass
- "root"
- --log
- debug
- rocksdb:/data/database.db

10
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
service-account.json

42
frontend/README.md Normal file
View File

@ -0,0 +1,42 @@
# Nuxt 3 Minimal Starter
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# yarn
yarn install
# npm
npm install
# pnpm
pnpm install --shamefully-hoist
```
## Development Server
Start the development server on http://localhost:3000
```bash
npm run dev
```
## Production
Build the application for production:
```bash
npm run build
```
Locally preview production build:
```bash
npm run preview
```
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.

72
frontend/app.vue Normal file
View File

@ -0,0 +1,72 @@
<template>
<div v-if="showInstallGuide !== null && showInstallGuide && host != 'localhost'" class="bg-neutral-1100 dark:bg-neutral-900 h-screen flex flex-col px-5 text-center text-black dark:text-gray-200">
<h1 class="font-bold text-3xl text-center mb-10 mt-20 text-primary">Reddingsbrigade Waddinxveen</h1>
<p class="mb-10">Om gebruik te maken van de WRB App moet je deze installeren</p>
<h2 class="font-bold">Op een iPhone:</h2>
<p class="mb-10">
Ga naar <a href="https://apps.apple.com/us/app/waddinxveense-reddingsbrigade/id6443636255" class="underline">deze link</a> en download de app via de App Store
</p>
<h2 class="font-bold">Op een Android:</h2>
<ol class="list-decimal list-inside mb-3">
<li>Druk op het opties icoon:<Icon size="1.7em" name="ion:md-more" /></li>
<li>En kies voor "Toevoegen aan startscherm" of "App installeren"</li>
</ol>
<i>Als deze optie er niet is, gebruik dan de chrome browser</i>
</div>
<div v-else-if="userStore.userLoaded">
<div v-if="userStore.isAuthenticated" class="bg-neutral-100 dark:bg-neutral-900 text-primary h-screen flex flex-col">
<LayoutTopbar />
<div class="overflow-y-auto pt-3">
<NuxtPage />
</div>
<LayoutNavbar class="mt-auto" />
</div>
<div v-else class="bg-neutral-100 dark:bg-neutral-900 text-primary h-screen flex flex-col">
<Login />
</div>
</div>
<div v-else class="bg-neutral-100 dark:bg-neutral-900 text-primary h-screen flex justify-center items-center">
<div>
<Icon size="4em" name="ion:load-c" class="animate-spin" />
<h2 class="mt-2 font-bold">Loading...</h2>
</div>
</div>
</template>
<script setup>
import { Device } from '@capacitor/device';
const userStore = useUserStore()
const showInstallGuide = ref(null)
const host = ref(null)
onMounted(async () => {
host.value = window.location.hostname
userStore.init()
Device.getInfo().then(info => {
if (info.platform === 'ios') {
showInstallGuide.value = false;
document.getElementsByClassName('top-right')[0].classList.add('toastios')
} else if (info.platform === 'web' && process.client && 'serviceWorker' in navigator && window.isSecureContext) {
if (window.matchMedia('(display-mode: standalone)').matches) showInstallGuide.value = false
else showInstallGuide.value = true
registerServiceWorker()
} else {
showInstallGuide.value = true
}
});
})
</script>
<style>
.body {
margin-bottom: env(safe-area-inset-bottom);
}
.toastios {
padding-top: env(safe-area-inset-top);
}
</style>

View File

@ -0,0 +1,37 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.item-hover {
@apply p-3 cursor-pointer hover:bg-neutral-300 dark:hover:bg-neutral-700 transition-all;
}
.item {
@apply p-3;
}
.container {
@apply bg-neutral-200 dark:bg-neutral-800 dark:text-gray-300 text-gray-900 shadow rounded;
}
.btn {
@apply bg-primary hover:bg-orange-700 text-gray-900 font-bold px-3 py-1 shadow rounded transition-all hover:cursor-pointer disabled:opacity-50 disabled:hover:cursor-not-allowed;
}
.divider {
@apply w-full h-1 bg-neutral-250 dark:bg-neutral-850;
}
.checkbox {
@apply h-4 w-4 text-primary rounded dark:bg-neutral-700 bg-neutral-300 hover:cursor-pointer focus:ring-transparent focus:ring-offset-0 border-none
}
.input {
@apply bg-neutral-200 dark:bg-neutral-800 dark:text-gray-300 text-gray-900 shadow rounded border-none focus:ring-primary
}
.text-default {
@apply dark:text-gray-300 text-gray-900
}
}

View File

@ -0,0 +1,10 @@
{
"appId": "com.reddingsbrigadewaddinxveen.wrbapp",
"appName": "WRB App",
"webDir": ".output/public",
"bundledWebRuntime": false,
"server": {
"url": "https://app.reddingsbrigadewaddinxveen.nl/",
"cleartext": true
}
}

View File

@ -0,0 +1,179 @@
<template>
<div class="flex flex-col justify-center h-screen items-center px-2 pb-20">
<h1 class="font-bold text-3xl text-center mb-20">Reddingsbrigade Waddinxveen</h1>
<div class="max-w-sm w-full">
<form v-if="!creatingAccount" @submit.prevent="submitLoginForm" class="flex flex-col">
<label class="font-bold">Email</label>
<input v-model="form.email" required="true" placeholder="user@example.com" class="input mb-5" type="email">
<label class="font-bold">Wachtwoord</label>
<input v-model="form.password" required="true" class="input" :type="showPassword ? 'text' : 'password'">
<div class="mb-5 mt-1 flex items-center text-default">
<input v-model="showPassword" type="checkbox" class="mr-1 checkbox ">
<span>Toon wachtwoord</span>
</div>
<div class="w-full flex flex-wrap justify-between">
<input :disabled="disableButtons" type="submit" value="Login" class="btn w-full sm:w-24 mb-1">
<button @click="forgotPassword" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Wachtwoord vergeten?</button>
</div>
<i class="mt-10 text-black dark:text-gray-200">Is dit jouw eerste keer inloggen? Gebruik dan als email het email dat gebruikt is bij de inschrijving en het wachtwoord is uw Lidnummmer / Sportlinknummer</i>
</form>
<form v-else @submit.prevent="submitCreateForm" class="flex flex-col">
<h3 class="text-center text-default text-lg mb-5">Account aanmaken voor <br><b>{{ form.email }}</b></h3>
<label class="font-bold">Nieuw wachtwoord</label>
<input v-model="form.newPassword" required="true" class="input mb-1" :type="showPassword ? 'text' : 'password'">
<span class="mb-5 text-default italic text-sm">Minimaal 8 karakters</span>
<label class="font-bold">Herhaal nieuw wachtwoord</label>
<input v-model="form.confirmNewPassword" required="true" class="input" :type="showPassword ? 'text' : 'password'">
<div class="mb-5 mt-1 flex items-center text-default">
<input v-model="showPassword" type="checkbox" class="mr-1 checkbox ">
<span>Toon wachtwoord</span>
</div>
<div class="w-full flex flex-wrap">
<input :disabled="disableButtons" type="submit" value="Account Aanmaken" class="btn w-full sm:w-40 mb-1">
<button @click="goBack" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Jij niet? Ga terug</button>
</div>
</form>
</div>
</div>
</template>
<script setup>
import { useToast } from 'vue-toastification'
import { createUserWithEmailAndPassword, signInWithEmailAndPassword, sendPasswordResetEmail } from "firebase/auth";
import { doc, setDoc, getFirestore } from "firebase/firestore";
const toast = useToast()
const userStore = useUserStore()
const showPassword = ref(false)
const creatingAccount = ref(false)
const disableButtons = ref(false)
const db = getFirestore()
const form = ref({
email: '',
password: '',
newPassword: '',
confirmNewPassword: ''
})
const submitLoginForm = () => {
disableButtons.value = true
signInWithEmailAndPassword(userStore.auth, form.value.email, form.value.password)
.then(() => disableButtons.value = false)
.catch(async (error) => {
if (error.code === 'auth/user-not-found') {
const { error: err, data } = await useFetch('/api/checkrelatiecode', {
method: 'post',
body: { email: form.value.email, relatiecode: form.value.password.toUpperCase() }
})
if (err.value) {
console.log(err.value)
disableButtons.value = false
return toast.error('Error tijdens het controleren van relatiecode')
}
disableButtons.value = false
if (data.value.code === 'incorrect') return toast.error('Email, wachtwoord of relatiecode onjuist')
else if (data.value.code === 'correct') return creatingAccount.value = true
} else if (error.code === 'auth/wrong-password') {
toast.error('Verkeerde wachtwoord')
} else {
toast.error('Error met inloggen')
}
disableButtons.value = false
console.log(error.message)
});
}
const submitCreateForm = () => {
if (form.value.newPassword !== form.value.confirmNewPassword) return toast.error('Wachtwoorden zijn niet hetzelfde');
if (form.value.newPassword.length < 8) return toast.error('Wachtwoord heeft te weinig karakters');
disableButtons.value = true
createUserWithEmailAndPassword(userStore.auth, form.value.email, form.value.newPassword)
.then(async (userCredential) => {
const idToken = await userStore.auth.currentUser.getIdToken(true)
const { error, data } = await useFetch('/api/getrelatiecodes', {
method: 'post',
body: { email: form.value.email, token: idToken }
})
if (error.value) {
console.log(error.value)
disableButtons.value = false
return toast.error('Error tijdens het controleren van relatiecode')
}
disableButtons.value = false
if (data.value.code === 'error') return toast.error('Error tijdens maken van account')
else if (data.value.code === 'success') {
await setDoc(doc(db, "users", userCredential.user.uid), {
email: form.value.email,
relatiecodes: [form.value.password.toUpperCase()],
allRelatiecodes: data.value.relatiecodes,
id: userCredential.user.uid,
});
data.value.persons.forEach(person => {
if (person.relatiecode === form.value.password.toUpperCase()) {
person.checked = true
} else {
person.checked = false
}
})
userStore.userAllPersons = data.value.persons
if (data.value.relatiecodes.length > 1) {
return navigateTo('/settings/config/managerelatiecodes')
}
}
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log(error)
toast.error('Error tijdens het maken van account')
});
}
const forgotPassword = () => {
sendPasswordResetEmail(userStore.auth, form.value.email)
.then(() => {
toast.info('Wachtwoord vergeten email verstuurd!')
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log(error)
toast.error('Error tijdens het versturen van het wachtwoord vergeten email')
});
}
const goBack = () => {
creatingAccount.value = false
form.value = {
email: form.value.email,
password: form.value.password,
newPassword: '',
confirmNewPassword: ''
}
}
</script>

View File

@ -0,0 +1,48 @@
<template>
<div v-if="route.meta.key !== 'disable'" class="w-full bg-neutral-200 dark:bg-neutral-800 flex justify-center items-center shadow" :class="platform === 'ios' ? 'navbar' : 'py-3'">
<div class="flex text-sm justify-evenly items-center gap-1 w-full max-w-lg dark:text-gray-300 text-gray-900 overflow-x-hidden">
<NuxtLink to="/" class="flex flex-col items-center hover:cursor-pointer drop-shadow" :class="route.path === '/' ? 'text-primary' : ''">
<Icon size="1.8em" name="ion:home-outline" />
<span>Home</span>
</NuxtLink>
<NuxtLink to="/news" class="flex flex-col items-center hover:cursor-pointer drop-shadow" :class="route.path.startsWith('/news') ? 'text-primary' : ''">
<Icon size="1.8em" name="ion:newspaper-outline" />
<span>Nieuws</span>
</NuxtLink>
<NuxtLink to="/calendar" class="flex flex-col items-center hover:cursor-pointer drop-shadow" :class="route.path === '/calendar' ? 'text-primary' : ''">
<Icon size="1.8em" name="ion:calendar-outline" />
<span>Agenda</span>
</NuxtLink>
<NuxtLink to="/settings" class="flex flex-col items-center hover:cursor-pointer drop-shadow" :class="route.path.startsWith('/settings') ? 'text-primary' : ''">
<Icon size="1.8em" name="ion:settings-sharp" />
<span>Settings</span>
</NuxtLink>
<NuxtLink v-if="userStore.userPersons[0] && userStore.userPersons.filter(a => a.wedstrijdteam).length > 0" to="/wedstrijd" class="flex flex-col items-center hover:cursor-pointer drop-shadow" :class="route.path.startsWith('/wedstrijd') ? 'text-primary' : ''">
<Icon size="1.8em" name="ion:podium-outline" />
<span>Wedstrijd</span>
</NuxtLink>
</div>
</div>
</template>
<script setup>
import { Device } from '@capacitor/device';
const route = useRoute()
const userStore = useUserStore()
const platform = ref(null)
onMounted(() => {
Device.getInfo().then(info => {
platform.value = info.platform
});
})
</script>
<style>
.navbar {
padding-bottom: calc(env(safe-area-inset-bottom) - 10px);
padding-top: 12px;
}
</style>

View File

@ -0,0 +1,20 @@
<template>
<div v-if="route.meta.key !== 'disable'" class="w-full topbar h-10 bg-neutral-200 dark:bg-neutral-800 flex justify-center items-center shadow px-5 gap-2">
<div class="flex justify-evenly items-center gap-3 w-full max-w-xl dark:text-gray-300 text-gray-900">
<Icon v-if="route.meta.key === 'back'" size="1.75em" @click="router.back()" class="hover:cursor-pointer" name="ion:arrow-back"/>
<h1 class="capitalize font-bold text-xl mr-auto">{{ route.meta.title }}</h1>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const router = useRouter()
</script>
<style>
.topbar {
padding-top: calc(env(safe-area-inset-top) + 20px);
padding-bottom: 20px;
}
</style>

View File

@ -0,0 +1,121 @@
import { getMessaging, getToken, onMessage } from 'firebase/messaging'
import { PushNotifications } from '@capacitor/push-notifications';
import { Device } from '@capacitor/device';
export const setupIosNotifications = () => {
const userStore = useUserStore()
const toast = useToast()
// Request permission to use push notifications
// iOS will prompt user and return if they granted permission or not
// Android will just grant without prompting
PushNotifications.requestPermissions().then(result => {
if (result.receive === 'granted') {
// Register with Apple / Google to receive push via APNS/FCM
PushNotifications.register()
} else {
toast.error('Error tijdens het registrenen van push notificaties')
}
});
// On success, we should be able to receive notifications
PushNotifications.addListener('registration',
async (token) => {
// alert('Push registration success, token: ' + token.value);
userStore.registrationToken = token
const { error } = await useFetch('/api/subscribetotopic', {
method: 'post',
body: { topic: 'all', registrationToken: token.value }
})
if (error.value) {
console.log(error.value)
return toast.error('Error tijdens het krijgen van relateicodes')
}
console.log('Subscribed to topic!')
}
);
// Some issue with our setup and push will not work
PushNotifications.addListener('registrationError',
(error) => {
toast.error('Error tijdens het registreren van push notificaties')
console.log(error)
}
);
// Show us the notification payload if the app is open on our device
PushNotifications.addListener('pushNotificationReceived',
(notification) => {
toast.info(`${notification.title}`, {
onClick: () => navigateTo('/news')
})
}
);
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(notification) => {
navigateTo('/news')
}
);
}
export const setupWebNotifications = () => {
const messaging = getMessaging()
const userStore = useUserStore()
const toast = useToast()
getToken(messaging, { vapidKey: 'BI7l3nyGV6wJcFh7wrwmQ42W7RSXl46bmhXZJmDd4P-0K_JFP0ClTqjO-rr5H5DXBbmVR4kXwxFpUlo_d6cUy4Q' }).then(async (currentToken) => {
if (currentToken) {
console.log(currentToken)
const { error} = await useFetch('/api/subscribetotopic', {
method: 'post',
body: { topic: 'all', registrationToken: currentToken }
})
if (error.value) {
console.log(error.value)
return toast.error('Error tijdens het registreren van push notifications')
}
userStore.registrationToken = currentToken
console.log('Subscribed to topic!')
} else {
// Show permission request UI
console.log('No registration token available. Request permission to generate one.');
// ...
}
}).catch((err) => {
console.log('An error occurred while retrieving token. ', err);
// ...
});
}
export const setupNotifications = async () => {
const info = await Device.getInfo();
if (info.platform !== 'web') setupIosNotifications()
else setupWebNotifications()
}
export const registerServiceWorker = () => {
const messaging = getMessaging()
const toast = useToast()
navigator.serviceWorker.register('/sw.js').catch(e => alert(e));
onMessage(messaging, (payload) => {
console.log('Message received. ', payload);
toast.info(`${payload.notification.title}`, {
onClick: () => navigateTo('/news')
})
});
}

View File

@ -0,0 +1,6 @@
import { useToast } from 'vue-toastification'
export default () => {
const toast = useToast()
return toast
}

13
frontend/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
App/build
App/Pods
App/Podfile.lock
App/App/public
DerivedData
xcuserdata
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml

View File

@ -0,0 +1,406 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
);
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* App.app */,
);
name = Products;
sourceTree = "<group>";
};
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
504EC3131FED79650016851F /* Info.plist */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
);
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = App;
productName = App;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* App */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
504EC3021FED79650016851F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
504EC3001FED79650016851F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC30C1FED79650016851F /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC3111FED79650016851F /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
504EC3141FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
504EC3151FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.reddingsbrigadewaddinxveen.wrbapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.reddingsbrigadewaddinxveen.wrbapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3141FED79650016851F /* Debug */,
504EC3151FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
504EC3181FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:App.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:App.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,66 @@
import UIKit
import Capacitor
import Firebase
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
Messaging.messaging().token(completion: { (token, error) in
if let error = error {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
} else if let token = token {
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: token)
}
})
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,116 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "AppIcon-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "AppIcon-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "AppIcon-29x29@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "AppIcon-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "AppIcon-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "AppIcon-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "AppIcon-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "AppIcon-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "AppIcon-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "AppIcon-20x20@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "AppIcon-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "AppIcon-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "AppIcon-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "AppIcon-40x40@2x-1.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "AppIcon-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "AppIcon-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "AppIcon-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "AppIcon-512@2x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "splash-2732x2732-2.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</imageView>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="Splash" width="1366" height="1366"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
</dependencies>
<scenes>
<!--Bridge View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>160377508482-r1822jlgp287ks09ajb27qg1oko80lpr.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.160377508482-r1822jlgp287ks09ajb27qg1oko80lpr</string>
<key>API_KEY</key>
<string>AIzaSyAjlJhSGUMzTFVx-ICML_8DVlDFUQqN8WY</string>
<key>GCM_SENDER_ID</key>
<string>160377508482</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.reddingsbrigadewaddinxveen.wrbapp</string>
<key>PROJECT_ID</key>
<string>wrbapp</string>
<key>STORAGE_BUCKET</key>
<string>wrbapp.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:160377508482:ios:0079517b62e9684f879a9b</string>
</dict>
</plist>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>wrbapp</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
</plist>

26
frontend/ios/App/Podfile Normal file
View File

@ -0,0 +1,26 @@
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '13.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCommunityFcm', :path => '../../node_modules/@capacitor-community/fcm'
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
end
target 'App' do
capacitor_pods
pod 'Firebase/Messaging'
end
post_install do |installer|
assertDeploymentTarget(installer)
end

5
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,5 @@
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"allowJs": true
}

41
frontend/nuxt.config.ts Normal file
View File

@ -0,0 +1,41 @@
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
ssr: false,
imports: {
dirs: ['stores']
},
modules: [
'@nuxtjs/tailwindcss',
'nuxt-icon',
'@vueuse/nuxt',
'@nuxtjs/robots',
[ '@pinia/nuxt', { autoImports: [ 'defineStore' ]} ],
],
build: {
transpile: ['vue-toastification'],
},
app: {
head: {
title: 'WRB App',
charset: 'utf-8',
viewport: 'width=device-width initial-scale=1 viewport-fit=cover',
meta: [
{ name: 'theme-color', content: '#eb6330' },
{ name: 'description', content: 'De officiele app voor de Waddinxveense Reddingsbrigade'}
],
link: [
{ rel: 'manifest', href: '/manifest.json' },
{ rel: 'icon', href: '/favicon.ico', type: 'image/x-icon' }
],
script: [
{ src: 'https://umami.xeovalyte.dev/script.js', async: true, 'data-website-id': '59577dd0-b790-488c-af69-7f8d2cce0537' },
],
}
},
runtimeConfig: {
privateKeyId: '',
privateKey: '',
clientId: ''
}
})

22038
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
frontend/package.json Normal file
View File

@ -0,0 +1,38 @@
{
"private": true,
"overrides": {
"vue": "latest"
},
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@capacitor/cli": "^4.7.1",
"@nuxtjs/tailwindcss": "^6.6.0",
"@tailwindcss/forms": "^0.5.3",
"@vueuse/core": "^9.13.0",
"@vueuse/nuxt": "^9.13.0",
"nuxt": "^3.3.1",
"nuxt-icon": "^0.3.3"
},
"dependencies": {
"@capacitor/core": "^4.7.1",
"@capacitor/device": "^4.1.0",
"@capacitor/ios": "^4.7.1",
"@capacitor/push-notifications": "^4.1.2",
"@formkit/nuxt": "^1.0.0-beta.11-c95e605",
"@nuxtjs/robots": "^3.0.0",
"@pinia/nuxt": "^0.4.7",
"@vueuse/components": "^9.13.0",
"@vueuse/firebase": "^9.13.0",
"@vueuse/shared": "^9.13.0",
"firebase": "^9.18.0",
"firebase-admin": "^11.5.0",
"pinia": "^2.0.33",
"vue-toastification": "^2.0.0-rc.5"
}
}

View File

@ -0,0 +1,55 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<div v-if="calendarStore.events[0]" class="flex flex-col gap-3">
<div v-for="(event, index) in calendarStore.events" :key="index">
<div class="item container flex flex-col">
<h2 class="">{{ longEventDate(event.date) }}</h2>
<p class="whitespace-pre overflow-x-auto font-bold text-xl">{{ event.description}}</p>
</div>
</div>
</div>
<div class="w-full flex flex-col justify-center items-center" v-else>
<Icon size="2em" name="ion:load-c" class="animate-spin" />
<h2 class="mt-2 font-bold">Loading...</h2>
</div>
</div>
</template>
<script setup>
definePageMeta({
title: 'Agenda'
})
const userStore = useUserStore()
const calendarStore = useCalendarStore()
onMounted(() => {
getEvents()
})
const getEvents = () => {
if (userStore.userPersons[0]) {
const groups = [...new Set(userStore.userPersons.map(a => a.groups.join()).join().split(','))]
calendarStore.getEvents(groups)
} else {
setTimeout(() => { getEvents() }, 50)
}
}
const longEventDate = (eventDate) => {
const date = new Date(eventDate)
return date.toLocaleString('nl-NL', {
weekday: 'short',
day: 'numeric',
year: 'numeric',
month: 'long',
hour: 'numeric',
minute: 'numeric'
}
)}
</script>

29
frontend/pages/index.vue Normal file
View File

@ -0,0 +1,29 @@
<template>
<div class="flex flex-col justify-center items-center px-2 overflow-hidden">
<h1 class="font-bold text-3xl text-center mt-6 mb-3">Reddingsbrigade Waddinxveen</h1>
<h2 class="text-xl text-center mb-12">{{ userStore.userPersons.map(a => a.fullName).join(', ')}} {{ userStore.userPerons }}</h2>
<div class="container w-full max-w-md">
<NuxtLink to="/news" class="rounded-t item-hover py-2 flex items-center">
<span>Nieuws</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />
<NuxtLink to="/calendar" class="item-hover py-2 flex items-center">
<span>Agenda</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />
<NuxtLink to="/settings" class="rounded-b item-hover py-2 flex items-center">
<span>Settings</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
</div>
</div>
</template>
<script setup>
definePageMeta({
title: 'Home',
})
const userStore = useUserStore()
</script>

View File

@ -0,0 +1,86 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<div v-if="newsStore.loaded && newsStore.news" class="flex flex-col gap-3">
<NuxtLink to="/news/newmessage" v-if="userStore.userData.sendNews" class="item-hover border-dashed border-2 container text-center font-bold text-xl border-neutral-500 mb-3">
Nieuw Bericht
</NuxtLink>
<div v-if="newsStore.news[0]" v-for="(item, index) in newsStore.news" :key="index">
<div class="item container flex flex-col relative">
<h3 class="text-sm">{{ longEventDate(item.date.toDate()) }}</h3>
<h2 class="text-2xl font-bold">{{ item.title }}</h2>
<p class="description" v-html="convertLinks(item.description)"></p>
<Icon v-if="userStore.userData.sendNews" @click="newsStore.deleteNews(item, index)" size="1.5em" name="ion:trash-sharp" class="absolute top-3 right-3 hover:cursor-pointer text-red-500" />
</div>
</div>
<h2 v-else class="font-bold text-center text-xl mt-3">
Er is geen nieuws
</h2>
</div>
<div class="w-full flex flex-col justify-center items-center" v-else>
<Icon size="2em" name="ion:load-c" class="animate-spin" />
<h2 class="mt-2 font-bold">Loading...</h2>
</div>
</div>
</template>
<script setup>
definePageMeta({
title: 'Nieuws'
})
const userStore = useUserStore()
const newsStore = useNewsStore()
onMounted(() => {
newsStore.getNews()
})
const convertLinks = ( input ) => {
let text = input;
const linksFound = text.match( /(?:www|https?)[^\s]+/g );
const aLink = [];
if ( linksFound != null ) {
for ( let i=0; i<linksFound.length; i++ ) {
let replace = linksFound[i];
if ( !( linksFound[i].match( /(http(s?)):\/\// ) ) ) { replace = 'http://' + linksFound[i] }
let linkText = replace.split( '/' )[2];
if ( linkText.substring( 0, 3 ) == 'www' ) { linkText = linkText.replace( 'www.', '' ) }
if ( linkText.match( /youtu/ ) ) {
let youtubeID = replace.split( '/' ).slice(-1)[0];
aLink.push( '<div class="video-wrapper"><iframe src="https://www.youtube.com/embed/' + youtubeID + '" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>' )
}
else if ( linkText.match( /vimeo/ ) ) {
let vimeoID = replace.split( '/' ).slice(-1)[0];
aLink.push( '<div class="video-wrapper"><iframe src="https://player.vimeo.com/video/' + vimeoID + '" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>' )
}
else {
aLink.push( '<a class="text-primary hover:underline underline-offset-2" href="' + replace + '" target="_blank">' + linkText + '</a>' );
}
text = text.split( linksFound[i] ).map(item => { return aLink[i].includes('iframe') ? item.trim() : item } ).join( aLink[i] );
}
return text;
}
else {
return input;
}
}
const longEventDate = (eventDate) => {
const date = new Date(eventDate)
return date.toLocaleString('nl-NL', {
weekday: 'short',
day: 'numeric',
year: 'numeric',
month: 'long',
hour: 'numeric',
minute: 'numeric'
}
)}
</script>

View File

@ -0,0 +1,56 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<form @submit.prevent="sendNews" class="flex flex-col">
<label class="font-bold">Titel</label>
<input v-model="form.title" required="true" class="input mb-5" type="text">
<label class="font-bold">Beschrijving</label>
<textarea v-model="form.description" required="true" class="input mb-5" />
<label class="font-bold">Groep</label>
<select v-model="form.topic" required="true" class="input mb-5">
<option value="all">Iedereen</option>
<option value="test">Test</option>
</select>
<div class="w-full flex flex-wrap justify-between">
<input :disabled="disableButtons" type="submit" value="Stuur Bericht" class="btn w-full sm:w-40 mb-1">
<button @click="router.back()" type="button" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Annuleer</button>
</div>
</form>
</div>
</template>
<script setup>
definePageMeta({
title: 'Nieuw Bericht',
key: 'back'
})
const router = useRouter()
const toast = useToast()
const newsStore = useNewsStore()
const disableButtons = ref(false)
const sendNews = async () => {
try {
disableButtons.value = true
await newsStore.send(form.value)
disableButtons.value = false
} catch (e) {
console.log(e)
disableButtons.value = false
toast.error('Error tijdens versturen bericht')
}
}
const form = ref({
title: '',
description: '',
topic: ''
})
</script>

View File

@ -0,0 +1,265 @@
<template>
<div @click.self="showModel = false" v-if="showModel" class="fixed flex justify-center items-center h-screen w-full bg-black top-0 left-0 z-50 bg-opacity-50" >
<form @submit.prevent="submitModelForm" class="dark:bg-neutral-800 bg-neutral-200 p-10 rounded-xl flex flex-col w-full max-w-sm">
<h1 class="font-bold text-center text-lg mb-5">Beheer Persoon</h1>
<div class="text-default mb-2">
Relatiecode: <b>{{ modelData.relatiecode }}</b>
</div>
<div class="text-default mb-2">
Naam: <b>{{ modelData.fullName }}</b>
</div>
<div class="text-default mb-5">
Groepen: <b>{{ modelData.groups.join(', ') }}</b>
</div>
<label class="relative inline-flex items-center cursor-pointer mb-5">
<input type="checkbox" value="" v-model="modelData.wedstrijdteam" class="sr-only peer">
<div class="w-11 h-6 bg-neutral-300 peer-focus:outline-none peer-focus:ring-none rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
<span class="ml-3 font-medium text-gray-900 dark:text-gray-300">Wedstrijd Team</span>
</label>
<input :disabled="disableButtons" type="submit" class="btn" :value="'Bewerken'" />
</form>
</div>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<div class="mb-5">
<form @submit.prevent="submitLedenlijst" class="flex flex-col">
<input required="true" @change="handleFileChanged" accept=".csv" class="my-2" type="file">
<span class="text-sm"><i>Met de volgende kolommen: Relatiecode, Volledige naam(1), Roepnaam, E-mail, 2e E-mail, Verenigingssporten, Diploma</i></span>
<button :disabled="disableButtons" class="btn mx-auto mt-2">Publish Ledenlijst</button>
</form>
</div>
<div class="flex flex-col gap-3">
<input v-model="searchTerm" class="input mb-2 font-bold" type="search" placeholder="Search">
<div v-for="lid in filteredLedenlijst" :key="lid.relatiecode">
<div @click="handleModel(lid)" class="item container flex flex-wrap hover:cursor-pointer">
<b class="w-24">{{ lid.relatiecode }}</b> {{ lid.fullName }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { doc, getDocs, collection, writeBatch, updateDoc, setDoc, getFirestore } from "firebase/firestore";
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Ledenlijst',
key: 'back'
})
const toast = useToast()
const usersStore = useUsersStore()
const modelData = ref(null)
const db = getFirestore()
const file = ref(null)
const disableButtons = ref(false)
const searchTerm = ref('')
const newLedenlijst = ref([])
const showModel = ref(false)
onMounted(async () => {
usersStore.getLedenlijst()
})
const handleModel = (lid) => {
modelData.value = lid
if (!modelData.value.hasOwnProperty('wedstrijdteam')) modelData.value.wedstrijdteam = false
modelData.value.oldWedstrijdteam = modelData.value.wedstrijdteam
showModel.value = true
}
const submitModelForm = async () => {
disableButtons.value = true
usersStore.ledenlijst.filter(a => a.relatiecode === modelData.value.relatiecode)[0].wedstrijdteam = modelData.value.wedstrijdteam
if (modelData.value.wedstrijdteam === modelData.value.oldWedstrijdteam) {
disableButtons.value = false
showModel.value = false
return;
}
let docRef = doc(db, "ledenlijst", modelData.value.relatiecode)
await updateDoc(docRef, {
wedstrijdteam: modelData.value.wedstrijdteam
})
docRef = doc(db, "competitors", modelData.value.relatiecode)
if (modelData.value.wedstrijdteam) {
await setDoc(docRef, {
relatiecode: modelData.value.relatiecode,
name: modelData.value.fullName,
active: true,
})
} else {
await updateDoc(docRef, {
active: false,
})
}
toast.success('Persoon is bewerkt')
disableButtons.value = false
showModel.value = false
}
const filteredLedenlijst = computed(() => {
return usersStore.ledenlijst.filter(lid => lid.fullName.toLowerCase().includes(searchTerm.value.toLowerCase()))
})
const handleFileChanged = (event) => {
const target = event.target;
if (target && target.files) {
file.value = target.files[0];
}
}
const submitLedenlijst = () => {
disableButtons.value = true
let reader = new FileReader()
reader.onload = function() {
csvToJson(reader.result);
};
reader.onerror = function() {
console.log(reader.error);
};
reader.readAsText(file.value)
}
const csvToJson = (csv) => {
let arr = csv.split('\n');
var result = [];
var headers = arr[0].split(';');
for(var i = 1; i < arr.length; i++) {
var data = arr[i].split(';');
var obj = {};
for(var j = 0; j < data.length; j++) {
obj[headers[j].trim()] = data[j].trim();
}
result.push(obj);
}
if (!Object.hasOwn(result[0], 'Relatiecode') || !Object.hasOwn(result[0], 'Volledige naam (1)') || !Object.hasOwn(result[0], 'E-mail') || !Object.hasOwn(result[0], '2e E-mail') || !Object.hasOwn(result[0], 'Verenigingssporten') || !Object.hasOwn(result[0], 'Diploma dropdown 1')) return toast.error('Missing properties')
newLedenlijst.value = []
for (let i in result) {
let groups = []
let correctGroups = null
if (!result[i].Relatiecode) break;
if (result[i].Verenigingssporten.includes(',')) correctGroups = result[i].Verenigingssporten.split(',')
else correctGroups = [result[i].Verenigingssporten]
correctGroups.forEach(group => {
const x = group.split(' - ')
if (x[2] === 'Week') groups.push('Vrijdag')
else if (x[2] === 'Zaterdag' && x[1] !== 'Wedstrijd') groups.push('Zaterdag')
groups.push(x[1])
})
if (groups[2] === 'Week') groups[2] = 'Vrijdag'
let inwedstrijdteam
if (usersStore.ledenlijst.filter(x => x.relatiecode === result[i].Relatiecode)[0]) {
inwedstrijdteam = usersStore.ledenlijst.filter(x => x.relatiecode === result[i].Relatiecode)[0].wedstrijdteam;
} else {
inwedstrijdteam = false;
}
const wedstrijdteam = inwedstrijdteam ? true : false
newLedenlijst.value.push({ relatiecode: result[i].Relatiecode, wedstrijdteam, fullName: result[i]['Volledige naam (1)'], email: [result[i]['E-mail'], result[i]['2e E-mail']], groups: [...new Set(groups)], diploma: result[i]['Diploma dropdown 1'] })
}
uploadLedenlijst()
}
const uploadLedenlijst = async () => {
try {
const batch = writeBatch(db)
newLedenlijst.value.forEach(lid => {
const docRef = doc(db, "ledenlijst", lid.relatiecode)
const exists = usersStore.ledenlijst.filter(a => a.relatiecode === lid.relatiecode).length > 1
if (!exists) {
return batch.set(docRef, lid);
}
batch.update(docRef, { fullName: lid.relatiecode, email: lid.email, groups: lid.groups, diploma: lid.diploma})
})
const deleteLeden = usersStore.ledenlijst.filter(a => newLedenlijst.value.map(x => x.relatiecode).indexOf(a.relatiecode) === -1)
deleteLeden.forEach(lid => {
const docRef = doc(db, "ledenlijst", lid.relatiecode)
batch.delete(docRef)
})
await batch.commit();
toast.success('Published ledenlijst')
} catch (e) {
toast.error("Error updating ledenlijst");
console.log(e)
}
usersStore.ledenlijst = newLedenlijst.value
updateUsers()
}
const updateUsers = async () => {
try {
const querySnapshot = await getDocs(collection(db, "users"));
querySnapshot.forEach((doc) => {
const data = doc.data()
data.id = doc.id
usersStore.users.push(data)
});
} catch (e) {
console.log(e)
toast.error('Error getting users')
}
const batch = writeBatch(db);
usersStore.users.forEach(user => {
const lid = usersStore.ledenlijst.filter(a => a.email.includes(user.email))
const newRelatiecodes = lid.map(a => a.relatiecode)
user.allRelatiecodes = newRelatiecodes
user.relatiecodes.forEach((relatiecode, index) => {
if (!newRelatiecodes.includes(relatiecode)) { user.relatiecodes.splice(index, 1); console.log('removed item', relatiecode)}
})
const userRef = doc(db, "users", user.id)
batch.update(userRef, user)
})
await batch.commit();
disableButtons.value = false
}
</script>

View File

@ -0,0 +1,98 @@
<template>
<div @click.self="showModel = false" v-if="showModel" class="fixed flex justify-center items-center h-screen w-full bg-black top-0 left-0 z-50 bg-opacity-50" >
<form @submit.prevent="submitModelForm" class="dark:bg-neutral-800 bg-neutral-200 p-10 rounded-xl flex flex-col w-full max-w-sm">
<h1 class="font-bold text-center text-lg mb-5">Beheer Gebruiker</h1>
<div class="text-default mb-2">
Relatiecodes: <b>{{ modelData.relatiecodes.join(', ') }}</b>
</div>
<div class="text-default mb-2">
Email: <b>{{ modelData.email }}</b>
</div>
<div class="text-default mb-5">
ID: <b>{{ modelData.id }}</b>
</div>
<label class="relative inline-flex items-center cursor-pointer mb-5">
<input type="checkbox" value="" v-model="modelData.admin" class="sr-only peer">
<div class="w-11 h-6 bg-neutral-300 peer-focus:outline-none peer-focus:ring-none rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
<span class="ml-3 font-medium text-gray-900 dark:text-gray-300">Admin</span>
</label>
<label class="relative inline-flex items-center cursor-pointer mb-5">
<input type="checkbox" value="" v-model="modelData.sendNews" class="sr-only peer">
<div class="w-11 h-6 bg-neutral-300 peer-focus:outline-none peer-focus:ring-none rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
<span class="ml-3 font-medium text-gray-900 dark:text-gray-300">Berichten Sturen</span>
</label>
<label class="relative inline-flex items-center cursor-pointer mb-5">
<input type="checkbox" value="" v-model="modelData.wedstrijdAdmin" class="sr-only peer">
<div class="w-11 h-6 bg-neutral-300 peer-focus:outline-none peer-focus:ring-none rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
<span class="ml-3 font-medium text-gray-900 dark:text-gray-300">Wedstrijd Admin</span>
</label>
<input :disabled="disableButtons" type="submit" class="btn" :value="'Bewerken'" />
</form>
</div>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<input v-model="searchTerm" class="input mb-2 font-bold" type="search" placeholder="Search">
<div v-for="user in filteredUsers" :key="user.relatiecode">
<div @click="handleModel(user)" class="item container flex flex-wrap hover:cursor-pointer">
<b class="w-24">{{ user.email }}</b>
</div>
</div>
</div>
</template>
<script setup>
import { getDocs, collection, doc, updateDoc, getFirestore } from 'firebase/firestore'
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Manage users',
key: 'back'
})
const toast = useToast()
const db = getFirestore()
const usersStore = useUsersStore()
const searchTerm = ref('')
const disableButtons = ref(false)
const showModel = ref(false)
const modelData = ref({})
onMounted(async () => {
usersStore.getUsers()
})
const handleModel = (user) => {
modelData.value = user
if (!modelData.value.hasOwnProperty('admin')) modelData.value.admin = false
if (!modelData.value.hasOwnProperty('sendNews')) modelData.value.sendNews = false
if (!modelData.value.hasOwnProperty('wedstrijdAdmin')) modelData.value.wedstrijdAdmin = false
showModel.value = true
}
const filteredUsers = computed(() => {
return usersStore.users.filter(user => user.email.toLowerCase().includes(searchTerm.value.toLowerCase()))
})
const submitModelForm = async () => {
disableButtons.value = true
const docRef = doc(db, "users", modelData.value.id)
await updateDoc(docRef, {
admin: modelData.value.admin,
sendNews: modelData.value.sendNews,
wedstrijdAdmin: modelData.value.wedstrijdAdmin
})
toast.success('Gebruiker is bewerkt')
disableButtons.value = false
showModel.value = false
}
</script>

View File

@ -0,0 +1,87 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<form @submit.prevent="savePassword" class="flex flex-col">
<label class="font-bold">Oud Wachtwoord</label>
<input v-model="form.oldPassword" required="true" class="input mb-5" :type="showPassword ? 'text' : 'password'">
<label class="font-bold">Nieuw Wachtwoord</label>
<input v-model="form.newPassword" required="true" class="input mb-5" :type="showPassword ? 'text' : 'password'">
<label class="font-bold">Herhaal Nieuw Wachtwoord</label>
<input v-model="form.confirmNewPassword" required="true" class="input" :type="showPassword ? 'text' : 'password'">
<div class="mb-5 mt-1 flex items-center text-default">
<input v-model="showPassword" type="checkbox" class="mr-1 checkbox ">
<span>Toon Wachtwoord</span>
</div>
<div class="w-full flex flex-wrap justify-between">
<input :disabled="disableButtons" type="submit" value="Wijzig Wachtwoord" class="btn w-full sm:w-52 mb-1">
<button @click="router.back()" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Annuleer</button>
</div>
</form>
</div>
</template>
<script setup>
import { reauthenticateWithCredential, EmailAuthProvider, updatePassword } from 'firebase/auth'
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Wachtwoord Wijzigen',
key: 'back'
})
const toast = useToast()
const router = useRouter()
const userStore = useUserStore()
const form = ref({
oldPassword: '',
newPassword: '',
confirmNewPassword: '',
})
onMounted(() => {
form.value = {
oldPassword: '',
newPassword: '',
confirmNewPassword: '',
}
})
const savePassword = () => {
if (form.value.newPassword !== form.value.confirmNewPassword) return alert ('Niewe wachtwoorden zijn niet hetzelfde')
disableButtons.value = true
const credential = EmailAuthProvider.credential(
userStore.user.email,
form.value.oldPassword
)
reauthenticateWithCredential(userStore.auth.currentUser, credential).then(() => {
updatePassword(userStore.auth.currentUser, form.value.newPassword).then(() => {
toast.success('Wachtwoord is veranderd')
navigateTo('/settings')
disableButtons.value = false
}).catch((error) => {
toast.error('Error tijdens het wachtwoord veranderen')
console.log(error)
disableButtons.value = false
});
}).catch((error) => {
disableButtons.value = false
if (error.code === 'auth/wrong-password') return toast.error('Oude wachtwoord is onjuist')
toast.error('Error tijdens het wachtwoord veranderen')
console.log(error)
});
}
const showPassword = ref(false)
const disableButtons = ref(false)
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<div v-if="userStore.userAllPersons.length !== 0 && userStore.userPersons.length !== 0" class="flex flex-col gap-3">
<div v-for="person in userStore.userAllPersons" :key="person.relatiecode">
<div @click="updateCheckbox(person)" class="item container flex flex-wrap" :class="person.relatiecode === userStore.userPersons[0].relatiecode ? 'bg-neutral-200 dark:bg-neutral-850 text-neutral-400 dark:text-neutral-500 hover:cursor-not-allowed' : 'hover:cursor-pointer'">
<input v-model="person.checked" :disabled="person.relatiecode === userStore.userPersons[0].relatiecode" class="checkbox my-auto mr-3 disabled:bg-neutral-300 disabled:hover:text-neutral-300 dark:disabled:bg-neutral-600 dark:disabled:hover:text-neutral-600 disabled:hover:cursor-not-allowed" type="checkbox">
<span><b>{{ person.fullName }}</b></span>
</div>
</div>
<div class="w-full flex flex-wrap">
<button :disabled="buttonsDisabled" @click="save" class="btn w-full sm:w-40 mb-1">Opslaan</button>
<span @click="router.back()" class="hover:underline font-bold w-full text-center sm:w-max sm:ml-auto hover:cursor-pointer">Annuleer</span>
</div>
</div>
<div class="w-full flex flex-col justify-center items-center" v-else>
<Icon size="2em" name="ion:load-c" class="animate-spin" />
<h2 class="mt-2 font-bold">Loading...</h2>
</div>
</div>
</template>
<script setup>
import { updateDoc, doc, getFirestore } from 'firebase/firestore'
import { useToast } from 'vue-toastification'
const router = useRouter()
definePageMeta({
title: 'Beheer Personen',
key: 'back'
})
const toast = useToast()
const userStore = useUserStore()
const db = getFirestore()
const buttonsDisabled = ref(false)
onMounted(() => {
userStore.getAllPersons()
})
const save = async () => {
buttonsDisabled.value = true
const newRelatiecodes = []
userStore.userAllPersons.forEach(person => {
if (person.checked) {
newRelatiecodes.push(person.relatiecode)
}
})
await updateDoc(doc(db, "users", userStore.user.uid), {
relatiecodes: newRelatiecodes
})
userStore.getPersons(newRelatiecodes)
buttonsDisabled.value = false
navigateTo('/settings')
}
const updateCheckbox = (person) => {
if (person.relatiecode === userStore.userPersons[0].relatiecode) return;
person.checked = !person.checked
}
</script>

View File

@ -0,0 +1,85 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<div>
<h1 class="text-xl ml-2 font-bold">Info</h1>
<div class="container">
<div class="item">
Email: <b>{{ userStore.user.email }}</b>
</div>
<div class="divider" />
<div class="item">
Personen: <b>{{ userStore.userPersons.map(a => a.fullName).join(', ')}}</b>
</div>
<div class="divider" />
<div class="item">
Groepen: <b>{{ groups.join(', ') }}</b>
</div>
<div v-if="userStore.userPersons.map(a => a.diploma).filter(n => n !== '').join('')" class="divider" />
<div v-if="userStore.userPersons.map(a => a.diploma).filter(n => n !== '').join('')" class="item">
Diploma: <b>{{ userStore.userPersons.map(a => a.diploma).filter(n => n !== '').join(', ')}}</b>
</div>
<div class="divider" />
<NuxtLink to="/settings/moreinfo" class="item-hover py-2 rounded-t flex items-center">
<span>Meer Informatie</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
</div>
</div>
<div>
<h1 class="text-xl ml-2 font-bold">Account</h1>
<div class="container">
<NuxtLink to="/settings/config/managerelatiecodes" class="item-hover py-2 rounded-t flex items-center">
<span>Beheer Personen</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />
<NuxtLink to="/settings/config/changepassword" class="item-hover py-2 flex items-center">
<span>Wachtwoord Wijzigen</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />
<div @click="logout" class="item-hover rounded-b flex items-center">
Uitloggen
</div>
</div>
</div>
<div v-if="userStore.userData.admin">
<h1 class="text-xl ml-2 font-bold">Admin</h1>
<div class="container">
<NuxtLink to="/settings/admin/users" class="rounded-t item-hover py-2 flex items-center">
<span>Beheer gebruikers</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />
<NuxtLink to="/settings/admin/ledenlijst" class="rounded-b item-hover py-2 flex items-center">
<span>Ledenlijst</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
</div>
</div>
<div>
<h2 class="text-center font-bold">Gemaakt door <u><a href="https://xeovalyte.com/">Timo Boomers</a></u></h2>
</div>
</div>
</template>
<script setup>
import { signOut } from "firebase/auth";
definePageMeta({
title: 'Settings'
})
const userStore = useUserStore()
const groups = computed(() => {
return [...new Set(userStore.userPersons.map(a => a.groups.join()).join().split(','))]
})
const logout = () => {
signOut(userStore.auth)
.catch((error) => {
console.log(error)
})
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md text-default text-sm">
<div>
<h1 class="text-xl ml-2 font-bold">Privacybeleid</h1>
<div class="container">
<div class="item">
<h2 class="font-bold">Privacy</h2>
Gegevens binnen deze app worden gebruikt voor de interne organisatie. Via Google analytics wordt bijgehouden welke schermen het meest worden gebruikt. Daarnaast maken wij gebruik van Firebase voor het anoniem verzamelen van gegevens omtrent crashes, bugs en het gebruik van de app.
<h2 class="font-bold mt-5">AVG</h2>
Sinds 25 mei 2018 is de Algemene verordening gegevensbescherming (AVG) van toepassing waardoor elke vereniging helder moet maken wat zij doen om de privacy van persoonsgegevens te waarborgen. U kunt <a href="https://www.reddingsbrigadewaddinxveen.nl/Doc/201809%20-%20Privacyverklaring%20WRB.pdf" class="underline">hier</a> onze privacy verklaring vinden.
</div>
</div>
</div>
<div>
<h1 class="text-xl ml-2 font-bold"></h1>
<div class="container">
<div class="item break-words ">
Registration Token: <b>{{ userStore.registrationToken }}</b>
</div>
<div class="divider" />
<div class="item break-words ">
User ID: <b>{{ userStore.userData.id }}</b>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
title: 'Meer Informatie',
key: 'back'
})
const userStore = useUserStore()
</script>

View File

@ -0,0 +1,235 @@
<template>
<div @click.self="showModel = false" v-if="showModel" class="fixed flex justify-center items-center h-screen w-full bg-black top-0 left-0 z-50 bg-opacity-50" >
<form @submit.prevent="submitModelForm" class="dark:bg-neutral-800 bg-neutral-200 p-10 rounded-xl flex flex-col w-full max-w-sm">
<h1 class="font-bold text-center text-lg mb-5">Deelnemer Toevoegen</h1>
<label class="font-bold text-default">Deelnemer</label>
<select :disabled="modelData.edit" required v-model="modelData.relatiecode" class="input dark:bg-neutral-700 bg-neutral-300 mb-5">
<option v-if="!modelData.edit" v-for="user in contestStore.competitors.filter(x => !contest.events[modelData.event].competitors.map(y => y.relatiecode).includes(x.relatiecode))" :value="user.relatiecode">{{ user.name}} ({{ user.relatiecode }})</option>
<option v-else v-for="user in competitors" :value="user.relatiecode">{{ user.name}} ({{ user.relatiecode }})</option>
</select>
<label class="font-bold text-default">Tijd</label>
<div class="mb-1">
<input v-model="modelData.time.minutes" type="number" step="1" min="0" max="99" placeholder="mm" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
<span class="text-default text-xl font-bold mx-1">:</span>
<input v-model="modelData.time.seconds" type="number" step="1" min="0" max="99" placeholder="ss" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
<span class="text-default text-xl font-bold mx-1">:</span>
<input v-model="modelData.time.milliseconds" type="number" step="1" min="0" max="99" placeholder="ms" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
</div>
<div class="flex items-center mb-5">
<input type="checkbox" v-model="modelData.dsq" class="mr-1 checkbox">
<span class="text-default">Diskwalificatie</span>
</div>
<label class="font-bold">Info (Optioneel)</label>
<input v-model="modelData.info" type="text" placeholder="Bijv. Een diskwalificatie" class="input dark:bg-neutral-700 bg-neutral-300 mb-10" />
<input :disabled="disableButtons" type="submit" class="btn" :value="modelData.edit ? 'Bewerken' : 'Toevoegen'" />
</form>
</div>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<form @submit.prevent="submitContestForm" class="flex flex-col">
<label class="font-bold">Locatie Wedstrijd</label>
<input v-model="contest.location" required="true" class="input mb-5 " type="text">
<label class="font-bold">Type Zwembad</label>
<select v-model="contest.type" required="true" class="input mb-5 " type="text">
<option value="50m">50 Meter</option>
<option value="25m">25 Meter</option>
</select>
<label class="font-bold">Datum</label>
<input v-model="contest.date" required="true" class="input w-min hover:cursor-pointer pr-0 mb-5 " type="date">
<label class="font-bold">Onderdelen</label>
<div class="flex flex-col gap-y-3">
<div v-if="contestStore.competitors[0]" v-for="event in contest.events" class="container p-2">
<div @click="event.open = !event.open" class="flex hover:cursor-pointer">
<h2 class="font-bold">{{ event.name }}</h2>
<Icon size="1.2em" name="ion:arrow-down-b" class="ml-auto my-auto mr-2 transition-all" :class="{'rotate-180' : event.open }" />
</div>
<div v-if="event.open" class="mt-2">
<table class="table-fixed text-left w-full even:bg-gray-500">
<thead class="font-bold">
<tr>
<th class="w-3/6">Naam</th>
<th class="w-2/6">Tijd</th>
<th class="w-1/6">DSQ</th>
</tr>
</thead>
<tbody>
<tr @click="handleModel(competitor, event.id, true, index)" v-for="(competitor, index) in event.competitors" class="even:dark:bg-neutral-700 even:bg-neutral-300 hover:cursor-pointer">
<td class="py-1 pl-1">{{ contestStore.competitors.find(x => x.relatiecode === competitor.relatiecode ).name }}</td>
<td>{{ competitor.time.minutes.toString().padStart(2, '0') }}:{{ competitor.time.seconds.toString().padStart(2, '0') }}:{{ competitor.time.milliseconds.toString().padStart(2, '0') }}</td>
<td>{{ competitor.dsq }}</td>
</tr>
<tr v-if="contestStore.competitors.filter(x => !event.competitors.map(y => y.relatiecode).includes(x.relatiecode)).length > 0" class="even:dark:bg-neutral-700 even:bg-neutral-300">
<td @click="handleModel(null, event.id)" class="hover:cursor-pointer py-1 pl-1">+ Deelnemer toevoegen</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<input :disabled="disableButtons" type="submit" class="btn mt-10 px-5 w-min mx-auto" value="Wedstrijd toevoegen" />
</form>
</div>
</template>
<script setup>
import { getDocs, collection, writeBatch, doc, getFirestore } from "firebase/firestore"
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Wedstrijd Toevoegen',
key: 'back'
})
const toast = useToast()
const contestStore = useContestStore()
const userStore = useUserStore()
const db = getFirestore()
const showModel = ref(false)
const disableButtons = ref(false)
const modelData = ref({
relatiecode: '',
time: {
minutes: null,
seconds: null,
milliseconds: null,
},
dsq: false,
info: '',
})
const contest = ref({
location: '',
type: '',
date: Date,
events: {
obstacleSwim: {
open: false,
name: '200m Obstacle Swim',
id: 'obstacleSwim',
competitors: [],
},
manikinCarry: {
open: false,
name: '50m Manikin Carry',
id: 'manikinCarry',
competitors: [],
},
rescueMedley: {
open: false,
name: '100m Rescue Medley',
id: 'rescueMedley',
competitors: [],
},
manikinCarryWithFins: {
open: false,
name: '100m Manikin Carry with Fins',
id: 'manikinCarryWithFins',
competitors: [],
},
manikinTowWithFins: {
open: false,
name: '100m Manikin Tow with Fins',
id: 'manikinTowWithFins',
competitors: [],
},
superLifesaver: {
open: false,
name: '200m Super Lifesaver',
id: 'superLifesaver',
competitors: [],
},
}
})
const handleModel = (competitor, event, edit, index) => {
if(!competitor) competitor = {
relatiecode: '',
time: {
minutes: null,
seconds: null,
milliseconds: null,
},
dsq: false,
info: '',
}
modelData.value = competitor
modelData.value.event = event
modelData.value.edit = edit
modelData.value.index = index
showModel.value = true
}
const submitModelForm = () => {
if (!modelData.value.time.minutes) modelData.value.time.minutes = 0
if (!modelData.value.time.seconds) modelData.value.time.seconds = 0
if (!modelData.value.time.milliseconds) modelData.value.time.milliseconds = 0
const index = modelData.value.index
const edit = modelData.value.edit
delete modelData.value.index
delete modelData.value.edit
if (!edit) contest.value.events[modelData.value.event].competitors.push(modelData.value)
showModel.value = false
}
const submitContestForm = async () => {
disableButtons.value = true
const batch = writeBatch(db)
Object.values(contest.value.events).forEach(event => {
event.competitors.forEach(competitor => {
const combinedTime = competitor.time.minutes.toString().padStart(2, '0') + competitor.time.seconds.toString().padStart(2, '0') + competitor.time.milliseconds.toString().padStart(2, '0')
const docRef = doc(collection(db, 'timings'))
batch.set(docRef, {
relatiecode: competitor.relatiecode,
contest: { location: contest.value.location, date: new Date(contest.value.date), type: contest.value.type },
event: event.id,
time: { minutes: competitor.time.minutes.toString().padStart(2, '0'), seconds: competitor.time.seconds.toString().padStart(2, '0'), milliseconds: competitor.time.milliseconds.toString().padStart(2, '0'), combined: combinedTime },
dsq: competitor.dsq,
info: competitor.info || ''
})
})
})
await batch.commit()
disableButtons.value = false
toast.success('Wedstrijd is toegevoegd')
navigateTo('/wedstrijd')
}
onMounted(() => {
contestStore.getCompetitors()
})
</script>
<style scoped>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
}
</style>

View File

@ -0,0 +1,263 @@
<template>
<div @click.self="showModel = false" v-if="showModel" class="fixed flex justify-center items-center h-screen w-full bg-black top-0 left-0 z-50 bg-opacity-50" >
<form @submit.prevent="submitModalForm" class="dark:bg-neutral-800 bg-neutral-200 p-10 rounded-xl flex flex-col w-full max-w-sm">
<h1 class="font-bold text-center text-lg mb-5">Tijd {{ modelData.eventName }}</h1>
<label class="font-bold">Locatie</label>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.contest.location" type="text" class="input dark:bg-neutral-700 bg-neutral-300 mb-5" />
<label class="font-bold">Type zwembad</label>
<select :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.contest.type" required="true" class="input dark:bg-neutral-700 bg-neutral-300 mb-5">
<option value="50m">50 Meter</option>
<option value="25m">25 Meter</option>
</select>
<label class="font-bold">Datum</label>
<input :disabled="!userStore.userData.wedstrijdAdmin" :value="dateToYYYYMMDD(modelData.contest.date)" @input="modelData.contest.date = $event.target.valueAsDate" required="true" class="input dark:bg-neutral-700 bg-neutral-300 w-min hover:cursor-pointer pr-0 mb-5 " type="date">
<label class="font-bold">Tijd</label>
<div class="mb-1">
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.time.minutes" type="number" step="1" min="0" max="99" placeholder="mm" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
<span class="text-default text-xl font-bold mx-1">:</span>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.time.seconds" type="number" step="1" min="0" max="99" placeholder="ss" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
<span class="text-default text-xl font-bold mx-1">:</span>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.time.milliseconds" type="number" step="1" min="0" max="99" placeholder="ms" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
</div>
<div class="flex items-center mb-5">
<input :disabled="!userStore.userData.wedstrijdAdmin" type="checkbox" v-model="modelData.dsq" class="mr-1 checkbox">
<span class="text-default">Diskwalificatie</span>
</div>
<label class="font-bold">Info </label>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.info" type="text" placeholder="Bijv. Een diskwalificatie" class="input dark:bg-neutral-700 bg-neutral-300 mb-10" />
<input v-if="userStore.userData.wedstrijdAdmin" :disabled="disableButtons" type="submit" class="btn" value="Bewerken" />
</form>
</div>
<div v-if="contestStore.timings[0]" class="flex flex-col justify-center items-center gap-y-3 px-2 overflow-hidden">
<div class="flex gap-x-5">
<div class="relative">
<button @click.stop="showDeelnemersDropdown = !showDeelnemersDropdown" class="btn">Deelnemers <Icon size="1.2em" name="ion:arrow-down-b" /></button>
<div v-if="showDeelnemersDropdown" v-on-click-outside.bubble="handleDeelnemersDropdown" class="w-48 mt-2 container absolute rounded-lg shadow p-3 overflow-y-auto">
<ul class="space-y-2 text-default">
<li v-for="competitor in contestStore.competitors" @click="competitor.checked = !competitor.checked" class="flex gap-x-1 items-center hover:cursor-pointer">
<input v-model="competitor.checked" type="checkbox" class="checkbox">
<label class="hover:cursor-pointer">{{ competitor.name }}</label>
</li>
</ul>
</div>
</div>
<div class="relative">
<button @click.stop="showPropertiesDropdown = !showPropertiesDropdown" class="btn">Eigenschappen <Icon size="1.2em" name="ion:arrow-down-b" /></button>
<div v-if="showPropertiesDropdown" v-on-click-outside.bubble="handlePropertiesDropdown" class="w-48 mt-2 container absolute rounded-lg shadow p-3">
<ul class="space-y-2 text-default">
<li v-for="property in Object.values(properties)" @click="property.enabled = !property.enabled" class="flex gap-x-1 items-center hover:cursor-pointer">
<input v-model="property.enabled" type="checkbox" class="checkbox">
<label class="hover:cursor-pointer">{{ property.name }}</label>
</li>
</ul>
</div>
</div>
</div>
<div v-for="event in events" class="container w-full max-w-3xl py-2 px-4">
<div @click="event.open = !event.open" class="flex hover:cursor-pointer">
<h2 class="font-bold mr-auto">{{ event.name }}</h2>
<span v-if="contestStore.filteredTimings.filter(a => a.event === event.id).length > 0" class="">
<span>
{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].time.minutes }}:{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].time.seconds }}:{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].time.milliseconds }} |
</span>
<span class="hidden md:inline-block mr-1">
{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].contest.type }} |
{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].contest.date.toLocaleDateString('nl-NL') }} |
{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].contest.location }} |
</span>
</span>
<span v-else class="">Geen tijd</span>
<Icon size="1.2em" name="ion:arrow-down-b" class="my-auto ml-2 transition-all" :class="{'rotate-180' : event.open }" />
</div>
<div v-if="event.open" class="mt-2">
<table class="table-fixed text-left w-full even:bg-gray-500">
<thead class="font-bold">
<tr>
<th v-for="property in Object.values(properties).filter(a => a.enabled === true)">{{ property.name }}</th>
</tr>
</thead>
<tbody>
<tr @click="handleModel(time, event, index)" v-for="(time, index) in contestStore.filteredTimings.filter(a => a.event === event.id)" class="even:dark:bg-neutral-700 even:bg-neutral-300 hover:cursor-pointer">
<td v-if="properties.time.enabled" class="pl-1" :class="time.dsq ? 'line-through' : ''">{{ time.time.minutes }}:{{ time.time.seconds }}:{{ time.time.milliseconds}}</td>
<td v-if="properties.date.enabled">{{ time.contest.date.toLocaleDateString('nl-NL') }}</td>
<td v-if="properties.name.enabled" class="overflow-hidden whitespace-nowrap truncate">{{ contestStore.competitors.filter(a => a.relatiecode === time.relatiecode)[0].name.split(', ')[1] + ' ' + contestStore.competitors.filter(a => a.relatiecode === time.relatiecode)[0].name.split(', ')[0] }}</td>
<td v-if="properties.type.enabled">{{ time.contest.type }}</td>
<td v-if="properties.location.enabled">{{ time.contest.location }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { collection, query, getDocs, doc, updateDoc } from "firebase/firestore";
import { useToast } from 'vue-toastification'
import { vOnClickOutside} from '@vueuse/components'
definePageMeta({
title: 'Brigade Tijden',
key: 'back'
})
const toast = useToast()
const userStore = useUserStore()
const contestStore = useContestStore()
const showModel = ref(false)
const disableButtons = ref(false)
const showDeelnemersDropdown = ref(false)
const showPropertiesDropdown = ref(false)
const handleDeelnemersDropdown = () => {
showDeelnemersDropdown.value = false
}
const handlePropertiesDropdown = () => {
showPropertiesDropdown.value = false
}
const modelData = ref({
time: {
minutes: null,
seconds: null,
milliseconds: null,
},
dsq: false,
info: '',
contest: {
type: '',
location: '',
date: Date,
}
})
const properties = ref({
time: {
enabled: true,
id: 'time',
name: 'Tijd',
},
date: {
enabled: true,
id: 'date',
name: 'Datum',
},
name: {
enabled: true,
id: 'name',
name: 'Naam',
},
type: {
enabled: false,
id: 'type',
name: 'Type zwembad',
},
location: {
enabled: false,
id: 'location',
name: 'Locatie',
},
})
const events = ref({
obstacleSwim: {
open: false,
name: '200m Obstacle Swim',
id: 'obstacleSwim',
competitors: [],
},
manikinCarry: {
open: false,
name: '50m Manikin Carry',
id: 'manikinCarry',
competitors: [],
},
rescueMedley: {
open: false,
name: '100m Rescue Medley',
id: 'rescueMedley',
competitors: [],
},
manikinCarryWithFins: {
open: false,
name: '100m Manikin Carry with Fins',
id: 'manikinCarryWithFins',
competitors: [],
},
manikinTowWithFins: {
open: false,
name: '100m Manikin Tow with Fins',
id: 'manikinTowWithFins',
competitors: [],
},
superLifesaver: {
open: false,
name: '200m Super Lifesaver',
id: 'superLifesaver',
competitors: [],
},
})
onMounted(async () => {
await contestStore.getCompetitors()
await contestStore.getTimings()
contestStore.selectCompetitors('all')
})
const dateToYYYYMMDD = (d) => {
return d && new Date(d.getTime()-(d.getTimezoneOffset()*60*1000)).toISOString().split('T')[0]
}
const handleModel = (competitor, e, index) => {
modelData.value = competitor
modelData.value.index = index
modelData.value.eventId = e.id
showModel.value = true
}
const submitModalForm = async () => {
if (!modelData.value.time.minutes) modelData.value.time.minutes = 0
if (!modelData.value.time.seconds) modelData.value.time.seconds = 0
if (!modelData.value.time.milliseconds) modelData.value.time.milliseconds = 0
const id = modelData.value.id
delete modelData.value.index
delete modelData.value.eventId
delete modelData.value.id
disableButtons.value = true
const combinedTime = modelData.value.time.minutes.toString().padStart(2, '0') + modelData.value.time.seconds.toString().padStart(2, '0') + modelData.value.time.milliseconds.toString().padStart(2, '0')
modelData.value.time.combinedTime = combinedTime
const docRef = doc(db, "timings", id);
await updateDoc(docRef, modelData.value);
toast.success('Tijd is bewerkt')
showModel.value = false
disableButtons.value = false
}
</script>
<style scoped>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
}
</style>

View File

@ -0,0 +1,28 @@
<template>
<div class="flex flex-col justify-center items-center px-2 overflow-hidden">
<div class="container w-full max-w-md">
<NuxtLink to="/wedstrijd/owntimes" class="rounded-t item-hover py-2 flex items-center">
<span>Eigen Tijden</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />
<NuxtLink to="/wedstrijd/alltimes" class="item-hover py-2 flex items-center">
<span>Brigade Tijden</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div v-if="userStore.userData.wedstrijdAdmin" class="divider" />
<NuxtLink v-if="userStore.userData.wedstrijdAdmin" to="/wedstrijd/addcontest" class="rounded-b item-hover py-2 flex items-center">
<span>Tijden Toevoegen</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
</div>
</div>
</template>
<script setup>
definePageMeta({
title: 'Wedstrijd'
})
const userStore = useUserStore()
</script>

View File

@ -0,0 +1,190 @@
<template>
<div @click.self="showModel = false" v-if="showModel" class="fixed flex justify-center items-center h-screen w-full bg-black top-0 left-0 z-50 bg-opacity-50" >
<form @submit.prevent="submitModalForm" class="dark:bg-neutral-800 bg-neutral-200 p-10 rounded-xl flex flex-col w-full max-w-sm">
<h1 class="font-bold text-center text-lg mb-5">Tijd {{ modelData.eventName }}</h1>
<label class="font-bold">Locatie</label>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.contest.location" type="text" class="input dark:bg-neutral-700 bg-neutral-300 mb-5" />
<label class="font-bold">Datum</label>
<input :disabled="!userStore.userData.wedstrijdAdmin" :value="dateToYYYYMMDD(modelData.contest.date)" @input="modelData.contest.date = $event.target.valueAsDate" required="true" class="input dark:bg-neutral-700 bg-neutral-300 w-min hover:cursor-pointer pr-0 mb-5 " type="date">
<label class="font-bold">Type zwembad</label>
<select :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.contest.type" required="true" class="input dark:bg-neutral-700 bg-neutral-300 mb-5">
<option value="50m">50 Meter</option>
<option value="25m">25 Meter</option>
</select>
<label class="font-bold">Tijd</label>
<div class="mb-1">
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.time.minutes" type="number" step="1" min="0" max="99" placeholder="mm" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
<span class="text-default text-xl font-bold mx-1">:</span>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.time.seconds" type="number" step="1" min="0" max="99" placeholder="ss" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
<span class="text-default text-xl font-bold mx-1">:</span>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.time.milliseconds" type="number" step="1" min="0" max="99" placeholder="ms" class="input dark:bg-neutral-700 bg-neutral-300 w-10 text-center p-1" />
</div>
<div class="flex items-center mb-5">
<input :disabled="!userStore.userData.wedstrijdAdmin" type="checkbox" v-model="modelData.dsq" class="mr-1 checkbox">
<span class="text-default">Diskwalificatie</span>
</div>
<label class="font-bold">Info </label>
<input :disabled="!userStore.userData.wedstrijdAdmin" v-model="modelData.info" type="text" placeholder="Bijv. Een diskwalificatie" class="input dark:bg-neutral-700 bg-neutral-300 mb-10" />
<input v-if="userStore.userData.wedstrijdAdmin" :disabled="disableButtons" type="submit" class="btn" value="Bewerken" />
</form>
</div>
<div v-if="contestStore.filteredTimings[0]" class="flex flex-col justify-center items-center gap-y-3 px-2 overflow-hidden">
<div v-for="event in events" class="container w-full max-w-md py-2 px-4">
<div @click="event.open = !event.open" class="flex hover:cursor-pointer">
<h2 class="font-bold">{{ event.name }}</h2>
<span v-if="contestStore.filteredTimings.filter(a => a.event === event.id)[0]" class="ml-auto">{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].time.minutes }}:{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].time.seconds }}:{{ contestStore.filteredTimings.filter(a => a.event === event.id)[0].time.milliseconds }}</span>
<span v-else class="ml-auto">Geen tijd</span>
<Icon size="1.2em" name="ion:arrow-down-b" class="my-auto ml-2 transition-all" :class="{'rotate-180' : event.open }" />
</div>
<div v-if="event.open" class="mt-2">
<table class="table-fixed text-left w-full even:bg-gray-500">
<thead class="font-bold">
<tr>
<th class="w-3/7">Tijd</th>
<th class="w-1/7">Datum</th>
<th class="w-3/7">Type</th>
</tr>
</thead>
<tbody>
<tr @click="handleModel(time, event, index)" v-for="(time, index) in contestStore.filteredTimings.filter(a => a.event === event.id)" class="even:dark:bg-neutral-700 even:bg-neutral-300 hover:cursor-pointer">
<td class="pl-1" :class="time.dsq ? 'line-through' : ''">{{ time.time.minutes }}:{{ time.time.seconds }}:{{ time.time.milliseconds}}</td>
<td>{{ time.contest.date.toLocaleDateString('nl-NL') }}</td>
<td>{{ time.contest.type }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { collection, query, where, getDocs, doc, updateDoc} from "firebase/firestore";
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Eigen Tijden',
key: 'back'
})
const toast = useToast()
const contestStore = useContestStore()
const userStore = useUserStore()
const showModel = ref(false)
const disableButtons = ref(false)
const modelData = ref({
time: {
minutes: null,
seconds: null,
milliseconds: null,
},
dsq: false,
info: '',
contest: {
type: '',
location: '',
date: '',
}
})
const events = ref({
obstacleSwim: {
open: false,
name: '200m Obstacle Swim',
id: 'obstacleSwim',
competitors: [],
},
manikinCarry: {
open: false,
name: '50m Manikin Carry',
id: 'manikinCarry',
competitors: [],
},
rescueMedley: {
open: false,
name: '100m Rescue Medley',
id: 'rescueMedley',
competitors: [],
},
manikinCarryWithFins: {
open: false,
name: '100m Manikin Carry with Fins',
id: 'manikinCarryWithFins',
competitors: [],
},
manikinTowWithFins: {
open: false,
name: '100m Manikin Tow with Fins',
id: 'manikinTowWithFins',
competitors: [],
},
superLifesaver: {
open: false,
name: '200m Super Lifesaver',
id: 'superLifesaver',
competitors: [],
},
})
onMounted(async () => {
await contestStore.getTimings()
contestStore.selectCompetitors('user', userStore.userData.relatiecodes)
})
const handleModel = (competitor, e, index) => {
modelData.value = competitor
modelData.value.index = index
modelData.value.eventId = e.id
showModel.value = true
}
const dateToYYYYMMDD = (d) => {
return d && new Date(d.getTime()-(d.getTimezoneOffset()*60*1000)).toISOString().split('T')[0]
}
const submitModalForm = async () => {
if (!modelData.value.time.minutes) modelData.value.time.minutes = 0
if (!modelData.value.time.seconds) modelData.value.time.seconds = 0
if (!modelData.value.time.milliseconds) modelData.value.time.milliseconds = 0
const id = modelData.value.id
delete modelData.value.index
delete modelData.value.eventId
delete modelData.value.id
disableButtons.value = true
const combinedTime = modelData.value.time.minutes.toString().padStart(2, '0') + modelData.value.time.seconds.toString().padStart(2, '0') + modelData.value.time.milliseconds.toString().padStart(2, '0')
modelData.value.time.combinedTime = combinedTime
const docRef = doc(db, "timings", id);
await updateDoc(docRef, modelData.value);
toast.success('Tijd is bewerkt')
showModel.value = false
disableButtons.value = false
}
</script>
<style scoped>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
}
</style>

View File

@ -0,0 +1,23 @@
import { initializeApp } from "firebase/app";
import { getMessaging } from "firebase/messaging";
import { getAnalytics } from "firebase/analytics";
export default defineNuxtPlugin((nuxtApp) => {
const firebaseConfig = {
apiKey: "AIzaSyCtHFyfCRkBt8MX5LPFogBi8ssKSypkW0g",
authDomain: "wrbapp.firebaseapp.com",
projectId: "wrbapp",
storageBucket: "wrbapp.appspot.com",
messagingSenderId: "160377508482",
appId: "1:160377508482:web:f651ccf2b242daf4879a9b",
measurementId: "G-31HEXDSVPZ"
};
const app = initializeApp(firebaseConfig);
try {
const messaging = getMessaging(app);
} catch {}
const analytics = getAnalytics(app);
})

View File

@ -0,0 +1,6 @@
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Toast)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,33 @@
self.addEventListener('notificationclick', (event) => {
console.log('On notification click: ', event.notification.tag);
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({
type: "window"
}).then((clientList) => {
for (const client of clientList) {
if (client.url === '/news' && 'focus' in client)
return client.focus();
}
if (clients.openWindow)
return clients.openWindow('/news');
}));
});
importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js');
const firebaseConfig = {
apiKey: "AIzaSyCtHFyfCRkBt8MX5LPFogBi8ssKSypkW0g",
authDomain: "wrbapp.firebaseapp.com",
projectId: "wrbapp",
storageBucket: "wrbapp.appspot.com",
messagingSenderId: "160377508482",
appId: "1:160377508482:web:f651ccf2b242daf4879a9b",
measurementId: "G-31HEXDSVPZ"
};
const app = firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();

BIN
frontend/public/ios/100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

BIN
frontend/public/ios/114.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
frontend/public/ios/120.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
frontend/public/ios/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
frontend/public/ios/144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
frontend/public/ios/152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
frontend/public/ios/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Some files were not shown because too many files have changed in this diff Show More