-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(admin): events management #7
Merged
Merged
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
ec47dae
wip: events table
Ethan-Chew 97da2ad
feat: create and delete event
Ethan-Chew d882b2e
feat: event information
Ethan-Chew 7bdd607
fix: attendees import
Ethan-Chew 7b63805
fix: checked-in users
Ethan-Chew c03767e
feat: add nuxtui login
Ethan-Chew 23e3cb1
feat: add image URL validation
Ethan-Chew 856c4f8
fix: verify image url on server
Ethan-Chew 895a062
feat: update event
Ethan-Chew 1895948
fix: restrictTo exco for update
Ethan-Chew 2732319
fix: resolved most comments
Ethan-Chew 9a54153
fix: removed difference check when updating
Ethan-Chew 5bb8c7f
Merge branch 'main' into feat/nuxtui
Ethan-Chew 1cbe61f
fix: readded pnpm-lock
Ethan-Chew 658acda
fix: update name case
Ethan-Chew fa07bab
Revert "fix: update name case"
qin-guan efa799b
fix: lint rules + dep upgrade
qin-guan 6caf032
chore: remote stash
qin-guan 71172b1
fix: reload on scope change
qin-guan 7522edb
chore: remote stash
qin-guan 7c3ccf0
feat: schema update
qin-guan ad39696
chore: dep upgrade and some fixes
qin-guan 32fe27f
chore: update
qin-guan 8885f74
feat: remove ts-rest, fix type issues, use new persister
qin-guan 69eb3a1
chore: wip
qin-guan ab0c981
feat: update
qin-guan da1c371
feat: clean up
qin-guan f53d606
feat: add buster
qin-guan 5df3ada
Merge branch 'main' into feat/nuxtui
qin-guan 81857d6
chore: lint
qin-guan 14f3eb7
feat: members page
qin-guan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<script setup lang="ts"> | ||
import type { EventWithAttendees } from '~/shared/types' | ||
|
||
const props = defineProps<{ | ||
event: EventWithAttendees | ||
}>() | ||
</script> | ||
|
||
<template> | ||
<UCard> | ||
<template #header> | ||
<span class="font-semibold text-2xl">Attendees</span> | ||
</template> | ||
<div v-for="attendee in props.event.attendees" v-if="props.event.attendees.length > 0" :key="attendee.id"> | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<div class="mb-3"> | ||
<p class="font-bold text-xl"> | ||
{{ attendee.name }} | ||
</p> | ||
<p><span class="font-semibold">ID: </span>{{ attendee.id }}</p> | ||
<hr class="my-2"> | ||
</div> | ||
</div> | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<div v-if="props.event.attendees.length === 0" class="flex justify-center align-items-center"> | ||
<div class="text-center"> | ||
<UIcon class="text-5xl" name="i-heroicons-face-frown" /> | ||
<p class="font-bold text-xl"> | ||
No attendees yet! | ||
</p> | ||
<p>Upload attendees using a .csv file first!</p> | ||
</div> | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</div> | ||
</UCard> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<script setup lang="ts"> | ||
import dayjs from 'dayjs' | ||
import type { EventWithAttendees } from '~/shared/types' | ||
|
||
const props = defineProps<{ | ||
event: EventWithAttendees | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}>() | ||
</script> | ||
|
||
<template> | ||
<UCard> | ||
<template #header> | ||
<div class="relative"> | ||
<img :src="props.event.badgeImage" class="object-cover w-full max-h-60 object-top rounded-xl"> | ||
<div class="p-10 absolute z-10 inset-0 flex flex-col bg-gray-800/[0.3] space-y-6 backdrop-blur-sm rounded-xl"> | ||
<div class="space-y-3 flex flex-col"> | ||
<span class="font-bold text-3xl md:text-4xl">{{ props.event.name }}</span> | ||
<div> | ||
<div class="flex-inline shrink grow-0 p-1 px-4 border rounded-lg"> | ||
{{ `0 / ${event.attendees.length}` }} | ||
</div> | ||
</div> | ||
</div> | ||
<div class="text-lg flex flex-col"> | ||
<span><span class="font-bold">{{ `${dayjs(parseInt(event.startDateTime) * 1000).format("D MMM YYYY")} | ${dayjs(parseInt(event.startDateTime) * 1000).format("H:mm")}` }}</span> to <span class="font-bold">{{ `${dayjs(parseInt(event.endDateTime) * 1000).format("D MMM YYYY")} | ${dayjs(parseInt(event.endDateTime) * 1000).format("H:mm")}` }}</span></span> | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<span><span class="font-bold">Location:</span> {{ props.event.location }}</span> | ||
</div> | ||
</div> | ||
</div> | ||
</template> | ||
<div class=""> | ||
<span class="text-xl font-bold">Description</span> | ||
<p class="text-md mt-2"> | ||
{{ props.event.description }} | ||
</p> | ||
</div> | ||
</UCard> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
<script setup lang="ts"> | ||
import type { ParseResult } from 'papaparse' | ||
import { parse } from 'papaparse' | ||
import { ref } from 'vue' | ||
import { createId } from '@paralleldrive/cuid2' | ||
import type { EventWithAttendees, User } from '~/shared/types' | ||
import { useQueryClient } from '@tanstack/vue-query' | ||
|
||
const props = defineProps<{ | ||
event: EventWithAttendees | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}>() | ||
const queryClient = useQueryClient() | ||
|
||
let uploadedAttendees: Omit<User, 'firebaseId' | 'id'>[] | ||
const pending = ref(false) | ||
const isUploaded = ref(false) | ||
const error = ref({ | ||
didError: false, | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
msg: '', | ||
}) | ||
const file = ref<File | undefined>(undefined) | ||
|
||
// Check for Type | ||
function isFile(file: File | undefined): file is File { | ||
return file instanceof File | ||
} | ||
|
||
function handleFileUpload(e: Event) { | ||
pending.value = true | ||
file.value = (e.target as HTMLInputElement).files?.[0] | ||
|
||
// Parse the CSV File | ||
if (isFile(file.value)) { | ||
parse(file.value, { | ||
skipEmptyLines: true, | ||
header: true, | ||
complete(results: ParseResult<Omit<User, 'firebaseId' | 'id'>>) { | ||
uploadedAttendees = results.data | ||
|
||
if (JSON.stringify(results.meta.fields?.sort()) !== JSON.stringify(['name', 'email', 'memberType', 'graduationYear'].sort())) { | ||
error.value.didError = true | ||
error.value.msg = 'CSV has missing fields! Please check the file and reupload it.' | ||
return | ||
} | ||
|
||
pending.value = uploadedAttendees === undefined | ||
}, | ||
}) | ||
} | ||
} | ||
|
||
async function uploadToDB() { | ||
qin-guan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Upload to users database | ||
const createdUsers = [] | ||
|
||
for (let i = 0; i < uploadedAttendees.length; i += 30) { | ||
let usersBatch = uploadedAttendees.slice(i, i + 29) | ||
|
||
usersBatch = usersBatch.map(item => ({ | ||
...item, | ||
graduationYear: Number.parseInt((item.graduationYear).toString(), 10), | ||
memberId: createId(), | ||
})) | ||
|
||
try { | ||
const res = await $api('/api/user/bulk', { | ||
method: 'POST', | ||
body: usersBatch, | ||
}) | ||
|
||
createdUsers.push(res) | ||
} | ||
catch (err) { | ||
error.value.didError = true | ||
error.value.msg = `Failed to push chunk ${i} to the users database.` | ||
} | ||
} | ||
// Upload to users_event database | ||
for (let i = 0; i < createdUsers.length; i += 1) { | ||
const chunk = [] | ||
for (let j = 0; j < createdUsers[i].length; j += 1) { | ||
chunk.push({ | ||
userId: createdUsers[i][j].id, | ||
eventId: props.event.id, | ||
admissionKey: createId(), | ||
}) | ||
} | ||
// Upload the chunk to the database | ||
try { | ||
await $api('/api/event/users', { | ||
method: 'POST', | ||
body: chunk, | ||
}) | ||
} | ||
catch (err) { | ||
error.value.didError = true | ||
error.value.msg = `Failed to push chunk ${i} to the users_event database.` | ||
} | ||
} | ||
|
||
pending.value = false | ||
if (!error.value.didError) | ||
isUploaded.value = true | ||
queryClient.invalidateQueries({ queryKey: ['events'] }) | ||
} | ||
</script> | ||
|
||
<template> | ||
<UCard> | ||
<template #header> | ||
<div class="flex flex-col space-y-1"> | ||
<span class="font-semibold text-2xl">Upload Attendees</span> | ||
<span class="text-base">Upload a <i>.csv</i> file containing the <span class="font-bold">Name, Email, Member Type and Graduation Year</span> of the attendees attending this event. Do ensure that this file contains headers for <span class="font-bold">all</span> the columns.</span> | ||
</div> | ||
</template> | ||
|
||
<div class="space-y-2"> | ||
<div class="space-x-5"> | ||
<label class="inline-block p-2 px-4 bg-[#007aff]/[0.4] hover:bg-[#007aff]/[0.7] duration-100 rounded-xl cursor-pointer"> | ||
<input type="file" accept=".csv" class="hidden" @change="handleFileUpload($event)"> | ||
<div class="space-x-2"> | ||
<UIcon name="i-heroicons-arrow-up-tray" /> | ||
<span class="text-md font-semibold">Upload File</span> | ||
</div> | ||
</label> | ||
<span><span class="font-bold">{{ file ? `Uploaded File:` : '' }}</span> {{ file ? file.name : '' }}</span> | ||
</div> | ||
<UButton v-if="file" tonal class="flex-inline max-w-xs" :loading="pending" :disabled="pending" @click="uploadToDB"> | ||
Populate Database | ||
</UButton> | ||
<span v-if="isUploaded">Successfully added users to the database. </span> | ||
<span v-if="error.didError">{{ error.msg }}</span> | ||
</div> | ||
</UCard> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<script setup lang="ts"> | ||
import dayjs from 'dayjs' | ||
import { ref } from 'vue' | ||
import { useMutation, useQueryClient } from '@tanstack/vue-query' | ||
import type { Event } from '~/shared/types' | ||
|
||
const props = defineProps<{ | ||
showPopup: boolean | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}>() | ||
const queryClient = useQueryClient() | ||
|
||
const closePopup = defineEmits(['closePopup']) | ||
const state = reactive({ | ||
name: '', | ||
description: '', | ||
location: '', | ||
badgeImage: '', | ||
startDateTime: '', | ||
endDateTime: '', | ||
}) | ||
const newEventId = ref('') | ||
const inputError = ref('') | ||
|
||
const mutation = useMutation({ | ||
mutationFn: (newEvent: Omit<Event, 'id'>) => $api('/api/event', { | ||
method: 'POST', | ||
body: newEvent, | ||
}), | ||
}) | ||
|
||
async function handleSubmit() { | ||
inputError.value = '' | ||
|
||
const newEvent = { ...state } | ||
newEvent.startDateTime = dayjs(state.startDateTime).toISOString() | ||
newEvent.endDateTime = dayjs(state.endDateTime).toISOString() | ||
|
||
try { | ||
const res = await mutation.mutateAsync(newEvent) | ||
|
||
newEventId.value = res.id | ||
queryClient.invalidateQueries({ queryKey: ['events'] }) | ||
} catch (err) { | ||
if (err instanceof Error && err.message.includes('Invalid Image URL')) { | ||
inputError.value = "Invalid Image URL" | ||
} | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<UModal v-model="props.showPopup"> | ||
<UCard> | ||
<template #header> | ||
<div class="flex flex-row items-start"> | ||
<div> | ||
<h1 class="text-xl font-bold pb-1"> | ||
Create Event | ||
</h1> | ||
<p class="text-md"> | ||
Create a new SSTAA event | ||
</p> | ||
</div> | ||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="ml-auto -my-1" @click="$emit('closePopup')" /> | ||
</div> | ||
</template> | ||
|
||
<UForm :state="state" class="space-y-7" @submit="handleSubmit"> | ||
<div class="space-y-5"> | ||
<UFormGroup label="Event Name" required :error="!state.name && 'You must enter an Event Name!'"> | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<UInput v-model="state.name" placeholder="Amazing Event" color="gray" variant="outline" :disabled="newEventId !== ''" /> | ||
</UFormGroup> | ||
<UFormGroup label="Description" description="Provide a short description describing what this event is about" required :error="!state.description && 'You must enter an Event Description!'"> | ||
<UInput v-model="state.description" placeholder="Description" color="gray" variant="outline" :disabled="newEventId !== ''" /> | ||
</UFormGroup> | ||
<UFormGroup label="Location" description="Where is this event held at" required :error="!state.location && 'You must enter the Event Location!'"> | ||
<UInput v-model="state.location" placeholder="School of Science and Technology, Singapore" color="gray" variant="outline" :disabled="newEventId !== ''" /> | ||
</UFormGroup> | ||
<UFormGroup name="imageurl" label="Banner Image" description="The image shown to everyone viewing this event. Enter a valid Image URL." required :error="!state.badgeImage && 'You must enter a valid Image URL'"> | ||
<UInput v-model="state.badgeImage" name="imageurl" type="url" placeholder="https://example.com" color="gray" variant="outline" :disabled="newEventId !== ''" /> | ||
</UFormGroup> | ||
<UFormGroup label="Start Date and Time" required :error="!state.startDateTime && 'You must enter the Start Date!'"> | ||
<UInput v-model="state.startDateTime" type="datetime-local" color="gray" variant="outline" :disabled="newEventId !== ''" /> | ||
</UFormGroup> | ||
<UFormGroup label="End Date and Time" required :error="!state.endDateTime && 'You must enter the End Date!'"> | ||
<UInput v-model="state.endDateTime" type="datetime-local" color="gray" variant="outline" :disabled="newEventId !== ''" /> | ||
</UFormGroup> | ||
</div> | ||
|
||
<UButton v-if="newEventId === ''" type="submit" color="green" :loading="mutation.isLoading.value"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need .value |
||
Create Event | ||
</UButton> | ||
<p v-if="inputError !== ''"> | ||
Ethan-Chew marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{{ inputError }} | ||
</p> | ||
</UForm> | ||
<div v-if="mutation.isSuccess.value" class="space-y-3 mt-4"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need .value |
||
<p>Created Event with an ID of <b>{{ newEventId }}</b>.</p> | ||
<UButton color="blue" @click="$emit('closePopup')"> | ||
Done | ||
</UButton> | ||
</div> | ||
</UCard> | ||
</UModal> | ||
</div> | ||
</template> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why this is included?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For configuring where the NuxtUI notifications will show
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh, by default it doesn't show correctly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, but shows on the bottom right. Saw it from the documentation that you can configure the rendering location by adding it there (https://ui.nuxt.com/overlays/notification)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Does this affect mobile? Or will it show bottom center on mobile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll return bottom center on mobile