Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions components/AdminHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ export default function AdminHeader() {
>
Users Dashboard
</NavLink>
<NavLink
href="/admin/tickets"
exact={true}
addClass="border-b-2 border-white"
className="mx-4 sourceSansPro textGradient 2xl:text-4xl lg:text-3xl md:text-xl font-semibold"
>
Claim Tickets
</NavLink>
{isAuthorized(user) && (
<NavLink
href="/admin/stats"
Expand Down
8 changes: 8 additions & 0 deletions components/DashboardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export default function DashboardHeader() {
>
Ask a Question
</NavLink>
<NavLink
href="/dashboard/tickets"
exact={true}
addClass="border-b-2 border-white"
className="mx-4 sourceSansPro textGradient xl:text-5xl md:text-3xl font-semibold"
>
Submit Ticket
</NavLink>
</div>
</header>
<div className="my-4 md:hidden ">
Expand Down
16 changes: 16 additions & 0 deletions lib/tickets/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface TicketParticipantInfo {
id: string;
firstName: string;
lastName: string;
}

export interface TicketSubmissionPayload {
ticketCreator: TicketParticipantInfo;
content: string;
}

export interface Ticket extends TicketSubmissionPayload {
ticketClaimer: TicketParticipantInfo;
completed: boolean;
ticketId: string;
}
203 changes: 203 additions & 0 deletions pages/admin/tickets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import Head from 'next/head';
import { useState, useEffect } from 'react';
import { isAuthorized } from '.';
import AdminHeader from '../../components/AdminHeader';
import LoadIcon from '../../components/LoadIcon';
import { RequestHelper } from '../../lib/request-helper';
import { Ticket, TicketParticipantInfo } from '../../lib/tickets/types';
import { useAuthContext } from '../../lib/user/AuthContext';

export default function ClaimTicketPage() {
const { user, isSignedIn } = useAuthContext();
const [loading, setLoading] = useState(true);
const [myClaimedTickets, setMyClaimedTickets] = useState<Ticket[]>([]);
const [unclaimedTickets, setUnclaimedTickets] = useState<Ticket[]>([]);

useEffect(() => {
async function getData() {
const { data } = await RequestHelper.get<Ticket[]>('/api/tickets/unclaim', {
headers: {
Authorization: user.token,
},
});
setMyClaimedTickets(data.filter((ticket) => ticket.ticketClaimer.id === user.id));
setUnclaimedTickets(data.filter((ticket) => ticket.ticketClaimer.id === ''));
setLoading(false);
}
getData();
}, []);

const claimTicket = async (ticketIdx: number) => {
try {
await RequestHelper.post<{ ticketId: string; ticketClaimer: TicketParticipantInfo }, unknown>(
'/api/tickets/claim',
{
headers: {
'Content-Type': 'application/json',
Authorization: user.token,
},
},
{
ticketId: unclaimedTickets[ticketIdx].ticketId,
ticketClaimer: {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
},
},
);

alert('Ticket claimed');
setMyClaimedTickets((prev) => [...prev, { ...unclaimedTickets[ticketIdx] }]);
setUnclaimedTickets((prev) => {
const claimedTicketId = prev[ticketIdx].ticketId;
return prev.filter((ticket) => ticket.ticketId !== claimedTicketId);
});
} catch (error) {
console.error(error);
alert('Error claiming tickets');
}
};

const unclaimTicket = async (ticketIdx: number) => {
try {
await RequestHelper.post<{ ticketId: string }, unknown>(
'/api/tickets/unclaim',
{
headers: {
'Content-Type': 'application/json',
Authorization: user.token,
},
},
{
ticketId: myClaimedTickets[ticketIdx].ticketId,
},
);

alert('Ticket unclaimed');
setUnclaimedTickets((prev) => [...prev, { ...myClaimedTickets[ticketIdx] }]);
setMyClaimedTickets((prev) => {
const unclaimedTicketId = prev[ticketIdx].ticketId;
return prev.filter((ticket) => ticket.ticketId !== unclaimedTicketId);
});
} catch (error) {
console.error(error);
alert('Error unclaiming tickets');
}
};

const resolveTicket = async (ticketIdx: number) => {
try {
await RequestHelper.post<{ ticketId: string }, unknown>(
'/api/tickets/resolve',
{
headers: {
'Content-Type': 'application/json',
Authorization: user.token,
},
},
{
ticketId: myClaimedTickets[ticketIdx].ticketId,
},
);

alert('Ticket resolved');
setMyClaimedTickets((prev) => {
const unclaimedTicketId = prev[ticketIdx].ticketId;
return prev.filter((ticket) => ticket.ticketId !== unclaimedTicketId);
});
} catch (error) {
console.error(error);
alert('Error resolving tickets');
}
};

if (!isSignedIn || !isAuthorized(user))
return (
<div className="background h-screen">
<div className="md:text-4xl sm:text-2xl text-xl text-white font-medium text-center mt-[6rem]">
Unauthorized
</div>
</div>
);

if (loading) return <LoadIcon width={200} height={200} />;
return (
<div className="flex flex-col flex-grow h-screen background text-white">
<Head>
<title>HackUTD IX - Admin</title>
<meta name="description" content="HackUTD's Admin Page" />
</Head>
<AdminHeader />
<div className="p-4">
<div className="my-6 p-4">
<h1 className="text-xl text-white font-bold">Unclaimed Tickets</h1>
{unclaimedTickets.length === 0 ? (
<h1>No unclaimed tickets at the moment</h1>
) : (
unclaimedTickets.map((ticket, idx) => (
<div
key={idx}
className="flex my-3 items-center justify-between my-2 w-1/2 border-2 p-3 rounded-lg"
>
<div>
<h1>
<span className="font-bold">Ticket Creator: </span>
{ticket.ticketCreator.firstName + ' ' + ticket.ticketCreator.lastName}
</h1>
<h1>
<span className="font-bold">Ticket Content: </span>
{ticket.content}
</h1>
</div>
<div>
<button
className="border-2 p-3 rounded-lg text-white"
onClick={async () => {
await claimTicket(idx);
}}
>
Claim
</button>
</div>
</div>
))
)}
</div>
<div className="my-6 p-4">
<h1 className="text-xl text-white font-bold">My Claimed Tickets</h1>
{myClaimedTickets.length === 0 ? (
<h1>You do not have any tickets at the moment</h1>
) : (
myClaimedTickets.map((ticket, idx) => (
<div key={idx} className="flex flex-col gap-y-5 my-2 w-1/2 border-2 p-3 rounded-lg">
<h1>
<span className="font-bold">Ticket Content: </span>
{ticket.content}
</h1>
<div className="flex gap-x-2">
<button
className="border-2 p-3 rounded-lg text-white"
onClick={async () => {
await unclaimTicket(idx);
}}
>
Unclaim Ticket
</button>
<button
className="border-2 p-3 rounded-lg text-white"
onClick={async () => {
await resolveTicket(idx);
}}
>
Resolve Ticket
</button>
</div>
</div>
))
)}
</div>
</div>
</div>
);
}
36 changes: 36 additions & 0 deletions pages/api/tickets/claim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { firestore } from 'firebase-admin';
import { NextApiRequest, NextApiResponse } from 'next';
import initializeApi from '../../../lib/admin/init';
import { userIsAuthorized } from '../../../lib/authorization/check-authorization';

initializeApi();
const db = firestore();
const TICKET_COLLECTIONS = '/tickets';

async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) {
const userToken = req.headers['authorization'] as string;
const isAuthorized = await userIsAuthorized(userToken, ['admin', 'super_admin']);

if (!isAuthorized) {
return res.status(403).json({
msg: 'Request only available for admin',
});
}
const { ticketClaimer, ticketId } = req.body;
await db.collection(TICKET_COLLECTIONS).doc(ticketId).update({ ticketClaimer });
return res.json({
msg: 'Ticket update completed',
});
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
switch (method) {
case 'POST': {
return handlePostRequest(req, res);
}
default: {
return res.end();
}
}
}
50 changes: 50 additions & 0 deletions pages/api/tickets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { auth, firestore } from 'firebase-admin';
import { NextApiRequest, NextApiHandler, NextApiResponse } from 'next';
import initializeApi from '../../../lib/admin/init';

initializeApi();
const db = firestore();
const TICKET_COLLECTIONS = '/tickets';
const REGISTRATION_COLLECTION = '/registrations';

async function getUserId(token: string) {
if (!token) return null;
const payload = await auth().verifyIdToken(token);
const snapshot = await firestore()
.collection(REGISTRATION_COLLECTION)
.where('id', '==', payload.uid)
.get();
if (snapshot.empty) return null;
return payload.uid;
}

async function handleGetRequest(req: NextApiRequest, res: NextApiResponse) {
const userToken = req.headers['authorization'] as string;
const userId = await getUserId(userToken);

const snapshot = await firestore()
.collection(TICKET_COLLECTIONS)
.where('ticketCreator.id', '==', userId)
.get();

const tickets = [];
snapshot.forEach((doc) => {
tickets.push({
...doc.data(),
ticketId: doc.id,
});
});
return res.json(tickets);
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
switch (method) {
case 'GET': {
return handleGetRequest(req, res);
}
default: {
return res.end();
}
}
}
38 changes: 38 additions & 0 deletions pages/api/tickets/resolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { firestore } from 'firebase-admin';
import { NextApiRequest, NextApiResponse } from 'next';
import initializeApi from '../../../lib/admin/init';
import { userIsAuthorized } from '../../../lib/authorization/check-authorization';

initializeApi();
const db = firestore();
const TICKET_COLLECTIONS = '/tickets';

async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) {
const userToken = req.headers['authorization'] as string;
const isAuthorized = await userIsAuthorized(userToken, ['admin', 'super_admin']);

if (!isAuthorized) {
return res.status(403).json({
msg: 'Request only available for admin',
});
}
const { ticketId } = req.body;
await db.collection(TICKET_COLLECTIONS).doc(ticketId).update({
completed: true,
});
return res.json({
msg: 'Ticket update completed',
});
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
switch (method) {
case 'POST': {
return handlePostRequest(req, res);
}
default: {
return res.end();
}
}
}
Loading