Skip to content

Commit b80e80a

Browse files
committed
Added walk-in registration and stats page
1 parent 45d04d7 commit b80e80a

File tree

9 files changed

+311
-5
lines changed

9 files changed

+311
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ yarn-error.log*
2828
.pnpm-debug.log*
2929

3030
# local env files
31+
.env*
3132
.env*.local
3233

3334
# vercel

components/AdminStatsCard.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
interface AdminStatsCardProps {
2+
title: string;
3+
value: number;
4+
}
5+
6+
export default function AdminStatsCard({
7+
title,
8+
value,
9+
}: AdminStatsCardProps) {
10+
return (
11+
<div className="border-2 p-5 flex flex-col rounded-xl m-4">
12+
<div className="flex items-center gap-x-6">
13+
<div className="flex flex-col">
14+
<h1 className="text-xl">{title}</h1>
15+
<h1 className="text-3xl font-bold">{value}</h1>
16+
</div>
17+
</div>
18+
</div>
19+
);
20+
}

pages/api/batchadd.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2+
import type { NextApiRequest, NextApiResponse } from 'next';
3+
import qr from 'qrcode';
4+
import { auth, firestore } from 'firebase-admin';
5+
import initializeApi from '../../lib/admin/init';
6+
7+
initializeApi();
8+
9+
const REGISTRATION_COLLECTION = '/registrations';
10+
const db = firestore();
11+
12+
// NOTE: Max 500 emails at once lmao
13+
async function sendEmail(req: NextApiRequest, res: NextApiResponse) {
14+
// Make sure you set the Content-Type header to application/json
15+
const { emails } = req.body;
16+
17+
if (!emails) {
18+
return res.status(400).send('Invalid email list');
19+
}
20+
let batch = db.batch();
21+
emails.forEach((email) => {
22+
batch.set(db.collection(REGISTRATION_COLLECTION).doc(email), {});
23+
});
24+
await batch.commit();
25+
26+
res.status(200).json({});
27+
}
28+
29+
export default async function handler(
30+
req: NextApiRequest,
31+
res: NextApiResponse
32+
) {
33+
if (req.method === 'POST') {
34+
await sendEmail(req, res);
35+
} else {
36+
res.status(405).json({});
37+
}
38+
}

pages/api/count.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2+
import type { NextApiRequest, NextApiResponse } from 'next';
3+
import sendgrid from '@sendgrid/mail';
4+
import qr from 'qrcode';
5+
import { auth, firestore } from 'firebase-admin';
6+
import initializeApi from '../../lib/admin/init';
7+
8+
sendgrid.setApiKey(process.env.SENDGRID_API_KEY ?? '');
9+
10+
initializeApi();
11+
12+
const REGISTRATION_COLLECTION = '/registrations';
13+
const db = firestore();
14+
15+
async function countReg(req: NextApiRequest, res: NextApiResponse) {
16+
// Count which scans are done
17+
18+
const snapshot = await db.collection(REGISTRATION_COLLECTION).get();
19+
const scanCount = new Map<string, number>();
20+
snapshot.forEach((doc) => {
21+
const scanRef = doc.get('scans');
22+
if (scanRef === undefined) return;
23+
scanRef.forEach((scan) => {
24+
scanCount.set(scan, (scanCount.get(scan) ?? 0) + 1);
25+
});
26+
});
27+
res.status(200).json({ scans: Object.fromEntries(scanCount) });
28+
}
29+
30+
export default async function handler(
31+
req: NextApiRequest,
32+
res: NextApiResponse
33+
) {
34+
if (req.method === 'GET') {
35+
await countReg(req, res);
36+
} else {
37+
res.status(405).json({});
38+
}
39+
}

pages/api/email.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22
import type { NextApiRequest, NextApiResponse } from 'next';
33
import sendgrid from '@sendgrid/mail';
44
import qr from 'qrcode';
5+
import { auth, firestore } from 'firebase-admin';
6+
import initializeApi from '../../lib/admin/init';
57

68
sendgrid.setApiKey(process.env.SENDGRID_API_KEY ?? '');
79

10+
initializeApi();
11+
12+
const REGISTRATION_COLLECTION = '/registrations';
13+
const db = firestore();
14+
815
async function sendEmail(req: NextApiRequest, res: NextApiResponse) {
9-
const { emails } = JSON.parse(req.body);
16+
// Make sure you set the Content-Type header to application/json
17+
const { emails } = req.body;
1018

1119
if (!emails) {
1220
return res.status(400).send('Invalid email list');
1321
}
14-
1522
emails.forEach(async (email) => {
1623
const qrcode = (await qr.toDataURL(email)).replace(
1724
'data:image/png;base64,',
@@ -21,15 +28,18 @@ async function sendEmail(req: NextApiRequest, res: NextApiResponse) {
2128
to: email,
2229
from: process.env.SENDGRID_SENDER as string,
2330
subject: 'Axxess Hackathon QR Code',
24-
text: "Hello,\n\nThank you for registering for the Axxess Hackathon. Below is your unique QR-code for check-in, swag, and food! We recommend arriving at 8:45am to get in line for check-in as space is limited.\n\nYou'll recieve an email in a few days (Wednesday or Thursday) with all the Axxess Hackathon event details.\n\nHackathon check-in begins at April 1st, 9 a.m. CDT.\n\nLocation:\nECSW 1.100 Axxess Atrium\n800 W. Campbell Road, Richardson, Texas 75080\n\nIf you have any questions or concerns, please don't hesitate to reach out to us at [email protected].\n\nThank you again for registering for the Axxess Hackathon. We can't wait to see you there!\n\nBest regards,\n\nThe Axxess Hackathon Team",
31+
text: "Hello,\n\nThank you for registering for the Axxess Hackathon. Below is your unique QR-code for check-in, swag, and food! We recommend arriving at 8:45am to get in line for check-in as space is limited.\n\nYou'll recieve an email soon (Wednesday or Thursday) with all the Axxess Hackathon event details.\n\nHackathon check-in begins at April 1st, 9 a.m. CDT.\n\nLocation:\nECSW 1.100 Axxess Atrium\n800 W. Campbell Road, Richardson, Texas 75080\n\nIf you have any questions or concerns, please don't hesitate to reach out to us at [email protected].\n\nThank you again for registering for the Axxess Hackathon. We can't wait to see you there!\n\nBest regards,\n\nThe Axxess Hackathon Team",
2532
attachments: [
2633
{
2734
content: qrcode,
2835
filename: 'qrcode.png',
2936
},
3037
],
3138
};
32-
sendgrid.send(msg).catch((err) => console.log(err.response.body.errors));
39+
sendgrid.send(msg).catch((err) => {
40+
console.log(email);
41+
console.log(err.response.body.errors);
42+
});
3343
});
3444
res.status(200).json({});
3545
}

pages/api/walkin.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2+
import type { NextApiRequest, NextApiResponse } from "next";
3+
import sendgrid from "@sendgrid/mail";
4+
import qr from "qrcode";
5+
import { auth, firestore } from "firebase-admin";
6+
import initializeApi from "../../lib/admin/init";
7+
8+
sendgrid.setApiKey(process.env.SENDGRID_API_KEY ?? "");
9+
10+
initializeApi();
11+
12+
const REGISTRATION_COLLECTION = "/registrations";
13+
const db = firestore();
14+
15+
async function sendEmail(req: NextApiRequest, res: NextApiResponse) {
16+
const { email } = req.body;
17+
18+
if (!email) {
19+
return res.status(400).send("Invalid email");
20+
}
21+
22+
// Make sure user is not already in the collection
23+
const snapshot = await db
24+
.collection(REGISTRATION_COLLECTION)
25+
.doc(email)
26+
.get();
27+
if (snapshot.exists) return res.status(400).send("Email already exists");
28+
29+
// Create user in the collection
30+
await db.collection(REGISTRATION_COLLECTION).doc(email).set({});
31+
32+
const qrcode = (await qr.toDataURL(email)).replace(
33+
"data:image/png;base64,",
34+
""
35+
);
36+
const msg: sendgrid.MailDataRequired = {
37+
to: email,
38+
from: process.env.SENDGRID_SENDER as string,
39+
subject: "Axxess Hackathon QR Code",
40+
text: "Hello,\n\nThank you for registering for the Axxess Hackathon. Below is your unique QR-code for check-in, swag, and food! We recommend arriving at 8:45am to get in line for check-in as space is limited.\n\nYou'll recieve an email in a few days (Wednesday or Thursday) with all the Axxess Hackathon event details.\n\nHackathon check-in begins at April 1st, 9 a.m. CDT.\n\nLocation:\nECSW 1.100 Axxess Atrium\n800 W. Campbell Road, Richardson, Texas 75080\n\nIf you have any questions or concerns, please don't hesitate to reach out to us at [email protected].\n\nThank you again for registering for the Axxess Hackathon. We can't wait to see you there!\n\nBest regards,\n\nThe Axxess Hackathon Team",
41+
attachments: [
42+
{
43+
content: qrcode,
44+
filename: "qrcode.png",
45+
},
46+
],
47+
};
48+
sendgrid.send(msg).catch((err) => console.log(err.response.body.errors));
49+
res.status(200).json({});
50+
}
51+
52+
export default function handler(req: NextApiRequest, res: NextApiResponse) {
53+
if (req.method === "POST") {
54+
sendEmail(req, res);
55+
} else {
56+
res.status(405).json({});
57+
}
58+
}

pages/index.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react';
77
import { Dialog } from '@headlessui/react';
88
import LoadIcon from '../components/LoadIcon';
99
import ScanType from '../components/ScanType';
10+
import { useRouter } from 'next/router';
1011

1112
const successStrings = {
1213
claimed: 'Scan claimed...',
@@ -57,6 +58,8 @@ const Home: NextPage = () => {
5758
setCurrentScanIdx(idx);
5859
};
5960

61+
const router = useRouter();
62+
6063
const handleScan = async (data: string) => {
6164
const query = new URL(`http://localhost:3000/api/scan`);
6265
fetch(query.toString().replaceAll('http://localhost:3000', ''), {
@@ -475,7 +478,7 @@ const Home: NextPage = () => {
475478
!editScan &&
476479
!showDeleteScanDialog &&
477480
!startScan && (
478-
<div className="mx-auto my-5">
481+
<div className="mx-auto my-5 flex flex-col items-center">
479482
<button
480483
className="bg-green-300 p-3 rounded-lg font-bold hover:bg-green-200"
481484
onClick={() => {
@@ -484,6 +487,22 @@ const Home: NextPage = () => {
484487
>
485488
Add a new Scan
486489
</button>
490+
<button
491+
className="bg-blue-300 p-3 rounded-lg font-bold hover:bg-blue-200 block mt-4"
492+
onClick={() => {
493+
router.push('/new');
494+
}}
495+
>
496+
REGISTER WALK-INS
497+
</button>
498+
<button
499+
className="bg-red-300 p-3 rounded-lg font-bold hover:bg-red-200 block mt-4"
500+
onClick={() => {
501+
router.push('/stats');
502+
}}
503+
>
504+
Stats
505+
</button>
487506
</div>
488507
)}
489508
</div>

pages/new.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { NextPage } from "next";
2+
import { useRouter } from "next/router";
3+
import { useState } from "react";
4+
5+
const NewEmail: NextPage = () => {
6+
const [email, setEmail] = useState("");
7+
const router = useRouter();
8+
9+
const handleInputChange = (newEmail) => {
10+
setEmail(newEmail.target.value);
11+
};
12+
13+
const handleKeydown = (k) => {
14+
if (k.key == "Enter") {
15+
submit();
16+
}
17+
};
18+
19+
const submit = async () => {
20+
try {
21+
const res = await fetch("/api/walkin", {
22+
mode: "cors",
23+
method: "POST",
24+
headers: { "Content-Type": "application/json" },
25+
body: JSON.stringify({
26+
email,
27+
}),
28+
});
29+
if (res.status !== 200) {
30+
alert(await res.text());
31+
} else {
32+
alert("Email added!");
33+
}
34+
} catch (err) {
35+
alert(err);
36+
}
37+
setEmail("");
38+
};
39+
40+
return (
41+
<div className="flex flex-col items-center justify-center h-[100vh]">
42+
<h1 className="text-center text-3xl mb-4 text-red-300">Add a Walk-in</h1>
43+
<input
44+
type="text"
45+
className="text-2xl"
46+
value={email}
47+
onChange={handleInputChange}
48+
onKeyDown={handleKeydown}
49+
/>
50+
<button
51+
className="bg-green-300 p-3 rounded-lg font-bold hover:bg-green-200 block mt-4"
52+
onClick={submit}
53+
>
54+
Submit
55+
</button>
56+
57+
<button
58+
className="bg-orange-300 p-3 rounded-lg font-bold hover:bg-orange-200 block mt-4"
59+
onClick={() => {
60+
router.push("/");
61+
}}
62+
>
63+
back
64+
</button>
65+
</div>
66+
);
67+
};
68+
69+
export default NewEmail;

pages/stats.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { NextPage } from "next";
2+
import { useRouter } from "next/router";
3+
import { useEffect, useState } from "react";
4+
import AdminStatsCard from "../components/AdminStatsCard";
5+
6+
const StatsPage: NextPage = () => {
7+
const [statsData, setStatsData] = useState<Record<string, number>>();
8+
const router = useRouter();
9+
useEffect(() => {
10+
(async () => {
11+
try {
12+
const res = await fetch("/api/count", {
13+
mode: "cors",
14+
method: "GET",
15+
});
16+
if (res.status !== 200) {
17+
alert(await res.text());
18+
} else {
19+
const data = await res.json();
20+
console.log(data);
21+
setStatsData(data.scans);
22+
}
23+
} catch (err) {
24+
alert(err);
25+
}
26+
})();
27+
}, []);
28+
return (
29+
<div>
30+
<div className="w-full mx-auto flex flex-col gap-y-6">
31+
<div className="flex-col gap-y-3 w-full md:flex-row flex justify-around gap-x-2">
32+
{statsData &&
33+
Object.keys(statsData).map((scan) => (
34+
<AdminStatsCard title={scan} value={statsData[scan]} />
35+
))}
36+
</div>
37+
</div>
38+
<div className="w-full flex items-center justify-center">
39+
<button
40+
className="bg-blue-300 p-3 rounded-lg font-bold hover:bg-blue-200 block mt-4"
41+
onClick={() => {
42+
router.push("/");
43+
}}
44+
>
45+
Back
46+
</button>
47+
</div>
48+
</div>
49+
);
50+
};
51+
52+
export default StatsPage;

0 commit comments

Comments
 (0)