Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add admin dashboard #676

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
26 changes: 26 additions & 0 deletions components/AttendeeMention/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IAttendee, useAuth } from "@context/Auth";
import { ROLES } from "@lib/user";
import Link from "next/link";

interface Mention {
nickname: string;
name: string;
}

export default function AttendeeMention({ attendee }: { attendee: Mention }) {
const { user } = useAuth();
const isSelf =
user &&
user.type === ROLES.ATTENDEE &&
(user as IAttendee).nickname === attendee.nickname;

return (
<Link href={`/attendees/${attendee.nickname}`}>
<span
className={`font-ibold hover:underline ${isSelf ? "text-quinary" : ""}`}
>
{attendee.name}
</span>
</Link>
);
}
66 changes: 45 additions & 21 deletions components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,51 @@ import { Dialog, Transition } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBars, faTimes } from "@fortawesome/free-solid-svg-icons";

import { useAuth } from "@context/Auth";

const roleNavigations = {
sponsor: ["scanner", "visitors"],
attendee: [
"profile",
"wheel",
"badgedex",
"leaderboard",
"store",
"inventory",
"identifier",
],
admin: ["scanner", "visitors", "badges", "leaderboard", "users", "events"],
staff: ["badges", "leaderboard", "prizes", "identifier", "cv"],
import { IStaff, IUser, useAuth } from "@context/Auth";
import { ROLES } from "@lib/user";

// FIXME: normalize user type between moonstone and safira
const basePaths = {
[ROLES.SPONSOR]: "sponsor",
[ROLES.ATTENDEE]: "attendee",
[ROLES.STAFF]: "staff",
};

const roleNavigation = (user: IUser) => {
switch (user.type) {
case ROLES.SPONSOR:
return ["scanner", "visitors"];

case ROLES.ATTENDEE:
return [
"profile",
"wheel",
"badgedex",
"leaderboard",
"store",
"inventory",
"identifier",
];

case ROLES.STAFF:
return [
"leaderboard",
"badges",
"prizes",
"identifier",
"cv",
...((user as IStaff).is_admin ? [
"badgehistory",
"redeemhistory",
"spotlight",
] : [])
];

default:
throw new Error(`Unknown USER TYPE: ${user.type}`);
}
}

type LayoutProps = {
title?: string;
description?: string;
Expand All @@ -37,12 +65,8 @@ export default function Layout({ title, description, children }: LayoutProps) {
const router = useRouter();

const currentHref = router.asPath;
// FIXME: normalize user type between moonstone and safira
const links =
user.type === "company"
? roleNavigations["sponsor"]
: roleNavigations[user.type];
const basePath = user.type === "company" ? "sponsor" : user.type;
const links = roleNavigation(user);
const basePath = basePaths[user.type];

const openNavbar = () => {
setIsNavbarOpen(true);
Expand Down
14 changes: 8 additions & 6 deletions components/Navbar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const navigation = [
{ name: "FAQs", slug: "/faqs" },
];

const userNavigation = (type) => {
switch (type) {
const userNavigation = (user) => {
switch (user.type) {
case USER.ROLES.ATTENDEE:
return [{ name: "Dashboard", slug: "/attendee/profile" }];
case USER.ROLES.STAFF:
Expand All @@ -31,15 +31,17 @@ const userNavigation = (type) => {
{ name: "Give Badges", slug: "/staff/badges" },
{ name: "Give Prizes", slug: "/staff/prizes" },
{ name: "Upload CV", slug: "/staff/cv" },
...(user.is_admin ? [
{ name: "Manage Spotlight", slug: "/staff/spotlight" },
] : [])
];
case USER.ROLES.SPONSOR:
return [
{ name: "Scanner", slug: "/sponsor/scanner" },
{ name: "Visitors", slug: "/sponsor/visitors" },
];

default:
throw new Error(`Unknown USER TYPE: ${type}`);
throw new Error(`Unknown USER TYPE: ${user.type}`);
}
};

Expand Down Expand Up @@ -122,7 +124,7 @@ export default function Navbar({ bgColor, fgColor, button, children }) {
>
<Menu.Items className="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{user &&
userNavigation(user.type).map((item) => (
userNavigation(user).map((item) => (
<Menu.Item key={item.name}>
<Link
href={item.slug}
Expand Down Expand Up @@ -181,7 +183,7 @@ export default function Navbar({ bgColor, fgColor, button, children }) {
))}
{isAuthenticated &&
user &&
userNavigation(user.type).map((item) => (
userNavigation(user).map((item) => (
<Disclosure.Button
key={item.slug}
as="a"
Expand Down
32 changes: 32 additions & 0 deletions components/PaginatedSearch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ReactElement, useEffect, useState } from "react";

export default function PaginatedSearch<Elem>({
limit,
fetchElems,
showElems,
children
} : {
limit: number,
fetchElems: (query: string, offset: number, limit: number) => Promise<Elem[]> | Elem[],
showElems: (elems : Elem[]) => ReactElement
children?: ReactElement
}) {
const [ query, updateQuery ] = useState<string>("");
const [ offset, updateOffset ] = useState<number>(0);
const [ elems, updateElems ] = useState<Elem[]>([]);

useEffect(() => {
(async () => {
const fetched = await fetchElems(query, offset, limit);
updateElems(fetched);
})();
}, [query, offset]);

return (
<>
{ /*TODO: search bar and pagination*/ }
{ children }
{ showElems(elems) }
</>
);
}
2 changes: 1 addition & 1 deletion components/QRCodeCanvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Canvas = ({ uuid }: IProps) => {
const [src, setSrc] = useState("");

useEffect(() => {
QRCode.toDataURL(`https://seium.org/attendees/${uuid}`).then(setSrc);
QRCode.toDataURL(`https://${process.env.NEXT_PUBLIC_QRCODE_HOST}/attendees/${uuid}`).then(setSrc);
}, [uuid]);

return <img src={src} alt="qrcode" className="w-full object-contain"></img>;
Expand Down
61 changes: 61 additions & 0 deletions components/Table/TableColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { FunctionComponent, Key, ReactNode, createElement } from "react";

export enum Align {
Left,
Center,
Right
}

export function alignToCSS(align: Align): string {
switch (+align) {
case Align.Left: return "text-left justify-start";
case Align.Center: return "text-center justify-center";
case Align.Right: return "text-right justify-end";
}
}

export type TableColumnProps<T> = {
key?: Key;
header?: string;
headerAlign?: Align;
elemAlign?: Align;
elemPadding?: number;
colSpan?: number;
getter?: (e: T) => ReactNode;
};

type ResolvedTableColumnProps<T> = {
key: Key;
header: string;
headerAlign: Align;
elemAlign: Align;
elemPadding: number;
colSpan: number;
getter: (e: T) => ReactNode;
};

export function resolveProps<T>(props: TableColumnProps<T>): ResolvedTableColumnProps<T> {
return {
key: Math.random(),
header: "",
headerAlign: Align.Center,
elemAlign: Align.Center,
elemPadding: 0,
colSpan: 1,
getter: (x) => x as ReactNode,
...props
};
}

export function printHeader<T>(props : TableColumnProps<T>) {
return (
<div key={props.key} className={`flex col-span-${props.colSpan} ${alignToCSS(props.headerAlign)}`}>
{ props.header }
</div>
);
}


export default function TableColumn<T>(props : TableColumnProps<T>) {
return createElement<TableColumnProps<T>>(TableColumn<T>, props);
}
39 changes: 39 additions & 0 deletions components/Table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Key, ReactElement } from "react";
import TableColumn, { Align, TableColumnProps, alignToCSS, printHeader, resolveProps } from "./TableColumn";

export { TableColumn, Align };

type TableProps<T> = {
children: ReactElement<TableColumnProps<T>>[];
elems: T[];
elemKey?: (elem: T) => Key;
};

export default function Table<T>({ children, elems, elemKey } : TableProps<T>) {
const cols = children.map((c) => resolveProps(c.props));
const totalColSpan = cols.reduce((sum, c) => sum + c.colSpan, 0);
const anyHasHeader = cols.reduce((or, c) => c.header || or, false);

const printElem = (elem: T, isFirst: boolean, isLast: boolean) => {
const border = isLast ? "" : "border-b-2";
const key = elemKey ? elemKey(elem) : Math.random();

return <div key={key} className={`w-full py-4 ${border} grid grid-cols-${totalColSpan} items-center`}>
{ cols.map((c) =>
<div key={c.key} className={`flex col-span-${c.colSpan} ${alignToCSS(c.elemAlign)} px-${c.elemPadding}`}>
{ c.getter(elem) }
</div>)
}
</div>;
}

return <>
{
anyHasHeader &&
<div className={`w-full py-4 select-none text-iregular font-iregular grid grid-cols-${totalColSpan} items-center`}>
{ cols.map(printHeader) }
</div>
}
{ elems.map((e, i) => printElem(e, i === 0, i === elems.length - 1)) }
</>;
}
16 changes: 14 additions & 2 deletions context/Auth/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,21 @@ export interface IPrize {
not_redeemed: number;
}

export interface IRedeemable {
id: number;
image: string;
name: string;
not_redeemed: number;
price: number;
quantity: number;
}

export interface IAbstractUser {
email: string;
type: string;
}

export interface IAttendee extends IAbstractUser {
export interface IPublicAttendee {
avatar: string | null;
badge_count: number;
badges: IBadge[];
Expand All @@ -44,12 +53,15 @@ export interface IAttendee extends IAbstractUser {
name: string;
nickname: string;
prizes: IPrize[];
redeemables: IPrize[];
redeemables: IRedeemable[];
token_balance: number;
}

export interface IAttendee extends IAbstractUser, IPublicAttendee {}

export interface IStaff extends IAbstractUser {
id: number;
is_admin: boolean;
}

export interface ISponsor extends IAbstractUser {
Expand Down
2 changes: 2 additions & 0 deletions context/Auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export type {
IBadge,
IPrize,
IRedeemable,
IAbstractUser,
IPublicAttendee,
IAttendee,
IStaff,
ISponsor,
Expand Down
5 changes: 5 additions & 0 deletions context/Auth/withAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export function withAuth(WrappedComponent) {
"/staff/leaderboard",
"/staff/cv",
"/attendees/[uuid]",
...(user.is_admin ? [
"/staff/badgehistory",
"/staff/redeemhistory",
"/staff/spotlight",
] : [])
].includes(router.pathname)
) {
router.replace("/404");
Expand Down
Loading
Loading