mirror of
https://github.com/siv-org/siv.git
synced 2026-01-09 10:27:57 -05:00
Download Convention QRs: Display unique conv auth tokens and indices
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
35
pages/api/conventions/[convention_id]/download-set.ts
Normal file
35
pages/api/conventions/[convention_id]/download-set.ts
Normal 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 })
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user