Download Convention QRs: Display unique conv auth tokens and indices

This commit is contained in:
David Ernst
2024-04-11 00:37:22 -07:00
parent 3c6850a1d1
commit 9addcfa88c
7 changed files with 96 additions and 14 deletions

View File

@@ -23,11 +23,12 @@ module.exports = {
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0, // Verbose
'@typescript-eslint/no-empty-function': 0, // unnecessary
'@typescript-eslint/no-unused-vars': 1, // hint not error
'react/jsx-sort-props': [2, { callbacksLast: true, shorthandFirst: true }], // style
'react/no-unknown-property': [2, { ignore: ['jsx', 'global'] }], // inserted by next's styled-jsx
'react/react-in-jsx-scope': 0, // Handled by Next.js
'sort-destructure-keys/sort-destructure-keys': 2, // style
'sort-keys-fix/sort-keys-fix': 2, // style
'sort-destructure-keys/sort-destructure-keys': 1, // style
'sort-keys-fix/sort-keys-fix': 1, // style
},
settings: {
react: {

View File

@@ -37,7 +37,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
.doc(voter_id)
.set({
createdAt,
index: i + prev_num_voters,
index: i + prev_num_voters + 1,
setIndex: newSetIndex,
voter_id,
}),

View File

@@ -0,0 +1,35 @@
import { firebase } from 'api/_services'
import { NextApiRequest, NextApiResponse } from 'next'
import { checkJwtOwnsConvention } from '../../validate-admin-jwt'
export type ConventionVoter = {
index: number
setIndex: number
voter_id: string
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { convention_id, set } = req.query
if (!convention_id || typeof convention_id !== 'string')
return res.status(401).json({ error: `Missing convention_id` })
// Confirm they created this convention
const jwt = await checkJwtOwnsConvention(req, res, convention_id)
if (!jwt.valid) return
if (!set || typeof set !== 'string') return res.status(401).json({ error: `Missing set` })
// Get all voters in this set
const voterDocs = await firebase
.firestore()
.collection('conventions')
.doc(convention_id)
.collection('voter_ids')
.where('setIndex', '==', Number(set))
.get()
const voters = voterDocs.docs.map((d) => d.data()) as ConventionVoter[]
res.status(200).send({ convention_title: jwt.convention_title, voters })
}

View File

@@ -17,6 +17,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
convention_title,
created_at: new Date(),
creator: jwt.email,
id: convention_id,
num_voters: 0,
})

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useRef, useState } from 'react'
import { api } from 'src/api-helper'
import { SaveButton } from './SaveButton'
@@ -6,6 +6,7 @@ import { revalidate } from './useConventionInfo'
export const CreateVoterCredentials = ({ convention_id }: { convention_id: string }) => {
const [numVoters, setNumVoters] = useState<string>('')
const $saveBtn = useRef<HTMLAnchorElement>(null)
return (
<div>
@@ -18,10 +19,17 @@ export const CreateVoterCredentials = ({ convention_id }: { convention_id: strin
type="number"
value={numVoters}
onChange={(e) => setNumVoters(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur()
$saveBtn.current?.click()
}
}}
/>
<SaveButton
disabled={!numVoters}
ref={$saveBtn}
text="Create"
onPress={async () => {
await api(`/conventions/${convention_id}/add-voters`, { numVoters: Number(numVoters) })

View File

@@ -1,23 +1,60 @@
import { ConventionVoter } from 'api/conventions/[convention_id]/download-set'
import { useRouter } from 'next/router'
import { Head } from 'src/Head'
import useSWR from 'swr'
import TimeAgo from 'timeago-react'
import { QRCode } from './QRCode'
export const DownloadQRsPage = () => {
const {
query: { n },
} = useRouter()
const fetcher = (url: string) => fetch(url).then((r) => r.json())
if (!n || !Number(n) || isNaN(Number(n))) return <p className="p-4">Invalid number</p>
export const DownloadQRsPage = () => {
const { c, set } = useRouter().query
// Download voters on page load
const { data, error, isLoading } = useSWR(
!c || !set ? null : `/api/conventions/${c}/download-set?set=${set}`,
fetcher,
)
// Handle error conditions
if (!c || typeof c !== 'string') return <p className="p-4">Missing convention_id</p>
if (!set || typeof set !== 'string') return <p className="p-4">Missing set</p>
if (error) return <p className="p-4">Error: {JSON.stringify(error)}</p>
// console.log('swr ran:', { c, data, isLoading, set })
if (!data || isLoading) return <p className="p-4">Loading...</p>
const { convention_title, voters } = data
if (!voters.length) return <p className="p-4">Empty set</p>
const createdAt = new Date(voters[0].createdAt._seconds * 1000)
return (
<div className="p-4 overflow-auto">
<p>Right Click {'→'} Print</p>
<Head title={`${voters.length} for ${convention_title}`} />
{/* Header row */}
<p className="flex justify-between">
<span className="font-bold">Right Click {'→'} Print </span>
<span>
{convention_title}: Set of {voters.length}
</span>
<span>
<span className="text-sm"> Created </span>
{createdAt.toLocaleString()} <span className="text-sm opacity-60">({<TimeAgo datetime={createdAt} />})</span>
</span>
</p>
{/* Grid of QRs */}
<div className="flex flex-wrap -mx-2.5">
{new Array(Number(n || 0)).fill(0).map((_, i) => (
{(voters as ConventionVoter[]).map(({ index, voter_id }, i) => (
<span className="mx-2.5 my-1.5 text-center" key={i}>
<span className="text-sm opacity-50">{voter_id}</span>
<QRCode />
<span>{i + 1}</span>
<span>{index}</span>
</span>
))}
</div>

View File

@@ -4,7 +4,7 @@ import TimeAgo from 'timeago-react'
import { useConventionInfo } from './useConventionInfo'
export const ListOfVoterSets = () => {
const { voters } = useConventionInfo()
const { id, voters } = useConventionInfo()
return (
<ol className="inset-0 pl-6 mt-0 ml-0">
@@ -14,7 +14,7 @@ export const ListOfVoterSets = () => {
<span className="inline-block w-20">Set of {number} </span>
{/* Download */}
<Link href={`/admin/conventions/download?n=${number}`} target="_blank">
<Link href={`/admin/conventions/download?c=${id}&set=${i}`} target="_blank">
<a className="pl-1" target="_blank">
Download
</a>