Skip to content

Commit

Permalink
feat: attendee (C)R(UD) - for admin
Browse files Browse the repository at this point in the history
  • Loading branch information
jiyuujin committed Jul 5, 2024
1 parent e7fc7ca commit e2f1863
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 14 deletions.
158 changes: 158 additions & 0 deletions apps/web/app/components/admin/AttendeeItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useSupabase } from '~/composables/useSupabase'
import { useSupabaseStorage } from '~/composables/useSupabaseStorage'
import type { FormAttendee } from '~/types/supabase'
interface AddAttendeeProps {
attendee?: FormAttendee
}
const emit = defineEmits<{ close: [] }>()
const props = defineProps<AddAttendeeProps>()
const { upsertAttendee, uploadAvatar } = useSupabase()
const { getFullAvatarUrl } = useSupabaseStorage()
const newAttendee = ref({
...props.attendee?.id && { id: props.attendee?.id },
user_id: props.attendee?.user_id ?? '',
email: props.attendee?.email ?? '',
avatar_url: props.attendee?.avatar_url ?? '',
provider: props.attendee?.provider ?? '',
display_name: props.attendee?.display_name ?? '',
role: props.attendee?.role ?? '',
receipt_id: props.attendee?.receipt_id ?? '',
activated_at: props.attendee?.activated_at ?? null,
})
const checkFiles = async (files: File[]) => {
if (files.length === 0) return
const file = files[0]
// const filename = file.name
const fileExt = file.name.split('.').pop()
const filePath = `/${Math.random()}.${fileExt}`
uploadAvatar(filePath, file)
newAttendee.value.avatar_url = getFullAvatarUrl(filePath)
}
const updateDisplayName = (e: any) => {
newAttendee.value.display_name = e.target.value
}
const onSubmit = () => {
upsertAttendee('attendees', newAttendee.value)
}
</script>

<template>
<div class="container">
<VFTitle class="title">Sponsor</VFTitle>
<div class="form">
<form @submit="onSubmit">
<VFInputField
id="user_id"
v-model="newAttendee.user_id"
name="user_id"
label="User ID"
disabled
/>
<VFInputField
id="name"
v-model="newAttendee.email"
name="name"
label="Name"
disabled
/>
<VFDragDropArea file-name="profiledata" file-accept="image/*" @check-files="checkFiles">
<div class="upload">
<img
v-if="newAttendee.avatar_url"
alt=""
:src="newAttendee.avatar_url"
height="60"
decoding="async"
/>
<p>Drag & drop a file</p>
<p>または</p>
<p>Select a file</p>
</div>
</VFDragDropArea>
<VFInputField
id="provider"
v-model="newAttendee.provider"
name="provider"
label="Provider (GitHub or Google)"
disabled
/>
<VFInputField
id="display_name"
v-model="newAttendee.display_name"
name="display_name"
label="Display Name"
@input="updateDisplayName"
/>
<VFInputField
id="role"
v-model="newAttendee.role"
name="role"
label="Attendee Role"
disabled
/>
<VFInputField
id="receipt_id"
v-model="newAttendee.receipt_id"
name="receipt_id"
label="Receipt ID"
disabled
/>
<VFInputField
id="activated_at"
v-model="newAttendee.activated_at"
name="activated_at"
label="Activated DateTime"
disabled
/>
<div class="form-button">
<VFSubmitButton>Save</VFSubmitButton>
<VFLinkButton
is="button"
class="action"
background-color="white"
color="vue-blue"
@click="emit('close')"
>
Close
</VFLinkButton>
</div>
</form>
</div>
</div>
</template>

<style scoped>
.container {
height: 600px;
overflow-y: scroll;
}
.form {
padding: 40px 20px;
}
form {
display: grid;
gap: 40px;
width: 100%;
}
.action {
--height-button: 66px;
margin-top: 40px;
height: var(--height-button);
}
@media (--tablet) {
.action {
--height-button: 49px;
}
}
</style>
99 changes: 99 additions & 0 deletions apps/web/app/components/admin/AttendeeList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import type { Attendee } from '@vuejs-jp/model'
import { ref } from 'vue'
interface AttendeeListProps {
attendees: Attendee[]
}
const emit = defineEmits<{ edit: [id: string] }>()
const props = defineProps<AttendeeListProps>()
const showDialog = ref(false)
const attendeeId = ref('')
const handleDialog = (id?: string) => {
showDialog.value = !showDialog.value
attendeeId.value = id ?? ''
}
</script>

<template>
<table id="attendees">
<tr>
<th>user_id</th>
<th>email</th>
<th>avatar_url</th>
<th>provider</th>
<th>display_name</th>
<th>role</th>
<th>receipt_id</th>
<th>activated_at</th>
<th style="min-width: 80px">action</th>
</tr>
<tr v-for="attendee in attendees" :key="attendee.id">
<td>{{ attendee.user_id }}</td>
<td>{{ attendee.email }}</td>
<td>
<img
v-if="attendee.avatar_url"
alt=""
:src="attendee.avatar_url"
width="60"
height="60"
decoding="async"
/>
<p v-if="!attendee.avatar_url">
No image
</p>
</td>
<td>{{ attendee.provider }}</td>
<td>{{ attendee.display_name }}</td>
<td>{{ attendee.role }}</td>
<td>{{ attendee.receipt_id }}</td>
<td>{{ attendee.activated_at }}</td>
<td>
<VFLinkButton
is="button"
class="action"
background-color="white"
color="vue-blue"
@click="() => handleDialog(attendee?.id)"
>
Edit
</VFLinkButton>
</td>
</tr>
</table>
<VFDialog v-if="showDialog">
<AdminAttendeeItem :attendee="attendees.filter((s) => s.id === attendeeId)[0]" @close="handleDialog" />
</VFDialog>
</template>

<style scoped>
#attendees {
border-collapse: collapse;
width: 100%;
}
#attendees td,
#attendees th {
border: 1px solid #ddd;
padding: 8px;
}
#attendees tr:nth-child(even){
background-color: #f2f2f2;
}
#attendees tr:hover {
background-color: #ddd;
}
#attendees th {
padding: 12px auto;
text-align: left;
background-color: var(--color-vue-green200);
color: #fff;
}
</style>
35 changes: 27 additions & 8 deletions apps/web/app/components/admin/Page.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
<script setup lang="ts">
import { useAsyncData } from '#imports'
import type { AdminPage } from '@vuejs-jp/model'
import { type AdminPage, type Role, selectableRoleList } from '@vuejs-jp/model'
import { match } from 'ts-pattern'
import { ref } from 'vue'
import { useCsv } from '@vuejs-jp/composable'
import { useSupabase } from '~/composables/useSupabase'
import { useSupabaseCsv } from '~/composables/useSupabaseCsv'
import type AdminUserList from './AdminUserList.vue'
interface ListProps {
page: AdminPage
}
const { fetchData } = useSupabase()
const { exportSpeaker, exportSponsor, exportStaff } = useSupabaseCsv()
const selectedRole = ref<Role>('attendee')
const { fetchData, fetchAttendeeData } = useSupabase()
const { exportSpeaker, exportSponsor, exportAttendee, exportStaff } = useSupabaseCsv()
const { write } = useCsv()
const { data: speakers } = await useAsyncData('speakers', async () => {
return await fetchData('speakers')
})
const { data: sponsors } = await useAsyncData('sponsors', async () => {
return await fetchData('sponsors')
})
const { data: attendees } = await useAsyncData('attendees', async () => {
return await fetchAttendeeData('attendees', selectedRole.value)
})
const { data: staffs } = await useAsyncData('staffs', async () => {
return await fetchData('staffs')
})
Expand All @@ -38,7 +42,7 @@ const handleCsv = async () => {
.with('speaker', () => exportSpeaker('speakers'))
.with('sponsor', () => exportSponsor('sponsors'))
.with('adminUser', () => exportStaff('staffs'))
.with('namecard', () => null)
.with('namecard', () => exportAttendee('attendees'))
.exhaustive()
if (!res) return
Expand Down Expand Up @@ -67,13 +71,12 @@ const pageText = props.page.replace(/^[a-z]/g, function (val) {
</VFLinkButton>
<VFLinkButton
is="button"
v-if="page !== 'namecard'"
class="action"
background-color="white"
color="vue-blue"
@click="handleCsv"
>
{{ `Export ${pageText === 'AdminUser' ? 'staff' : pageText}` }}
{{ `Export ${pageText === 'AdminUser' ? 'staff' : (pageText === 'namecard' ? 'attendee' : pageText)}` }}
</VFLinkButton>
<VFLinkButton
v-if="page === 'adminUser'"
Expand All @@ -88,9 +91,19 @@ const pageText = props.page.replace(/^[a-z]/g, function (val) {
</div>
<AdminSpeakerList v-if="page === 'speaker'" :speakers="speakers?.data" />
<AdminSponsorList v-if="page === 'sponsor'" :sponsors="sponsors?.data" :speakers="speakers?.data" />
<div v-if="page === 'namecard'" class="tab-content-attendee">
<VFDropdownField
id="selected_role"
v-model="selectedRole"
name="selected_role"
label="Attendee Role"
:items="selectableRoleList"
/>
<AdminAttendeeList :attendees="attendees?.data" />
</div>
<div v-if="page === 'adminUser'" class="tab-content-admin">
<AdminStaffList :staffs="staffs?.data" />
<AdminUserList :admin-users="adminUsers?.data" />
<AdminAdminUserList :admin-users="adminUsers?.data" />
</div>
<VFDialog v-if="showDialog">
<AdminSpeakerItem v-if="page === 'speaker'" @close="handleDialog" />
Expand Down Expand Up @@ -118,8 +131,14 @@ const pageText = props.page.replace(/^[a-z]/g, function (val) {
.tab-content-header button {
width: 184px;
}
.tab-content-attendee,
.tab-content-admin {
display: grid;
gap: 20px;
}
.tab-content-attendee label {
width: 400px;
display: flex;
align-items: center;
}
</style>
17 changes: 14 additions & 3 deletions apps/web/app/composables/useSupabase.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSupabaseClient } from '#imports'
import { bucket, type Table } from '@vuejs-jp/model'
import { bucket, type Role, type Table } from '@vuejs-jp/model'
import type { Database } from '~/types/generated/supabase'
import type { FormSpeaker, FormSponsor, FormStaff } from '~/types/supabase'
import type { FormSpeaker, FormSponsor, FormAttendee, FormStaff } from '~/types/supabase'

export function useSupabase() {
const client = useSupabaseClient<Database>()
Expand All @@ -10,6 +10,10 @@ export function useSupabase() {
return await client.from(table).select()
}

async function fetchAttendeeData(table: Extract<Table, 'attendees'>, role: Role) {
return await client.from(table).select().eq('role', role)
}

async function upsertSpeaker(table: Extract<Table, 'speakers'>, target: FormSpeaker) {
const targetData = { ...target }

Expand All @@ -24,6 +28,13 @@ export function useSupabase() {
if (error) return
}

async function upsertAttendee(table: Extract<Table, 'attendees'>, target: FormAttendee) {
const targetData = { ...target }

const { error } = await client.from(table).upsert(targetData)
if (error) return
}

async function upsertStaff(table: Extract<Table, 'staffs'>, target: FormStaff) {
const targetData = { ...target }

Expand All @@ -35,5 +46,5 @@ export function useSupabase() {
await client.storage.from(bucket).upload(filePath, file)
}

return { fetchData, upsertSpeaker, upsertSponsor, upsertStaff, uploadAvatar }
return { fetchData, fetchAttendeeData, upsertSpeaker, upsertSponsor, upsertAttendee, upsertStaff, uploadAvatar }
}
Loading

0 comments on commit e2f1863

Please sign in to comment.