Merge stable version before large change #16

Merged
xeovalyte merged 28 commits from dev into main 2023-02-14 15:37:45 +01:00
37 changed files with 8603 additions and 8713 deletions

View File

@ -1,26 +1,6 @@
pipeline:
build-frontend:
image: node:16
commands:
- cd frontend
- npm i
- npm run generate
publish-frontend:
image: node:16
secrets:
- meli_api_token
commands:
- cd frontend
- npx -p "@getmeli/cli" meli upload .output/public --url https://meli.xeovalyte.dev --site 1e43e574-3eea-4e90-8c52-8a9bcab54f3a --token $$MELI_API_TOKEN --branch "main"
build-backend:
image: node:16
commands:
- cd backend
- npm i
publish-backend:
image: plugins/docker
publish-dev:
image: plugins/docker
secrets:
- docker_password
settings:
@ -29,7 +9,7 @@ pipeline:
from_secret: docker_password
repo: gitea.xeovalyte.dev/xeovalyte/wrbapp
tags:
- latest
- latest
registry: gitea.xeovalyte.dev
when:
branch: dev

View File

@ -1,9 +1,12 @@
FROM node:16
FROM node:18
WORKDIR /usr/src/app
COPY ./backend .
COPY ./frontend .
EXPOSE 7289
RUN npm install
RUN npm run build
CMD [ "node", "index.js" ]
EXPOSE 3000
CMD [ "node", ".output/server/index.mjs" ]

1
backend/.gitignore vendored
View File

@ -1 +0,0 @@
node_modules

View File

@ -1,12 +0,0 @@
{
"type": "service_account",
"project_id": "wrbapp",
"private_key_id": "1a8c65688260fed51f37d6d9383f6510eb066362",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPIOoKcZwGuW3V\nBwImp+1MV0VroKKOpg2XIwcUEvoGe4DDQ2EyTnCXOc14DRoQhlzU3YdfVX6WwXP6\nvRAKj5Bk1ML24Rm5/0ZuiJ2NvQ1A3le3htuKrLyfKjkBhNNc4m8/7ObHZ/t8wiKM\naR9XOCk6wLX7uB74YQdGxEdholRdvpNqPTPXSms+hWXchTBWDygNO2IZyMEdwswc\nlN8Lw0CfPrOZw+bUaGdNa8wf26jUECLZXKETeNdw+ZHqOT+0dG4pmeXt98/Oyui+\njyLs1hqbzRHCc3ap0/QjsKJKfjns8m7KHr6wNjlAeCbURb/lJddYTztsJINDX/ak\n5R4oj34dAgMBAAECggEAFROTlSP53TpNqY1ow9+3FxtTSlAicnMFs4EiNYH31KSJ\nFfV7hLe0H8NHO+XTZuaROCzhm0sTEkqVn2hRI034Araeryn5KPLHqzQ2GgfoexcU\n5G0x1lLc77JHcjbenhdBfEcCNbzIO6nyg4in4oNpuJuG+RoDdsFC0QnkfJQc0xwE\nOuWQRFmy2t2fmwP/HQ+SLioSlpfnRgo8CIJyQ5IsCXF7CAFWMhur9mMRSAUiloLU\nv/8+PUrwf8/1YAX+pI95NQ8Xl2HwLwBpvye/P/5v7Xd5tNJnBB5/QQYVmXC1T/01\nuw11Xvg/Y4LReOy9Fo67hDkSsSmTMUT9ypH5gDFKcQKBgQDrLh9E1UqfMiWmss+J\nS4zxwGmvBIU0cOVngeGBCzPJpbIZZ+VIe455vOrQB49xa5mEf3Hc063HYWGs9+hR\n2gkK1XVoBwfKPxPGoL2Tk1M3msjOtFaaW0AcrbWREniSmVMZjDypHRt7D0DTMkke\ngnVvkhX0kusVGSuhE2spIXa4cQKBgQDhdw9eYmIZ0ejv3MQ5s0FWM3ktPKZ4jXGl\nB3lN76cn1WChPVLquN1hVLG2PCfDTEU91z6bRMRmm0n1oHfi9V98rsZUcauyuO5H\nF1JYg0K5ZSLjkCrITlPVovpJAViDluWB5WawYIOdY63GBQn6tm4HmEn/fFz0U8pw\nSLEvZl1WbQKBgQCv+SzwhmB1ykId/8IGy39FDWKG0O0TFj6xOqAPvOAdTFx9Yh5Q\nJBOxx8gzrNSKW6bdW7dJMyLfA2Dg2gb96BXIA3z8P/Z2QMh9YZ04pY4pFyqWcJ40\nlX7ddqVbTeTmXM+vWB2ztNHxPLKW1ROdPqS8vSSsgppgiRr6RdtzRVTeIQKBgErd\nHrRHVK2gHolus5U5KSu3Qbg8mEYVKTQT7DptpgI6/q/rTdn0ckW8Opn5FXbqn18u\nVnJ1/gTX8VHm64fn08HxwpcNe2aHs07Vtpj/VKt8on4PQ7VpFLsuN48ALGTdOO3N\nvzA3i9w52dyTlcGyy4woDAISSEc0f1aTPIoxojJtAoGAFn7IuYlO+7mludy/LaiN\ndfz4VbXJglSIqSH6JNy2OnHJSK43bQV2MPJ9FVxN1WKHixdxTcrEdLWMvtlymHJq\ne90seug41Op+nT3e3HdhIf1HeVAFr5vxNbFABgpdkVDJs8lnVJP13D0mEjF5CRUS\nLUFh+r1bedz4f1R2s8ODhWQ=\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-iqgyy@wrbapp.iam.gserviceaccount.com",
"client_id": "101347063281519043654",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-iqgyy%40wrbapp.iam.gserviceaccount.com"
}

View File

@ -1,179 +0,0 @@
const express = require('express');
const cors = require('cors');
const { initializeApp, applicationDefault, cert } = require('firebase-admin/app');
const { getFirestore, Timestamp, FieldValue } = require('firebase-admin/firestore');
const { getMessaging } = require('firebase-admin/messaging')
const { getAuth } = require('firebase-admin/auth')
const serviceAccount = require('./firebase.json');
initializeApp({
credential: cert(serviceAccount)
});
const db = getFirestore();
const app = express();
app.use(express.json());
app.use(cors({
origin: '*',
}));
app.listen(7289, () => console.log('API is online!'));
app.get('/', (req, res) => {
res.status(200).send({
status: 'success',
});
});
app.post('/checkrelatiecode', async (req, res) => {
const { relatiecode, email } = req.body;
if (!relatiecode) return res.status(400).send({ code: 'no-relatiecode'})
if (!email) return res.status(400).send({ code: 'no-email'})
try {
const docRef = db.collection('ledenlijst').doc(relatiecode);
const doc = await docRef.get();
if (!doc.exists) return res.status(400).send({ code: 'incorrect'})
const data = doc.data()
if (data.email[0] === email || data.email[1] === email) {
return res.status(200).send({ code: 'correct'})
} else {
return res.status(400).send({ code: 'incorrect'})
}
} catch (e) {
return res.status(500).send({ code: 'error', error: e })
}
})
app.post('/getrelatiecodes', async (req, res) => {
const { email } = req.body;
if (!email) return res.status(400).send({ code: 'no-email'})
try {
const ledenlijstRef = db.collection('ledenlijst')
const snapshot = await ledenlijstRef.where("email", "array-contains", email).get()
if (snapshot.empty) {
res.status(400).send({ code: 'no-relatiecodes'})
return;
}
let relatiecodes = [];
let persons = [];
snapshot.forEach(doc => {
relatiecodes.push(doc.id)
const data = doc.data()
persons.push({ fullName: data.fullName, relatiecode: doc.id })
});
res.status(200).send({ code: 'success', relatiecodes: relatiecodes, persons: persons })
} catch (e) {
return res.status(500).send({ code: 'error', error: e })
}
})
app.post('/subscribetotopic', async (req, res) => {
const { topic, registrationToken } = req.body;
if (!topic) return res.status(400).send({ code: 'no-topic'})
if (!registrationToken) return res.status(400).send({ code: 'no-registrationToken'})
try {
getMessaging().subscribeToTopic([registrationToken], topic)
.then((response) => {
console.log('Successfully subscribed to topic:', response);
res.status(200).send({ code: 'success' })
})
.catch((error) => {
console.log('Error subscribing to topic:', error);
return res.status(500).send({ code: 'error', error: error })
});
} catch (e) {
return res.status(500).send({ code: 'error', error: e })
}
})
app.post('/sendmessage', async (req, res) => {
const { title, body, token } = req.body;
if (!title) return res.status(400).send({ code: 'no-topic'})
if (!body) return res.status(400).send({ code: 'no-registrationToken'})
if (!token) return res.status(400).send({ code: 'no-token'})
try {
getAuth()
.verifyIdToken(token)
.then(async (decodedToken) => {
const uid = decodedToken.uid;
const docRef = db.collection('users').doc(uid);
const doc = await docRef.get();
if (!doc.exists) return res.status(400).send({ code: 'not-found'})
const data = doc.data()
if (!data.sendNews) return res.status(400).send({ code: 'no-permissions'})
const message = {
notification: {
title: title,
body: body,
icon: 'https://wrbapp.xeovalyte.com/ios/256.png',
},
topic: 'all',
apns: {
payload: {
aps: {
sound: 'default'
}
}
}
};
getMessaging().send(message)
.then((response) => {
// Response is a message ID string.
console.log('Successfully sent message:', response);
res.status(200).send({ code: 'success', response: response })
})
.catch((error) => {
console.log('Error sending message:', error);
return res.status(500).send({ code: 'error', error: error })
});
})
.catch((error) => {
console.log(error)
return res.status(500).send({ code: 'error', error: error })
});
} catch (e) {
return res.status(500).send({ code: 'error', error: e })
}
})

Binary file not shown.

4036
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.1",
"firebase-admin": "^11.0.1"
}
}

2
frontend/.gitignore vendored
View File

@ -6,3 +6,5 @@ node_modules
.output
.env
dist
service-account.json

View File

@ -1,22 +1,22 @@
<template>
<div v-if="!userLoaded" 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 v-if="!userLoaded" 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 v-else class="">
<div v-if="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="">
<div v-if="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>
</template>
<script setup>
@ -48,149 +48,148 @@ const users = ref([])
const messaging = ref(null)
onMounted(() => {
auth.value = getAuth()
auth.value = getAuth()
if (process.client) {
if ('serviceWorker' in navigator && window.isSecureContext) {
Device.getInfo().then(info => {
if (info.platform === 'web') registerSW()
});
Device.getInfo().then(info => {
if (info.platform === 'ios') document.getElementsByClassName('top-right')[0].classList.add('toastios')
});
if (process.client) {
if ('serviceWorker' in navigator && window.isSecureContext) {
Device.getInfo().then(info => {
if (info.platform === 'web') registerSW()
else document.getElementsByClassName('top-right')[0].classList.add('toastios')
});
}
}
onAuthStateChanged(auth.value, async (usr) => {
if (usr) {
user.value = usr
let docRef = doc(db, "users", user.value.uid);
let docSnap = await getDoc(docRef);
if (docSnap.exists()) {
const data = docSnap.data()
userData.value = data
getPersons(userData.value.relatiecodes)
} else {
setTimeout(() => window.location.reload(true), 1000)
}
if (!userData.value.sendNews && route.path === '/news/newmessage') navigateTo('/')
if (!userData.value.admin && route.path.startsWith('/settings/admin')) navigateTo('/')
isAuthenticated.value = true
logDeviceInfo()
} else {
isAuthenticated.value = false
user.value = null
userData.value = null
userPersons.value = []
}
onAuthStateChanged(auth.value, async (usr) => {
if (usr) {
user.value = usr
let docRef = doc(db, "users", user.value.uid);
let docSnap = await getDoc(docRef);
if (docSnap.exists()) {
const data = docSnap.data()
userData.value = data
getPersons(userData.value.relatiecodes)
} else {
setTimeout(() => window.location.reload(true), 1000)
}
if (!userData.value.sendNews && route.path === '/news/newmessage') navigateTo('/')
if (!userData.value.admin && route.path.startsWith('/settings/admin')) navigateTo('/')
isAuthenticated.value = true
logDeviceInfo()
} else {
isAuthenticated.value = false
user.value = null
userData.value = null
userPersons.value = []
}
userLoaded.value = true
})
userLoaded.value = true
})
})
const getPersons = async (persons) => {
userPersons.value = [];
userPersons.value = [];
for (let i = 0; i < persons.length; i++) {
const docRef = doc(db, "ledenlijst", persons[i]);
const docSnap = await getDoc(docRef);
for (let i = 0; i < persons.length; i++) {
const docRef = doc(db, "ledenlijst", persons[i]);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
userPersons.value.push(docSnap.data())
}
if (docSnap.exists()) {
userPersons.value.push(docSnap.data())
}
}
}
const setupNotifications = () => {
console.log('Initializing HomePage');
console.log('Initializing HomePage');
// 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')
// 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);
registrationToken.value = 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')
}
});
// On success, we should be able to receive notifications
PushNotifications.addListener('registration',
(token) => {
// alert('Push registration success, token: ' + token.value);
registrationToken.value = token
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')
fetch('https://api.xeovalyte.com/subscribetotopic', {
method: 'POST',
headers: {
Authorization: 'Basic WGVvdmFseXRlOmtNKjhuRXMzNTchalJlXm1KYnZrRSFOIw==',
'content-type': 'application/json'
},
body: JSON.stringify({ topic: 'all', registrationToken: token.value })
}).then(response => response.json())
.then(response => {
console.log(response)
})
.catch(err => {
console.log(err)
toast.error('Error tijdens het registreren van push notificaties')
});
}
);
console.log(error)
}
);
// Some issue with our setup and push will not work
PushNotifications.addListener('registrationError',
(error) => {
toast.error('Error tijdens het registreren van push notificaties')
// Show us the notification payload if the app is open on our device
PushNotifications.addListener('pushNotificationReceived',
(notification) => {
console.log(error)
}
);
toast.info(`${notification.title}`, {
onClick: () => navigateTo('/news')
})
// 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')
}
);
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(notification) => {
navigateTo('/news')
}
);
}
const registerFCM = () => {
getToken(messaging.value, { vapidKey: 'BI7l3nyGV6wJcFh7wrwmQ42W7RSXl46bmhXZJmDd4P-0K_JFP0ClTqjO-rr5H5DXBbmVR4kXwxFpUlo_d6cUy4Q' }).then((currentToken) => {
getToken(messaging.value, { vapidKey: 'BI7l3nyGV6wJcFh7wrwmQ42W7RSXl46bmhXZJmDd4P-0K_JFP0ClTqjO-rr5H5DXBbmVR4kXwxFpUlo_d6cUy4Q' }).then(async (currentToken) => {
if (currentToken) {
console.log(currentToken)
fetch('https://api.xeovalyte.com/subscribetotopic', {
method: 'POST',
headers: {
Authorization: 'Basic WGVvdmFseXRlOmtNKjhuRXMzNTchalJlXm1KYnZrRSFOIw==',
'content-type': 'application/json'
},
body: JSON.stringify({ topic: 'all', registrationToken: currentToken })
}).then(response => response.json())
.then(response => {
console.log(response)
})
.catch(err => {
console.log(err)
});
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')
}
registrationToken.value = currentToken
console.log('Subscribed to topic!')
} else {
// Show permission request UI
console.log('No registration token available. Request permission to generate one.');
@ -227,13 +226,16 @@ const logDeviceInfo = async () => {
const ledenlijst = ref([])
provide('firebase', { db, ledenlijst, isAuthenticated, user, userData, userPersons, auth, users, userAllPersons, getPersons, calEvents, news, registrationToken, contestTimes, competitors })
provide('firebase', { db, ledenlijst, isAuthenticated, user, userData, userPersons, auth, users, userAllPersons, getPersons, calEvents, news, registrationToken, contestTimes, competitors, registrationToken })
</script>
<style scoped>
<style>
.body {
padding-top: 10px;
margin-bottom: env(safe-area-inset-bottom);
}
.toastios {
padding-top: 20px;
}
</style>

View File

@ -34,4 +34,4 @@
.text-default {
@apply dark:text-gray-300 text-gray-900
}
}
}

View File

@ -1,43 +1,43 @@
<template>
<div class="flex flex-col justify-center items-center px-2">
<h1 class="font-bold text-3xl text-center m-10">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">
<div class="flex flex-col justify-center items-center px-2">
<h1 class="font-bold text-3xl text-center m-10">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">Password</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>Show Password</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">Forgot Password?</button>
</div>
</form>
<form v-else @submit.prevent="submitCreateForm" class="flex flex-col">
<h3 class="text-center text-default text-lg mb-5">Creating account for <br><b>{{ form.email }}</b></h3>
<label class="font-bold">New Password</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">Confirm New Password</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>Show Password</span>
</div>
<div class="w-full flex flex-wrap">
<input :disabled="disableButtons" type="submit" value="Create Account" 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">Not you? Go back</button>
</div>
</form>
<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>
</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>
@ -46,7 +46,7 @@ import { createUserWithEmailAndPassword, signInWithEmailAndPassword, sendPasswor
import { doc, setDoc } from "firebase/firestore";
const { auth, db } = inject('firebase')
const { auth, db, userAllPersons } = inject('firebase')
const toast = useToast()
@ -55,125 +55,123 @@ const creatingAccount = ref(false)
const disableButtons = ref(false)
const form = ref({
email: '',
password: '',
newPassword: '',
confirmNewPassword: ''
email: '',
password: '',
newPassword: '',
confirmNewPassword: ''
})
const submitLoginForm = () => {
disableButtons.value = true
disableButtons.value = true
signInWithEmailAndPassword(auth.value, form.value.email, form.value.password)
.then(() => disableButtons.value = false)
.catch(async (error) => {
const errorCode = error.code;
const errorMessage = error.message;
signInWithEmailAndPassword(auth.value, form.value.email, form.value.password)
.then(() => disableButtons.value = false)
.catch(async (error) => {
if (error.code === 'auth/user-not-found') {
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() }
})
fetch('https://api.xeovalyte.com/checkrelatiecode', {
method: 'POST',
headers: {
Authorization: 'Basic WGVvdmFseXRlOmtNKjhuRXMzNTchalJlXm1KYnZrRSFOIw==',
'content-type': 'application/json'
},
body: JSON.stringify({ email: form.value.email, relatiecode: form.value.password.toUpperCase() })
}).then(response => response.json())
.then(response => {
disableButtons.value = false
if (response.code === 'incorrect') return toast.error('Email, wachtwoord of relatiecode onjuist')
else if (response.code === 'correct') return creatingAccount.value = true
})
.catch(err => {
disableButtons.value = false
console.log(err)
if (err.value) {
console.log(err.value)
disableButtons.value = false
return toast.error('Error tijdens het controleren van relatiecode')
}
toast.error('Error met het controleren van relatiecode')
});
} else if (error.code === 'auth/wrong-password') {
toast.error('Verkeerde wachtwoord')
} else {
toast.error('Error met inloggen')
}
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
disableButtons.value = false
console.log(error.message)
});
} 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');
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
disableButtons.value = true
createUserWithEmailAndPassword(auth.value, form.value.email, form.value.newPassword)
.then((userCredential) => {
fetch('https://api.xeovalyte.com/getrelatiecodes', {
method: 'POST',
headers: {
Authorization: 'Basic WGVvdmFseXRlOmtNKjhuRXMzNTchalJlXm1KYnZrRSFOIw==',
'content-type': 'application/json'
},
body: JSON.stringify({ email: form.value.email })
}).then(response => response.json())
.then(response => {
disableButtons.value = false
if (response.code === 'error') return toast.error('Error tijdens maken van account')
else if (response.code === 'success') {
setDoc(doc(db, "users", userCredential.user.uid), {
email: form.value.email,
relatiecodes: [form.value.password.toUpperCase()],
allRelatiecodes: response.relatiecodes,
});
createUserWithEmailAndPassword(auth.value, form.value.email, form.value.newPassword)
.then(async (userCredential) => {
const idToken = await auth.value.currentUser.getIdToken(true)
const { error, data } = await useFetch('/api/getrelatiecodes', {
method: 'post',
body: { email: form.value.email, token: idToken }
})
if (response.relatiecodes.length > 1) {
return navigateTo('/settings/config/managerelatiecodes')
}
}
})
.catch(err => {
disableButtons.value = false
console.log(err)
if (error.value) {
console.log(error.value)
disableButtons.value = false
return toast.error('Error tijdens het controleren van relatiecode')
}
toast.error('Error met het controleren van relatiecode')
});
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log(error)
toast.error('Error tijdens het maken van account')
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,
});
data.value.persons.forEach(person => {
if (person.relatiecode === form.value.password.toUpperCase()) {
person.checked = true
} else {
person.checked = false
}
})
userAllPersons.value = 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(auth.value, form.value.email)
.then(() => {
toast.info('Wachtwoord vergeten email verstuurd!')
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
sendPasswordResetEmail(auth.value, form.value.email)
.then(() => {
toast.info('Wachtwoord vergeten email verstuurd!')
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log(error)
console.log(error)
toast.error('Error tijdens het versturen van het wachtwoord vergeten email')
});
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: ''
}
creatingAccount.value = false
form.value = {
email: form.value.email,
password: form.value.password,
newPassword: '',
confirmNewPassword: ''
}
}
</script>
</script>

View File

@ -7,17 +7,17 @@
</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>News</span>
<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>Calendar</span>
<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="userData.wedstrijdteam" to="/wedstrijd" class="flex flex-col items-center hover:cursor-pointer drop-shadow" :class="route.path.startsWith('/wedstrijd') ? 'text-primary' : ''">
<NuxtLink v-if="userPersons[0] && 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>
@ -29,7 +29,7 @@
import { Device } from '@capacitor/device';
const route = useRoute()
const { userData } = inject('firebase')
const { userData, userPersons } = inject('firebase')
const platform = ref(null)

View File

@ -5,8 +5,13 @@ export default defineNuxtConfig({
'@nuxtjs/tailwindcss',
'nuxt-icon',
'@vueuse/nuxt',
'@nuxtjs/robots'
'@nuxtjs/robots',
'@nuxtjs/plausible',
],
plausible: {
domain: 'wrbapp.xeovalyte.com',
apiHost: 'https://plausible.xeovalyte.dev',
},
build: {
transpile: ['vue-toastification'],
},
@ -24,5 +29,10 @@ export default defineNuxtConfig({
{ rel: 'icon', href: '/favicon.ico', type: 'image/x-icon' }
]
}
},
runtimeConfig: {
privateKeyId: '',
privateKey: '',
clientId: ''
}
})

10536
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,24 +8,27 @@
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@capacitor/cli": "^4.3.0",
"@nuxtjs/tailwindcss": "^6.1.3",
"@capacitor/cli": "^4.6.3",
"@nuxtjs/plausible": "^0.2.0",
"@nuxtjs/tailwindcss": "^6.3.1",
"@tailwindcss/forms": "^0.5.3",
"@vueuse/core": "^9.6.0",
"@vueuse/nuxt": "^9.6.0",
"nuxt": "^3.0.0-rc.14",
"nuxt-icon": "^0.1.7"
"@vueuse/core": "^9.12.0",
"@vueuse/nuxt": "^9.12.0",
"nuxt": "^3.2.0",
"nuxt-icon": "^0.2.11"
},
"dependencies": {
"@capacitor/core": "^4.3.0",
"@capacitor/device": "^4.0.1",
"@capacitor/ios": "^4.3.0",
"@capacitor/push-notifications": "^4.1.0",
"@capacitor/core": "^4.6.3",
"@capacitor/device": "^4.1.0",
"@capacitor/ios": "^4.6.3",
"@capacitor/push-notifications": "^4.1.2",
"@formkit/nuxt": "^1.0.0-beta.11-c95e605",
"@nuxtjs/robots": "^3.0.0",
"@vueuse/firebase": "^9.2.0",
"@vueuse/shared": "^9.4.0",
"firebase": "^9.14.0",
"@vueuse/components": "^9.12.0",
"@vueuse/firebase": "^9.12.0",
"@vueuse/shared": "^9.12.0",
"firebase": "^9.17.1",
"firebase-admin": "^11.5.0",
"vue-toastification": "^2.0.0-rc.5"
}
}

View File

@ -17,7 +17,7 @@
<script setup>
definePageMeta({
title: 'Calendar'
title: 'Agenda'
})
const { userPersons, calEvents } = inject('firebase')
@ -93,4 +93,4 @@ const getCalendarEvents = async (group) => {
calEvents.value = events
}
</script>
</script>

View File

@ -9,7 +9,7 @@
</NuxtLink>
<div class="divider" />
<NuxtLink to="/calendar" class="item-hover py-2 flex items-center">
<span>Calendar</span>
<span>Agenda</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />

View File

@ -1,18 +1,24 @@
<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">Title</label>
<input v-model="form.title" required="true" class="input mb-5" type="text">
<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">Description</label>
<textarea v-model="form.description" required="true" class="input mb-5" />
<label class="font-bold">Beschrijving</label>
<textarea v-model="form.description" required="true" class="input mb-5" />
<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()" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Cancel</button>
</div>
</form>
</div>
<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()" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Annuleer</button>
</div>
</form>
</div>
</template>
<script setup>
@ -20,69 +26,62 @@ import { addDoc, collection, serverTimestamp, Timestamp } from 'firebase/firesto
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Nieuw Bericht',
key: 'back'
title: 'Nieuw Bericht',
key: 'back'
})
const { news, userData, db, auth } = inject('firebase')
const { news, db, auth } = inject('firebase')
const router = useRouter()
const toast = useToast()
const disableButtons = ref(false)
const form = ref({
title: '',
description: '',
title: '',
description: '',
topic: ''
})
const sendNews = async () => {
disableButtons.value = true
disableButtons.value = true
try {
const idToken = await auth.value.currentUser.getIdToken(true)
console.log(idToken)
try {
const idToken = await auth.value.currentUser.getIdToken(true)
console.log(idToken)
const { error } = await useFetch('/api/sendmessage', {
method: 'post',
body: { title: form.value.title, body: form.value.description, token: idToken, topic: form.value.topic }
})
await fetch('https://api.xeovalyte.com/sendmessage', {
method: 'POST',
headers: {
Authorization: 'Basic WGVvdmFseXRlOmtNKjhuRXMzNTchalJlXm1KYnZrRSFOIw==',
'content-type': 'application/json'
},
body: JSON.stringify({ title: form.value.title, body: form.value.description, token: idToken })
}).then(response => response.json())
.then(async response => {
console.log(response)
await addDoc(collection(db, "news"), {
title: form.value.title,
description: form.value.description,
date: serverTimestamp()
});
if (news.value) {
news.value.unshift({
title: form.value.title,
description: form.value.description,
date: Timestamp.now()
})
}
toast.success('Bericht is verstuurd')
navigateTo('/news')
})
.catch(err => {
console.log(err)
toast.error('Error tijdens het berict sturen')
});
} catch (e) {
console.log(e)
toast.error('Error tijdens het berict sturen')
if (error.value) {
console.log(error.value)
return toast.error('Error tijdens het versturen van het bericht')
}
disableButtons.value = false
await addDoc(collection(db, "news"), {
title: form.value.title,
description: form.value.description,
date: serverTimestamp()
});
if (news.value) {
news.value.unshift({
title: form.value.title,
description: form.value.description,
date: Timestamp.now()
})
}
toast.success('Bericht is verstuurd')
navigateTo('/news')
} catch (e) {
console.log(e)
toast.error('Error tijdens het berict sturen')
}
disableButtons.value = false
}
</script>
</script>

View File

@ -1,169 +1,232 @@
<template>
<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="enabled: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 class="item container flex flex-wrap">
<b class="w-24">{{ lid.relatiecode }}</b> {{ lid.fullName }}
</div>
</div>
<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, runTransaction } from "firebase/firestore";
import { doc, getDocs, collection, writeBatch, updateDoc, setDoc } from "firebase/firestore";
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Ledenlijst',
key: 'back'
title: 'Ledenlijst',
key: 'back'
})
const { db, ledenlijst, users } = inject('firebase')
const toast = useToast()
const modelData = ref(null)
const file = ref(null)
const disableButtons = ref(false)
const searchTerm = ref('')
const newLedenlijst = ref([])
const showModel = ref(false)
onMounted(async () => {
if (!ledenlijst.value.length) {
try {
const querySnapshot = await getDocs(collection(db, "ledenlijst"));
querySnapshot.forEach((doc) => {
ledenlijst.value.push(doc.data())
});
} catch (e) {
console.log(e)
}
ledenlijst.value.sort((a, b) => a.fullName.localeCompare(b.fullName))
if (!ledenlijst.value.length) {
try {
const querySnapshot = await getDocs(collection(db, "ledenlijst"));
querySnapshot.forEach((doc) => {
ledenlijst.value.push(doc.data())
});
} catch (e) {
console.log(e)
}
ledenlijst.value.sort((a, b) => a.fullName.localeCompare(b.fullName))
}
})
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
ledenlijst.value.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 ledenlijst.value.filter(lid => lid.fullName.toLowerCase().includes(searchTerm.value.toLowerCase()))
return ledenlijst.value.filter(lid => lid.fullName.toLowerCase().includes(searchTerm.value.toLowerCase()))
})
const handleFileChanged = (event) => {
const target = event.target;
const target = event.target;
if (target && target.files) {
file.value = target.files[0];
}
if (target && target.files) {
file.value = target.files[0];
}
}
const submitLedenlijst = () => {
disableButtons.value = true
disableButtons.value = true
let reader = new FileReader()
let reader = new FileReader()
reader.onload = function() {
csvToJson(reader.result);
};
reader.onload = function() {
csvToJson(reader.result);
};
reader.onerror = function() {
console.log(reader.error);
};
reader.onerror = function() {
console.log(reader.error);
};
reader.readAsText(file.value)
reader.readAsText(file.value)
}
const csvToJson = (csv) => {
let arr = csv.split('\n');
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);
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);
}
/*
let array = csv.split("\n")
let result = [];
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')
let headers = array[0].split(",")
newLedenlijst.value = []
for (let i = 1; i < array.length - 1; i++) {
let obj = {}
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]
let str = array[i]
let s = ''
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 flag = 0
for (let ch of str) {
if (ch === '"' && flag === 0) {
flag = 1
}
else if (ch === '"' && flag == 1) flag = 0
if (ch === ',' && flag === 0) ch = '|'
if (ch !== '"') s += ch
}
let properties = s.split("|")
for (let j in headers) {
if (properties[j].includes(",")) {
obj[headers[j]] = properties[j]
.split(",").map(item => item.trim())
}
else obj[headers[j]] = properties[j]
}
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')
ledenlijst.value = []
for (let i in result) {
let groups = []
const correctGroups = result[i].Verenigingssporten.replace(/,/g, " -")
console.log(correctGroups)
groups = correctGroups.split(' - ')
if (groups[2] === 'Week') groups[2] = 'Vrijdag'
groups = groups.filter((item) => item !== "Groep")
ledenlijst.value.push({ relatiecode: result[i].Relatiecode, 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()
newLedenlijst.value.push({ relatiecode: result[i].Relatiecode, 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)
try {
const batch = writeBatch(db)
for (let i = 0; i < ledenlijst.value.length; i++) {
const docRef = doc(db, "ledenlijst", ledenlijst.value[i].relatiecode)
batch.set(docRef, ledenlijst.value[i]);
}
newLedenlijst.value.forEach(lid => {
const docRef = doc(db, "ledenlijst", lid.relatiecode)
await batch.commit();
const exists = ledenlijst.value.filter(a => a.relatiecode === lid.relatiecode).length > 1
toast.success('Published ledenlijst')
} catch (e) {
toast.error("Error updating ledenlijst");
console.log(e)
}
if (!exists) {
return batch.set(docRef, lid);
}
batch.update(docRef, { fullName: lid.relatiecode, email: lid.email, groups: lid.groups, diploma: lid.diploma})
})
const deleteLeden = ledenlijst.value.filter(a => newLedenlijst.value.map(x => x.relatiecode).indexOf(a.relatiecode) === -1)
updateUsers()
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)
}
ledenlijst.value = newLedenlijst.value
updateUsers()
}
@ -190,7 +253,7 @@ const updateUsers = async () => {
user.allRelatiecodes = newRelatiecodes
user.relatiecodes.forEach((relatiecode, index) => {
if (!newRelatiecodes.includes(relatiecode)) { user.relatiecodes.splice(index, 1); console.log('removed item')}
if (!newRelatiecodes.includes(relatiecode)) { user.relatiecodes.splice(index, 1); console.log('removed item', relatiecode)}
})
const userRef = doc(db, "users", user.id)

View File

@ -1,13 +1,106 @@
<template>
<div>
Users
<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 } from 'firebase/firestore'
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Manage users',
key: 'back'
title: 'Manage users',
key: 'back'
})
</script>
const { users, db } = inject('firebase')
const toast = useToast()
const searchTerm = ref('')
const disableButtons = ref(false)
const showModel = ref(false)
const modelData = ref({})
onMounted(async () => {
if (!users.value.length) {
try {
const querySnapshot = await getDocs(collection(db, "users"));
querySnapshot.forEach((doc) => {
users.value.push(doc.data())
});
} catch (e) {
console.log(e)
}
}
})
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 users.value.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

@ -1,77 +0,0 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<form @submit.prevent="saveEmail" class="flex flex-col">
<p class="mb-5 text-lg text-red-500 font-bold">Let op! Je verandert alleen het Email van de app dus <u>NIET</u> van de vereniging!</p>
<label class="font-bold">Password</label>
<input v-model="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>Show Password</span>
</div>
<label class="font-bold">New Email</label>
<input v-model="email" required="true" placeholder="user@example.com" class="input mb-5" type="email">
<div class="w-full flex flex-wrap justify-between">
<input :disabled="disableButtons" type="submit" value="Change Email" class="btn w-full sm:w-40 mb-1">
<button @click="router.back()" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Cancel</button>
</div>
</form>
</div>
</template>
<script setup>
import { reauthenticateWithCredential, EmailAuthProvider, updateEmail } from 'firebase/auth'
import { updateDoc, doc } from 'firebase/firestore'
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Change Email',
key: 'back'
})
const { user, auth, db } = inject('firebase')
const toast = useToast()
const router = useRouter()
const password = ref('')
const email = ref('')
const disableButtons = ref(false)
const showPassword = ref(false)
const saveEmail = () => {
disableButtons.value = true
const credential = EmailAuthProvider.credential(
user.value.email,
password.value
)
reauthenticateWithCredential(auth.value.currentUser, credential).then(() => {
updateEmail(auth.value.currentUser, email.value).then(async () => {
await updateDoc(doc(db, "users", user.value.uid), {
email: email.value
})
toast.success('Email is veranderd')
navigateTo('/settings')
disableButtons.value = false
}).catch((error) => {
toast.error('Error tijdens het email veranderen')
console.log(error)
disableButtons.value = false
});
}).catch((error) => {
disableButtons.value = false
if (error.code === 'auth/wrong-password') return toast.error('Wachtwoord is onjuist')
toast.error('Error tijdens het email veranderen')
console.log(error)
});
}
</script>

View File

@ -1,23 +1,23 @@
<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">Old Password</label>
<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">New Password</label>
<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">Confirm New Password</label>
<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>Show Password</span>
<span>Toon Wachtwoord</span>
</div>
<div class="w-full flex flex-wrap justify-between">
<input :disabled="disableButtons" type="submit" value="Change Password" class="btn w-full sm:w-40 mb-1">
<button @click="router.back()" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Cancel</button>
<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>
@ -28,7 +28,7 @@ import { reauthenticateWithCredential, EmailAuthProvider, updatePassword } from
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Change Password',
title: 'Wachtwoord Wijzigen',
key: 'back'
})
@ -86,4 +86,4 @@ const savePassword = () => {
const showPassword = ref(false)
const disableButtons = ref(false)
</script>
</script>

View File

@ -8,8 +8,8 @@
</div>
</div>
<div class="w-full flex flex-wrap">
<button :disabled="buttonsDisabled" @click="save" class="btn w-full sm:w-40 mb-1">Save</button>
<span @click="router.back()" class="hover:underline font-bold w-full text-center sm:w-max sm:ml-auto hover:cursor-pointer">Cancel</span>
<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>
@ -30,7 +30,7 @@ definePageMeta({
key: 'back'
})
const { user, userAllPersons, userPersons, db, getPersons } = inject('firebase')
const { user, userAllPersons, userPersons, db, getPersons, auth } = inject('firebase')
const toast = useToast()
const buttonsDisabled = ref(false)
@ -50,24 +50,24 @@ onMounted(() => {
})
const save = async () => {
buttonsDisabled.value = true
buttonsDisabled.value = true
const newRelatiecodes = []
const newRelatiecodes = []
userAllPersons.value.forEach(person => {
if (person.checked) {
newRelatiecodes.push(person.relatiecode)
}
})
userAllPersons.value.forEach(person => {
if (person.checked) {
newRelatiecodes.push(person.relatiecode)
}
})
await updateDoc(doc(db, "users", user.value.uid), {
relatiecodes: newRelatiecodes
})
await updateDoc(doc(db, "users", user.value.uid), {
relatiecodes: newRelatiecodes
})
getPersons(newRelatiecodes)
getPersons(newRelatiecodes)
buttonsDisabled.value = false
navigateTo('/settings')
buttonsDisabled.value = false
navigateTo('/settings')
}
const updateCheckbox = (person) => {
@ -76,31 +76,30 @@ const updateCheckbox = (person) => {
person.checked = !person.checked
}
const getAllPersons = () => {
const getAllPersons = async () => {
if (userPersons.value.length === 0) return setTimeout(() => getAllPersons(), 50)
fetch('https://api.xeovalyte.com/getrelatiecodes', {
method: 'POST',
headers: {
Authorization: 'Basic WGVvdmFseXRlOmtNKjhuRXMzNTchalJlXm1KYnZrRSFOIw==',
'content-type': 'application/json'
},
body: JSON.stringify({ email: user.value.email })
}).then(response => response.json())
.then(response => {
response.persons.forEach(person => {
if (userPersons.value.map(a => a.relatiecode).includes(person.relatiecode)) {
person.checked = true
} else {
person.checked = false
}
})
userAllPersons.value = response.persons
})
.catch(err => {
console.log(err)
const idToken = await auth.value.currentUser.getIdToken(true)
toast.error('Error tijdens het ophalen van gegevens')
});
const { data: response, error } = await useFetch('/api/getrelatiecodes', {
method: 'post',
body: { email: user.value.email, token: idToken }
})
if (error.value) {
console.log(error.value)
return toast.error('Error tijdens het krijgen van relateicodes')
}
response.value.persons.forEach(person => {
if (userPersons.value.map(a => a.relatiecode).includes(person.relatiecode)) {
person.checked = true
} else {
person.checked = false
}
})
userAllPersons.value = response.value.persons
}
</script>
</script>

View File

@ -1,90 +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>{{ user.email }}</b>
</div>
<div class="divider" />
<div class="item">
Personen: <b>{{ userPersons.map(a => a.fullName).join(', ')}}</b>
</div>
<div class="divider" />
<div class="item">
Groups: <b>{{ groups.join(', ') }}</b>
</div>
<div v-if="userPersons.map(a => a.diploma).filter(n => n !== '').join('')" class="divider" />
<div v-if="userPersons.map(a => a.diploma).filter(n => n !== '').join('')" class="item">
Diploma: <b>{{ userPersons.map(a => a.diploma).filter(n => n !== '').join(', ')}}</b>
</div>
<div class="divider" />
<NuxtLink to="/settings/privacypolicy" class="item-hover py-2 rounded-t flex items-center">
<span>Privacy Policy</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
</div>
<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>{{ user.email }}</b>
</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>Change Password</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
<div class="divider" />
<NuxtLink to="/settings/config/changeemail" class="item-hover py-2 flex items-center">
<span>Change Email</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">
Logout
</div>
</div>
<div class="divider" />
<div class="item">
Personen: <b>{{ userPersons.map(a => a.fullName).join(', ')}}</b>
</div>
<div v-if="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>Manage users</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 class="divider" />
<div class="item">
Groepen: <b>{{ groups.join(', ') }}</b>
</div>
<div>
<h2 class="text-center font-bold">Gemaakt door <u><a href="https://xeovalyte.com/">Timo Boomers</a></u></h2>
<div v-if="userPersons.map(a => a.diploma).filter(n => n !== '').join('')" class="divider" />
<div v-if="userPersons.map(a => a.diploma).filter(n => n !== '').join('')" class="item">
Diploma: <b>{{ 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="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'
title: 'Settings'
})
const { auth, userData, userPersons, user } = inject('firebase')
const groups = computed(() => {
return [...new Set(userPersons.value.map(a => a.groups.join()).join().split(','))]
return [...new Set(userPersons.value.map(a => a.groups.join()).join().split(','))]
})
const logout = () => {
signOut(auth.value)
.catch((error) => {
console.log(error)
})
signOut(auth.value)
.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>{{ registrationToken }}</b>
</div>
<div class="divider" />
<div class="item break-words ">
User ID: <b>{{ userData.id }}</b>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
title: 'Meer Informatie',
key: 'back'
})
const { registrationToken, userData } = inject('firebase')
</script>

View File

@ -1,23 +1,23 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md text-default text-sm">
<h2 class="text-xl font-bold">
Privacy
</h2>
<p>
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.
</p>
<h2 class="text-xl font-bold">
AVG
</h2>
<p>
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.
</p>
</div>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md text-default text-sm">
<h2 class="text-xl font-bold">
Privacy
</h2>
<p>
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.
</p>
<h2 class="text-xl font-bold">
AVG
</h2>
<p>
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.
</p>
</div>
</template>
<script setup>
definePageMeta({
title: 'Privacy Policy',
key: 'back'
title: 'Privacybeleid',
key: 'back'
})
</script>

View File

@ -1,123 +1,154 @@
<template>
<div class="flex flex-col gap-5 mx-auto p-2 w-full max-w-md">
<form v-if="!newEvent" @submit.prevent="saveEmail" class="flex flex-col">
<label class="font-bold">Naam Wedstrijd</label>
<input v-model="contest.name" required="true" class="input mb-5 " type="text">
<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 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">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 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">Onderdelen</label>
<button @click="newEvent = true" class="item-hover border-dashed border-2 container text-center font-bold border-neutral-500 mb-3">Onderdeel Toevoegen</button>
<div class="flex flex-col gap-y-5 mb-5">
<div v-for="(event, index) in contest.events" :key="index" class="container p-2 flex flex-col gap-y-3">
<h2 class="text-center text-primary font-bold text-lg">{{ event.type }}</h2>
<table class="table-fixed text-left odd:bg-blue-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 v-for="competitor in event.competitors" class="even:dark:bg-neutral-700 even:bg-neutral-300">
<td>{{ competitors.find(x => x.relatiecode === competitor.relatiecode ).name }}</td>
<td>{{ competitor.time.minute}}:{{ competitor.time.seconds }}:{{ competitor.time.milliseconds }}</td>
<td>{{ competitor.dsq }}</td>
</tr>
</tbody>
</table>
</div>
</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" />
<div class="w-full flex flex-wrap justify-between">
<input :disabled="disableButtons" type="submit" value="Wedstrijd Toevoegen" class="btn w-full sm:w-48 mb-1">
<button @click="resetContest" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Reset</button>
</div>
</form>
<form v-else @submit.prevent="addEvent" class="flex flex-col">
<label class="font-bold">Onderdeel</label>
<select v-model="tempEvent.type" class="input mb-5">
<option value="200m-Obstacle-Swim">200m Obstacle Swim (Hindernis)</option>
<option value="50m-Manikin-Carry">50m Manikin Carry (Popvervoeren)</option>
<option value="100m-Rescue-Medley">100m Rescue Medley (Reddingswissel)</option>
<option value="100m-Manikin-Carry-with-Fins">100m Manikin Carry with Fins (Popvervoeren met finnen)</option>
<option value="100m-Manikin-Tow-with-Fins">100m Manikin Tow with Fins (Lifesaver)</option>
<option value="200m-Super-Lifesaver">200m Super Lifesaver</option>
<option value="Line-Throw">Line Throw</option>
<option value="4x25m-Manikin-Relay">4x25m Manikin Relay (Popvervoeren)</option>
<option value="4x50m-Obstacle-Relay">4x50m Obstacle Relay (Hindernis)</option>
<option value="4x50m-Medley-Relay">4x50m Medley Relay (Torpedoboei)</option>
</select>
<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">Deelnemers</label>
<button @click="tempEvent.competitors.unshift({ relatiecode: '', dsq: false, info: '', time: { minute: null, seconds: null, milliseconds: null }})" type="button" class="item-hover border-dashed border-2 container text-center font-bold border-neutral-500 mb-3">Deelnemer Toevoegen</button>
<div class="flex flex-col gap-y-3 mb-5">
<div v-for="(competitor, index) in tempEvent.competitors" :key="index">
<div class="container flex flex-col p-4">
<label class="font-bold">Deelnemer</label>
<select v-model="competitor.relatiecode" class="input dark:bg-neutral-700 bg-neutral-300 mb-5">
<option v-for="user in competitors" :value="user.relatiecode">{{ user.name}} ({{ user.relatiecode }})</option>
</select>
<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">Tijd</label>
<div class="flex dark:bg-neutral-700 bg-neutral-300 gap-x-2 w-min rounded mb-2">
<select v-model="competitor.time.minute" class="input pl-3 pr-8 bg-opacity-0 shadow-none ">
<option value="null" disabled selected>mm</option>
<option v-for="n in 60" :value="n < 11 ? `0${n-1}` : `${n-1}`">{{ n < 11 ? `0${n-1}` : `${n-1}` }}</option>
</select>
<span class="my-auto text-xl">:</span>
<select v-model="competitor.time.seconds" class="input pl-2 pr-8 bg-opacity-0 shadow-none ">
<option value="null" disabled selected>ss</option>
<option v-for="n in 60" :value="n < 11 ? `0${n-1}` : `${n-1}`">{{ n < 11 ? `0${n-1}` : `${n-1}`}}</option>
</select>
<span class="my-auto text-xl">:</span>
<select v-model="competitor.time.milliseconds" class="input pl-2 pr-8 bg-opacity-0 shadow-none ">
<option value="null" disabled selected>ms</option>
<option v-for="n in 100" :value="n < 11 ? `0${n-1}` : `${n-1}`">{{ n < 11 ? `0${n-1}` : `${n-1}` }}</option>
</select>
</div>
<div class="flex items-center mb-5">
<input type="checkbox" v-model="competitor.dsq" class="mr-1 checkbox">
<span>Diskwalificatie</span>
</div>
<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">Info (Optioneel)</label>
<input v-model="competitor.info" type="text" placeholder="Bijv. Een diskwalificatie" class="input dark:bg-neutral-700 bg-neutral-300" />
</div>
</div>
</div>
<div class="w-full flex flex-wrap justify-between">
<input :disabled="disableButtons" type="submit" value="Onderdeel Toevoegen" class="btn w-full sm:w-48 mb-1">
<button @click="backEvent" class="hover:underline font-bold w-full sm:w-max sm:ml-auto">Cancel</button>
</div>
</form>
</div>
<label class="font-bold">Onderdelen</label>
<div class="flex flex-col gap-y-3">
<div v-if="competitors" 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">{{ 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="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 } from "firebase/firestore"
import { getDocs, collection, writeBatch, doc } from "firebase/firestore"
import { useToast } from 'vue-toastification'
definePageMeta({
title: 'Wedstrijd Toevoegen'
title: 'Wedstrijd Toevoegen',
key: 'back'
})
const { userData, db, competitors } = inject('firebase')
const toast = useToast()
const { db, competitors } = inject('firebase')
const showModel = ref(false)
const disableButtons = ref(false)
const router = useRouter()
const newEvent = ref(false)
const modelData = ref({
relatiecode: '',
time: {
minutes: null,
seconds: null,
milliseconds: null,
},
dsq: false,
info: '',
})
const contest = ref({
name: '',
date: null,
events: [],
})
const tempEvent = ref({
location: '',
type: '',
competitors: []
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 getCompetitors = async () => {
@ -128,18 +159,82 @@ const getCompetitors = async () => {
})
}
const addEvent = () => {
newEvent.value = false
contest.value.events.unshift(tempEvent.value)
tempEvent.value = { type: '', 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 backEvent = () => {
newEvent.value = false
tempEvent.value = { type: '', competitors: []}
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: contest.value.date.toString(), 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(() => {
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,296 @@
<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="!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="!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="!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="!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="!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="!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="!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="!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="userData.wedstrijdAdmin" :disabled="disableButtons" type="submit" class="btn" value="Bewerken" />
</form>
</div>
<div v-if="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">
<ul class="space-y-2 text-default">
<li v-for="competitor in 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="filteredTimings.filter(a => a.event === event.id).length > 0" class="">
<span class="hidden md:inline-block mr-1">
{{ filteredTimings.filter(a => a.event === event.id)[0].contest.date.toLocaleDateString('nl-NL') }} |
{{ filteredTimings.filter(a => a.event === event.id)[0].contest.location }} |
{{ filteredTimings.filter(a => a.event === event.id)[0].contest.type }} |
</span>
<span>
{{ filteredTimings.filter(a => a.event === event.id)[0].time.minutes }}:{{ filteredTimings.filter(a => a.event === event.id)[0].time.seconds }}:{{ filteredTimings.filter(a => a.event === event.id)[0].time.milliseconds }}</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 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">{{ competitors.filter(a => a.relatiecode === time.relatiecode)[0].name.split(', ')[1] + ' ' + 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 { db, userData, competitors } = inject('firebase')
const toast = useToast()
const timings = ref([])
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 filteredTimings = computed(() => {
timings.value = timings.value.sort((a, b) => a.time.combined.localeCompare(b.time.combined))
timings.value.forEach((time, index) => {
if (time.dsq === true) {
timings.value.push(timings.value.splice(index, 1)[0])
}
})
return timings.value.filter(a => competitors.value.filter(b => b.checked === true).map(x => x.relatiecode).includes(a.relatiecode))
})
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: [],
},
})
const getCompetitors = async () => {
if (competitors.value[0]) return
const querySnapshot = await getDocs(collection(db, "competitors"))
querySnapshot.forEach((doc) => {
const data = doc.data()
data.checked = true
competitors.value.push(data)
})
}
onMounted(async () => {
const citiesRef = collection(db, "timings");
const q = query(citiesRef);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
const data = doc.data()
data.id = doc.id
data.contest.date = data.contest.date.toDate()
timings.value.push(data)
});
timings.value = timings.value.sort((a, b) => a.time.combined.localeCompare(b.time.combined))
getCompetitors()
})
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

@ -1,17 +1,17 @@
<template>
<div class="flex flex-col justify-center items-center px-2 overflow-hidden">
<div class="container w-full max-w-md">
<NuxtLink to="/news" class="rounded-t item-hover py-2 flex items-center">
<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="/calendar" class="item-hover py-2 flex items-center">
<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 class="divider" />
<NuxtLink to="/wedstrijd/addcontest" class="rounded-b item-hover py-2 flex items-center">
<div v-if="userData.wedstrijdAdmin" class="divider" />
<NuxtLink v-if="userData.wedstrijdAdmin" to="/wedstrijd/addcontest" class="rounded-b item-hover py-2 flex items-center">
<span>Wedstrijd Toevoegen</span>
<Icon class="ml-auto" size="2em" name="ion:arrow-forward"/>
</NuxtLink>
@ -21,6 +21,8 @@
<script setup>
definePageMeta({
title: 'Wedstrijd'
title: 'Wedstrijd'
})
const { userData } = inject('firebase')
</script>

View File

@ -0,0 +1,215 @@
<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="!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="!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="!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="!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="!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="!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="!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="!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="userData.wedstrijdAdmin" :disabled="disableButtons" type="submit" class="btn" value="Bewerken" />
</form>
</div>
<div v-if="timings[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="timings.filter(a => a.event === event.id).length > 0" class="ml-auto">{{ filteredTimings.filter(a => a.event === event.id)[0].time.minutes }}:{{ filteredTimings.filter(a => a.event === event.id)[0].time.seconds }}:{{ 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 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 { db, userData } = inject('firebase')
const toast = useToast()
const timings = ref([])
const showModel = ref(false)
const disableButtons = ref(false)
const filteredTimings = computed(() => {
timings.value = timings.value.sort((a, b) => a.time.combined.localeCompare(b.time.combined))
timings.value.forEach((time, index) => {
if (time.dsq === true) {
timings.value.push(timings.value.splice(index, 1)[0])
}
})
return timings.value
})
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 () => {
const citiesRef = collection(db, "timings");
const q = query(citiesRef, where("relatiecode", "in", userData.value.relatiecodes ));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
const data = doc.data()
data.id = doc.id
data.contest.date = data.contest.date.toDate()
timings.value.push(data)
});
timings.value = timings.value.sort((a, b) => a.time.combined.localeCompare(b.time.combined))
})
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

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

View File

@ -0,0 +1,26 @@
import { db } from '../utils/firebase'
export default defineEventHandler(async event => {
const { relatiecode, email } = await readBody(event);
if (!relatiecode) throw createError({ statusCode: 400, statusMessage: 'no-relatiecode'})
if (!email) throw createError({ statusCode: 400, statusMessage: 'no-email'})
try {
const docRef = db.collection('ledenlijst').doc(relatiecode);
const doc = await docRef.get();
if (!doc.exists) throw createError({ statusCode: 400, statusMessage: 'incorrect'})
const data = doc.data()
if (data.email[0] === email || data.email[1] === email) {
return { code: 'correct' }
} else {
throw createError({ statusCode: 400, statusMessage: 'incorrect'})
}
} catch (e) {
throw createError({ statusCode: 500, statusMessage: e.message })
}
})

View File

@ -0,0 +1,39 @@
import { db, auth } from '../utils/firebase'
export default defineEventHandler(async event => {
const { email, token } = await readBody(event);
if (!email) throw createError({ statusCode: 400, statusMessage: 'no-email'})
if (!token) throw createError({ statusCode: 400, statusMessage: 'no-token'})
try {
await auth.verifyIdToken(token)
} catch (e) {
console.log(e);
throw createError({ statusCode: 500, statusMessage: 'error-verify-id'})
}
try {
const ledenlijstRef = db.collection('ledenlijst')
const snapshot = await ledenlijstRef.where("email", "array-contains", email).get()
if (snapshot.empty) {
throw createError({ statusCode: 400, statusMessage: 'no-relatiecode'})
}
let relatiecodes = [];
let persons = [];
snapshot.forEach(doc => {
relatiecodes.push(doc.id)
const data = doc.data()
persons.push({ fullName: data.fullName, relatiecode: doc.id })
});
return { code: 'success', relatiecodes: relatiecodes, persons: persons }
} catch (e) {
throw createError({ statusCode: 500, statusMessage: e.message })
}
})

View File

@ -0,0 +1,63 @@
import { db, auth, messaging } from '../utils/firebase'
export default defineEventHandler(async event => {
const { token, body, title, topic } = await readBody(event);
if (!token) throw createError({ statusCode: 400, statusMessage: 'no-token'})
if (!body) throw createError({ statusCode: 400, statusMessage: 'no-body'})
if (!title) throw createError({ statusCode: 400, statusMessage: 'no-title'})
if (!topic) throw createError({ statusCode: 400, statusMessage: 'no-topic'})
let decodedToken = null;
try {
decodedToken = await auth.verifyIdToken(token)
} catch (e) {
console.log(e)
throw createError({ statusCode: 500, statusMessage: 'error-verify-id'})
}
if (!decodedToken) throw createError({ statusCode: 500, statusMessage: 'error-verify-id-test'})
try {
const uid = decodedToken.uid;
const docRef = db.collection('users').doc(uid);
const doc = await docRef.get();
if (!doc.exists) throw createError({ statusCode: 500, statusMessage: 'doc-not-found'})
const data = doc.data()
if (!data.sendNews) throw createError({ statusCode: 500, statusMessage: 'no-permissions'})
const message = {
notification: {
title: title,
body: body,
},
webpush: {
notification: {
icon: '/ios/256.png',
}
},
topic: topic,
apns: {
payload: {
aps: {
sound: 'default'
}
}
}
};
const response = await messaging.send(message)
console.log('Successfully sent message:', response);
return { code: 'success', response: response }
} catch (e) {
throw createError({ statusCode: 500, statusMessage: e.message })
}
})

View File

@ -0,0 +1,19 @@
import { messaging } from '../utils/firebase'
export default defineEventHandler(async event => {
const { topic, registrationToken } = await readBody(event);
if (!topic) throw createError({ statusCode: 400, statusMessage: 'no-topic'})
if (!registrationToken) throw createError({ statusCode: 400, statusMessage: 'no-registrationtoken'})
try {
await messaging.subscribeToTopic([registrationToken], topic)
return { code: 'success'}
} catch (e) {
console.log(e)
throw createError({ statusCode: 500, statusMessage: e.message })
}
})

View File

@ -0,0 +1,26 @@
import { initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { getMessaging } from 'firebase-admin/messaging';
import { getAuth } from 'firebase-admin/auth'
const config = useRuntimeConfig()
export const app = initializeApp({
credential: cert({
"project_id": "wrbapp",
"private_key_id": config.privateKeyId,
"private_key": config.privateKey,
"client_email": "firebase-adminsdk-iqgyy@wrbapp.iam.gserviceaccount.com",
"client_id": config.clientId,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-iqgyy%40wrbapp.iam.gserviceaccount.com"
})
})
export const firestore = getFirestore()
export const db = getFirestore();
export const messaging = getMessaging();
export const auth = getAuth()