Skip to content

Commit

Permalink
Merge branch 'main' into feat/nuxtui
Browse files Browse the repository at this point in the history
  • Loading branch information
Ethan-Chew committed Oct 24, 2023
2 parents 9a54153 + ba81823 commit 5bb8c7f
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 13,156 deletions.
6 changes: 3 additions & 3 deletions components/app/home/membership-card.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { f7Card, f7CardContent, f7CardFooter, f7SkeletonBlock } from 'framework7-vue'
import { f7Card, f7CardContent, f7CardFooter, f7List, f7SkeletonBlock } from 'framework7-vue'
import type { User } from '~/shared/types'
const { data: user, isLoading: userIsLoading } = useUser()
Expand All @@ -15,9 +15,9 @@ const membershipGradient: Record<Exclude<User['memberType'], null>, string> = {

<template>
<div>
<div v-if="userIsLoading" class="h-55">
<f7List v-if="userIsLoading" inset class="h-64">
<f7SkeletonBlock class="rounded-md" effect="fade" height="100%" />
</div>
</f7List>

<f7Card v-else-if="user" class="m-0!">
<f7CardContent class="h-44 rounded-[16px]" valign="top" :class="membershipGradient[user.memberType!]">
Expand Down
10 changes: 6 additions & 4 deletions components/app/home/news.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { f7BlockTitle, f7Button, f7Card, f7CardContent, f7CardFooter, f7CardHeader, f7SkeletonBlock } from 'framework7-vue'
import { f7BlockTitle, f7Button, f7Card, f7CardContent, f7CardFooter, f7CardHeader, f7List, f7SkeletonBlock } from 'framework7-vue'
const { data: news, isLoading: newsIsLoading } = useNewsArticles()
</script>
Expand All @@ -11,9 +11,11 @@ const { data: news, isLoading: newsIsLoading } = useNewsArticles()
</f7BlockTitle>

<div class="space-y-3">
<template v-if="newsIsLoading">
<f7SkeletonBlock v-for="n in 3" :key="n" class="rounded-md" effect="fade" height="10rem" />
</template>
<f7List v-if="newsIsLoading" inset>
<div class="space-y-3">
<f7SkeletonBlock v-for="n in 3" :key="n" class="rounded-md" effect="fade" height="10rem" />
</div>
</f7List>
<template v-else-if="news">
<span v-if="news.length === 0" class="mt-4 opacity-80">
Nothing exciting is happening right now.
Expand Down
2 changes: 1 addition & 1 deletion components/app/home/page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { f7List, f7Page } from 'framework7-vue'
</div>

<div>
<AppHomeNews />
<LazyAppHomeNews />
</div>
</f7List>
</f7Page>
Expand Down
181 changes: 179 additions & 2 deletions components/app/services/event/page.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,186 @@
<script setup lang="ts">
import { f7Page } from 'framework7-vue'
import { f7, f7BlockTitle, f7Button, f7Card, f7Chip, f7Link, f7List, f7ListItem, f7NavRight, f7NavTitle, f7Navbar, f7Page, f7Searchbar, f7SkeletonBlock, f7Subnavbar, f7SwipeoutActions, f7SwipeoutButton } from 'framework7-vue'
import type { VirtualList } from 'framework7/types'
import type { UnwrapRef } from 'vue'
import { useDatabaseList } from 'vuefire'
import { ref as dbRef, remove, set } from 'firebase/database'
const props = defineProps<{
id: string // Passed by f7router in URL param
}>()
const state = ref({
scannerOpened: false,
})
const { $dayjs } = useNuxtApp()
const db = useDatabase()
const { data: checkedInUsersList, pending: checkedInUsersListPending } = useDatabaseList<{ $value: number }>(dbRef(db, props.id))
const { data: event, isLoading: eventIsLoading } = useEvent(props.id)
const attendees = computed(() => {
return event.value?.attendees.map((attendee, index) => ({
...attendee,
index, // Used by f7 virtual list
})) ?? []
})
const checkedInUsers = computed(() => {
const data: Record<string, number> = {} // admissionKey -> timestamp in seconds
for (const item of checkedInUsersList.value)
data[item.id] = item.$value
return data
})
const attendeeVirtualListData = ref<Omit<Partial<VirtualList.VirtualListRenderData>, 'items'> & { items: UnwrapRef<typeof attendees> }>({
items: [],
})
const formattedDateCache = new Map<number, string>()
function formattedDate(date: number) {
if (formattedDateCache.has(date))
return formattedDateCache.get(date)
const data = $dayjs.unix(date).format('hh:mm A')
formattedDateCache.set(date, data)
return data
}
async function toggle(admissionKey: string) {
if (checkedInUsers.value[admissionKey])
await remove(dbRef(db, `${props.id}/${admissionKey}`))
else
await set(dbRef(db, `${props.id}/${admissionKey}`), $dayjs().unix())
}
async function onScan(admissionKey: string) {
const attendee = attendees.value.find(attendee => attendee.admissionKey === admissionKey)
if (!attendee) // Attendee is in master list from API
return
if (checkedInUsers.value[admissionKey]) { // Attendee is already checked in
f7.toast.show({
text: `${attendee.name} already checked in`,
closeTimeout: 3000,
})
return
}
await set(dbRef(db, `${props.id}/${admissionKey}`), $dayjs().unix())
f7.toast.show({
text: `Checked in ${attendee.name}`,
closeTimeout: 10000,
})
}
// Virtualized list helpers
function searchAll(query: string, items: UnwrapRef<typeof attendees>) {
const found = []
for (const idx in items) {
if (items[idx].name.toLowerCase().includes(query.toLowerCase()) || query.trim() === '')
found.push(idx)
}
return found
}
function renderExternal(_: unknown, data: VirtualList.VirtualListRenderData) {
attendeeVirtualListData.value = data
}
</script>

<template>
<f7Page>
Event page
<f7Navbar>
<f7NavTitle>SSTAARS</f7NavTitle>
<f7NavRight>
<f7Link popup-close>
Close
</f7Link>
</f7NavRight>
<f7Subnavbar :inner="false">
<f7Searchbar search-container=".virtual-list" search-item="li" search-in=".item-title" />
</f7Subnavbar>
</f7Navbar>

<f7List v-if="eventIsLoading || checkedInUsersListPending" inset>
<div class="space-y-2">
<f7SkeletonBlock v-for="n in 2" :key="n" :height="`${n * 100}px`" class="rounded-md" effect="fade" />
</div>
</f7List>

<template v-else-if="event">
<f7BlockTitle large>
{{ event.name }}
</f7BlockTitle>

<f7Card>
<template #header>
<div class="flex justify-between w-full">
<div>
Checked in
</div>
<div class="rounded-md">
{{ checkedInUsersList.length }} / {{ event.attendees.length }}
</div>
</div>
</template>
</f7Card>

<f7List inset>
<f7Button tonal @click="state.scannerOpened = !state.scannerOpened">
{{ state.scannerOpened ? 'Close' : 'Open' }} scanner
</f7Button>
</f7List>

<div v-if="state.scannerOpened">
<AppServicesEventScanner @scan="onScan" />
</div>

<f7BlockTitle>Attendees</f7BlockTitle>

<f7List
strong inset virtual-list :virtual-list-params="{
items: attendees,
searchAll,
renderExternal,
height: 52,
}"
>
<ul>
<f7ListItem
v-for="attendee in attendeeVirtualListData.items" :key="attendee.admissionKey" swipeout
:style="`top: ${attendeeVirtualListData.topPosition}px`" :virtual-list-index="attendee.index"
>
<f7SwipeoutActions right>
<f7SwipeoutButton
confirm-text="Are you sure?"
:color="checkedInUsers[attendee.admissionKey] ? 'red' : 'green'" @click="toggle(attendee.admissionKey)"
>
{{ checkedInUsers[attendee.admissionKey] ? 'Check out' : 'Check in' }}
</f7SwipeoutButton>
</f7SwipeoutActions>

<template #title>
{{ attendee.name }}
</template>

<template #after>
<f7Chip v-if="checkedInUsers[attendee.admissionKey]" color="green">
{{ formattedDate(checkedInUsers[attendee.admissionKey]) }}
</f7Chip>
<f7Chip v-else color="red">
Not checked in
</f7Chip>
</template>
</f7ListItem>
</ul>
</f7List>
</template>
</f7Page>
</template>
56 changes: 56 additions & 0 deletions components/app/services/event/scanner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { f7List, f7ListItem } from 'framework7-vue'
import { Html5Qrcode } from 'html5-qrcode'
const emit = defineEmits(['scan'])
const state = ref({
selectedCameraId: '',
})
const { data: cameras } = useQuery({
queryKey: ['cameras'],
queryFn: () => Html5Qrcode.getCameras(),
refetchOnWindowFocus: false,
})
watchEffect((onCleanup) => {
if (state.value.selectedCameraId.length === 0)
return
const html5Qrcode = new Html5Qrcode('reader')
onCleanup(async () => {
await html5Qrcode.stop()
html5Qrcode.clear()
})
html5Qrcode.start(
state.value.selectedCameraId,
{
fps: 10,
qrbox: 250,
},
(decodedText) => {
emit('scan', decodedText)
},
() => {}, // Ignore all errors, unlikely to happen
)
})
</script>

<template>
<div>
<f7List inset>
<f7ListItem v-if="cameras" title="Camera" smart-select :smart-select-params="{ openIn: 'popover' }">
<select v-model="state.selectedCameraId" name="camera">
<option v-for="{ id, label } in cameras" :key="id" :value="id">
{{ label }}
</option>
</select>
</f7ListItem>
</f7List>

<div id="reader" />
</div>
</template>
19 changes: 13 additions & 6 deletions components/app/services/page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ async function click() {
const data = await $api(`/api/event/${state.eventId}`)
sstaarsStore.value.previousEvents[data.id] = data.name
state.sheetOpened = false
await nextTick(() => {
f7.views.current.router.navigate(`/app/services/event/${data.id}`, { openIn: 'sheet' })
})
await openEvent(state.eventId)
}
catch (err) {
if (err instanceof FetchError) {
Expand All @@ -35,6 +34,13 @@ async function click() {
state.pending = false
}
}
async function openEvent(id: string) {
state.sheetOpened = false
await nextTick(() => {
f7.view.current.router.navigate(`/app/services/event/${id}`, { openIn: 'popup' })
})
}
</script>

<template>
Expand All @@ -48,7 +54,7 @@ async function click() {
</f7NavTitleLarge>
</f7Navbar>

<f7List strong inset>
<f7List v-if="$growthbook.isOn('sstaars')" strong inset>
<f7ListItem class="font-semibold">
SSTAA Registration System (SSTAARS)
</f7ListItem>
Expand All @@ -63,7 +69,7 @@ async function click() {
</f7ListButton>
</f7List>

<f7Sheet v-model:opened="state.sheetOpened" style="height: auto" swipe-to-close backdrop push>
<f7Sheet v-model:opened="state.sheetOpened" style="height: auto" swipe-to-close>
<f7PageContent>
<f7BlockTitle large>
SSTAARS
Expand Down Expand Up @@ -96,7 +102,8 @@ async function click() {
<span v-if="Object.keys(sstaarsStore.previousEvents).length === 0" class="opacity-80">
No previous events.
</span>
<f7ListItem v-for="name, id in sstaarsStore.previousEvents" :key="id" :title="name" :link="`/app/services/event/${id}`" />

<f7ListItem v-for="name, id in sstaarsStore.previousEvents" :key="id" :title="name" @click="openEvent(id)" />
</f7List>
</f7PageContent>
</f7Sheet>
Expand Down
11 changes: 11 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export default defineNuxtConfig({
devtools: { enabled: true },
spaLoadingTemplate: './app/spa-loading-template.html',

experimental: {
// https://github.com/unjs/nitro/issues/1844
appManifest: false,
},

typescript: {
strict: true,
},
Expand Down Expand Up @@ -126,5 +131,11 @@ export default defineNuxtConfig({
firebase: {
projectId: 'sstaa-app' || process.env.FIREBASE_PROJECT_ID,
},

public: {
growthbook: {
clientKey: '' || process.env.GROWTHBOOK_CLIENT_KEY,
},
},
},
})
Loading

0 comments on commit 5bb8c7f

Please sign in to comment.