Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions app/pages/admin/training.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<script setup lang="ts">
import { ref } from 'vue'

type Volunteer = {
id: number
name: string
verified: boolean
}

const volunteers = ref<Volunteer[]>([])

const { data } = await useFetch('/api/admin/training')

volunteers.value =
data.value?.map((v: any) => ({
id: v.id,
name: v.name || 'Unknown Volunteer',
verified: v.status === 'APPROVED'
})) || []
const newName = ref('')
const newEmail = ref('')

async function addVolunteer() {

const newVolunteer = await $fetch('/api/admin/training', {
method: 'POST',
body: {
name: newName.value,
email: newEmail.value
}
})

volunteers.value.push({
id: newVolunteer.id,
name: newVolunteer.name,
verified: false
})

newName.value = ''
newEmail.value = ''
}

async function toggle(v: Volunteer) {

v.verified = !v.verified

await $fetch('/api/admin/volunteer', {
method: 'PATCH',
body: {
id: v.id,
verified: v.verified
}
})
}

function removePerson(id: number) {
volunteers.value = volunteers.value.filter(v => v.id !== id)
}
</script>

<template>
<div class="page">
<h1>Volunteer Verification</h1>

<div class="add-form">
<input v-model="newName" placeholder="Volunteer Name" />
<input v-model="newEmail" placeholder="Email" />
<button @click="addVolunteer">Add Volunteer</button>
</div>

<div
class="card"
v-for="v in volunteers"
:key="v.id"
:class="{ verified: v.verified }"
>
<div class="left">
<span>{{ v.name }}</span>
<small v-if="v.verified">Verified</small>
<small v-else>Pending</small>
</div>

<div class="actions">
<input
type="checkbox"
:checked="v.verified"
@change="toggle(v)"
/>

<button @click="removePerson(v.id)">Delete</button>
</div>
</div>
</div>
</template>

<style scoped>
.page {
max-width: 650px;
margin: 120px auto 40px; /* pushes content down */
color: white;
}


.card {
background: #0f1b2d;
padding: 14px 18px;
border-radius: 10px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}

.card.verified {
border-left: 6px solid #4ade80;
}

.left {
display: flex;
flex-direction: column;
}

small {
font-size: 12px;
opacity: 0.8;
}

.actions {
display: flex;
align-items: center;
gap: 12px;
}

button {
background: #ef4444;
border: none;
color: white;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
}

button:hover {
opacity: 0.85;
}

input {
transform: scale(1.4);
}
</style>
34 changes: 15 additions & 19 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions prisma/schema/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,14 @@ model Verification {

@@map("verification")
}

model TrainingCertificate {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be linked to the current volunteer table? Like each volunteer has a list of training certificates, and each training certificate links to a volunteer?

id Int @id @default(autoincrement())
name String
email String
fileUrl String
status String @default("PENDING")
createdAt DateTime @default(now())
}


1 change: 1 addition & 0 deletions prisma/schema/volunteer.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ model Volunteer {
events RSVP[]

emailVerified Boolean @default(false)
zoomVerified Boolean @default(false)
imageURL String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Expand Down
8 changes: 8 additions & 0 deletions server/api/admin/training.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import prisma from "~~/server/utils/prisma";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On each API, ensure that the user is logged in (you can check using the betterauth client, there should be an example in one of the other apis)


export default defineEventHandler(async () => {
return await prisma.trainingCertificate.findMany({
where: { status: "PENDING" }
});
});

14 changes: 14 additions & 0 deletions server/api/admin/training.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import prisma from "~~/server/utils/prisma";

export default defineEventHandler(async (event) => {
const body = await readBody(event);

return await prisma.trainingCertificate.create({
data: {
name: body.name,
email: body.email,
fileUrl: "",
status: "PENDING"
}
});
});
9 changes: 9 additions & 0 deletions server/api/admin/volunteer.delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import prisma from "~~/server/utils/prisma";

export default defineEventHandler(async (event) => {
const body = await readBody(event)

return await prisma.volunteer.delete({
where: { id: body.id }
})
})
11 changes: 11 additions & 0 deletions server/api/admin/volunteer.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import prisma from "~~/server/utils/prisma";

export default defineEventHandler(async () => {
return await prisma.volunteer.findMany({
select: {
id: true,
name: true,
zoomVerified: true
}
});
});
14 changes: 14 additions & 0 deletions server/api/admin/volunteer.patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import prisma from "~~/server/utils/prisma";

export default defineEventHandler(async (event) => {
const body = await readBody(event);

return await prisma.trainingCertificate.update({
where: {
id: body.id
},
data: {
status: body.verified ? "APPROVED" : "PENDING"
}
});
});