diff --git a/components/AttendeeMention/index.tsx b/components/AttendeeMention/index.tsx
new file mode 100644
index 00000000..cd7f4f0c
--- /dev/null
+++ b/components/AttendeeMention/index.tsx
@@ -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 (
+
+
+ {attendee.name}
+
+
+ );
+}
diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx
index d1e79a5f..9da12491 100644
--- a/components/Layout/Layout.tsx
+++ b/components/Layout/Layout.tsx
@@ -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;
@@ -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);
diff --git a/components/Navbar/index.jsx b/components/Navbar/index.jsx
index 7b3ddb6d..312f3b30 100644
--- a/components/Navbar/index.jsx
+++ b/components/Navbar/index.jsx
@@ -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:
@@ -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}`);
}
};
@@ -122,7 +124,7 @@ export default function Navbar({ bgColor, fgColor, button, children }) {
>
{user &&
- userNavigation(user.type).map((item) => (
+ userNavigation(user).map((item) => (
(
+ userNavigation(user).map((item) => (
({
+ limit,
+ fetchElems,
+ showElems,
+ children
+} : {
+ limit: number,
+ fetchElems: (query: string, offset: number, limit: number) => Promise | Elem[],
+ showElems: (elems : Elem[]) => ReactElement
+ children?: ReactElement
+}) {
+ const [ query, updateQuery ] = useState("");
+ const [ offset, updateOffset ] = useState(0);
+ const [ elems, updateElems ] = useState([]);
+
+ useEffect(() => {
+ (async () => {
+ const fetched = await fetchElems(query, offset, limit);
+ updateElems(fetched);
+ })();
+ }, [query, offset]);
+
+ return (
+ <>
+ { /*TODO: search bar and pagination*/ }
+ { children }
+ { showElems(elems) }
+ >
+ );
+}
\ No newline at end of file
diff --git a/components/QRCodeCanvas/index.tsx b/components/QRCodeCanvas/index.tsx
index 538fd837..f6b76518 100644
--- a/components/QRCodeCanvas/index.tsx
+++ b/components/QRCodeCanvas/index.tsx
@@ -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 ;
diff --git a/components/Table/TableColumn.tsx b/components/Table/TableColumn.tsx
new file mode 100644
index 00000000..05625097
--- /dev/null
+++ b/components/Table/TableColumn.tsx
@@ -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 = {
+ key?: Key;
+ header?: string;
+ headerAlign?: Align;
+ elemAlign?: Align;
+ elemPadding?: number;
+ colSpan?: number;
+ getter?: (e: T) => ReactNode;
+};
+
+type ResolvedTableColumnProps = {
+ key: Key;
+ header: string;
+ headerAlign: Align;
+ elemAlign: Align;
+ elemPadding: number;
+ colSpan: number;
+ getter: (e: T) => ReactNode;
+};
+
+export function resolveProps(props: TableColumnProps): ResolvedTableColumnProps {
+ return {
+ key: Math.random(),
+ header: "",
+ headerAlign: Align.Center,
+ elemAlign: Align.Center,
+ elemPadding: 0,
+ colSpan: 1,
+ getter: (x) => x as ReactNode,
+ ...props
+ };
+}
+
+export function printHeader(props : TableColumnProps) {
+ return (
+
+ { props.header }
+
+ );
+}
+
+
+export default function TableColumn(props : TableColumnProps) {
+ return createElement>(TableColumn, props);
+}
\ No newline at end of file
diff --git a/components/Table/index.tsx b/components/Table/index.tsx
new file mode 100644
index 00000000..0457d84e
--- /dev/null
+++ b/components/Table/index.tsx
@@ -0,0 +1,39 @@
+import { Key, ReactElement } from "react";
+import TableColumn, { Align, TableColumnProps, alignToCSS, printHeader, resolveProps } from "./TableColumn";
+
+export { TableColumn, Align };
+
+type TableProps = {
+ children: ReactElement>[];
+ elems: T[];
+ elemKey?: (elem: T) => Key;
+};
+
+export default function Table({ children, elems, elemKey } : TableProps) {
+ 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
+ { cols.map((c) =>
+
+ { c.getter(elem) }
+
)
+ }
+
;
+ }
+
+ return <>
+ {
+ anyHasHeader &&
+
+ { cols.map(printHeader) }
+
+ }
+ { elems.map((e, i) => printElem(e, i === 0, i === elems.length - 1)) }
+ >;
+}
\ No newline at end of file
diff --git a/context/Auth/AuthContext.tsx b/context/Auth/AuthContext.tsx
index ef861ac8..d92ff1f0 100644
--- a/context/Auth/AuthContext.tsx
+++ b/context/Auth/AuthContext.tsx
@@ -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[];
@@ -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 {
diff --git a/context/Auth/index.ts b/context/Auth/index.ts
index 7847d76f..29d2888c 100644
--- a/context/Auth/index.ts
+++ b/context/Auth/index.ts
@@ -1,7 +1,9 @@
export type {
IBadge,
IPrize,
+ IRedeemable,
IAbstractUser,
+ IPublicAttendee,
IAttendee,
IStaff,
ISponsor,
diff --git a/context/Auth/withAuth.js b/context/Auth/withAuth.js
index 114ddac4..a080d389 100644
--- a/context/Auth/withAuth.js
+++ b/context/Auth/withAuth.js
@@ -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");
diff --git a/layout/Admin/BadgeHistory/BadgeHistory.tsx b/layout/Admin/BadgeHistory/BadgeHistory.tsx
new file mode 100644
index 00000000..da48fd21
--- /dev/null
+++ b/layout/Admin/BadgeHistory/BadgeHistory.tsx
@@ -0,0 +1,113 @@
+import AttendeeMention from "@components/AttendeeMention";
+import PaginatedSearch from "@components/PaginatedSearch";
+import Layout from "@components/Layout";
+import { IPublicAttendee, IBadge, IStaff, withAuth } from "@context/Auth";
+import { useEffect, useState } from "react";
+import Table, { TableColumn } from "@components/Table";
+
+interface IBadgeAward {
+ attendee: IPublicAttendee;
+ badge: IBadge;
+ staff: IStaff;
+ timestamp: string;
+}
+
+function displayTimestamp(timestamp: string) {
+ return timestamp; //TODO
+}
+
+export function BadgeHistory() {
+ const x = {
+ timestamp: "2023-02-16T15:34:27Z",
+ attendee: {
+ avatar:
+ "https://sei24-staging.s3.amazonaws.com/uploads/attendee/avatars/f7c90ab0-aa79-4528-bff3-3ad6a811ac83/original.png?v=63872645262",
+ badge_count: 13,
+ badges: [
+ {
+ avatar: null,
+ begin: "2023-02-14T09:00:00Z",
+ description: "Visita o stand da IT Sector",
+ end: "2023-02-16T19:00:00Z",
+ id: 28,
+ name: "IT Sector",
+ tokens: 40,
+ type: 4,
+ },
+ {
+ avatar: null,
+ begin: "2023-02-15T09:00:00Z",
+ description: "Visita o stand da Continental",
+ end: "2023-02-17T17:00:00Z",
+ id: 29,
+ name: "Continental",
+ tokens: 40,
+ type: 4,
+ },
+ ],
+ course: 2,
+ cv: "https://sei24-staging.s3.amazonaws.com/uploads/attendee/cvs/f7c90ab0-aa79-4528-bff3-3ad6a811ac83/original.pdf?v=63872641922",
+ entries: 14,
+ id: "f7c90ab0-aa79-4528-bff3-3ad6a811ac83",
+ name: "attendee69",
+ nickname: "jmf",
+ prizes: [
+ {
+ avatar:
+ "https://sei24-staging.s3.amazonaws.com/uploads/prize/avatars/14/original.png?v=63870223172",
+ id: 14,
+ is_redeemable: false,
+ name: "Nada",
+ not_redeemed: 1,
+ },
+ ],
+ redeemables: [
+ {
+ id: 18,
+ image:
+ "https://sei24-staging.s3.amazonaws.com/uploads/redeemable/avatars/18/original.png?v=63870223178",
+ name: "Caneca SEI '23",
+ not_redeemed: 1,
+ price: 0,
+ quantity: 1,
+ },
+ ],
+ token_balance: 110,
+ },
+ badge: {
+ avatar: null,
+ begin: "2023-02-14T09:00:00Z",
+ description: "Visita o stand da IT Sector",
+ end: "2023-02-16T19:00:00Z",
+ id: 28,
+ name: "IT Sector",
+ tokens: 40,
+ type: 4,
+ },
+ staff: {
+ email: "staff13@seium.org",
+ id: 13,
+ type: "staff",
+ is_admin: false
+ },
+ };
+ const fetchAwards = () => [x,x,x,x,x];
+
+ return (
+
+ fetchElems={fetchAwards}
+ showElems={(ee) =>
+ elems={ee}>
+ header="Attendee" getter={(e) => } />
+ header="Badge" getter={(e) => e.badge.name } />
+ header="Staff" getter={(e) => e.staff.email } />
+ header="Timestamp" getter={(e) => displayTimestamp(e.timestamp) } />
+
+ }
+ limit={50}
+ />
+
+ );
+}
+
+export default withAuth(BadgeHistory);
\ No newline at end of file
diff --git a/layout/Admin/BadgeHistory/index.tsx b/layout/Admin/BadgeHistory/index.tsx
new file mode 100644
index 00000000..ae857c09
--- /dev/null
+++ b/layout/Admin/BadgeHistory/index.tsx
@@ -0,0 +1 @@
+export { default } from "./BadgeHistory";
diff --git a/layout/Admin/RedeemHistory/RedeemHistory.tsx b/layout/Admin/RedeemHistory/RedeemHistory.tsx
new file mode 100644
index 00000000..47840589
--- /dev/null
+++ b/layout/Admin/RedeemHistory/RedeemHistory.tsx
@@ -0,0 +1,219 @@
+import AttendeeMention from "@components/AttendeeMention";
+import PaginatedSearch from "@components/PaginatedSearch";
+import Layout from "@components/Layout";
+import { IPublicAttendee, IPrize, IStaff, withAuth, IRedeemable, IAttendee } from "@context/Auth";
+import { useEffect, useState } from "react";
+import Table, { TableColumn } from "@components/Table";
+
+interface IRedeem {
+ attendee: IPublicAttendee;
+ redeemable?: IRedeemable;
+ prize?: IPrize;
+ quantity: number;
+ timestamp: string;
+}
+
+enum RedeemType {
+ Prize,
+ Redeemable
+}
+
+interface IRedeemDetails {
+ attendee: IPublicAttendee;
+ type: RedeemType;
+ name: string;
+ image: string;
+ quantity: string;
+ timestamp: string;
+}
+
+function redeemDetails(redeem : IRedeem) : IRedeemDetails {
+ if (redeem.prize) {
+ return {
+ attendee: redeem.attendee,
+ type: RedeemType.Prize,
+ name: redeem.prize.name,
+ image: redeem.prize.avatar,
+ quantity: redeem.quantity.toString(),
+ timestamp: redeem.timestamp
+ }
+ } else {
+ return {
+ attendee: redeem.attendee,
+ type: RedeemType.Redeemable,
+ name: redeem.redeemable.name,
+ image: redeem.redeemable.image,
+ quantity: "-",
+ timestamp: redeem.timestamp
+ }
+ }
+}
+
+function displayRedeemType(type: RedeemType) {
+ switch (type) {
+ case RedeemType.Prize: return "Prize";
+ case RedeemType.Redeemable: return "Shop Item"
+ }
+}
+
+function displayTimestamp(timestamp: string) {
+ return timestamp; //TODO
+}
+
+export function RedeemHistory() {
+ const x: IRedeem = {
+ timestamp: "2023-02-16T15:34:27Z",
+ attendee: {
+ avatar:
+ "https://sei24-staging.s3.amazonaws.com/uploads/attendee/avatars/f7c90ab0-aa79-4528-bff3-3ad6a811ac83/original.png?v=63872645262",
+ badge_count: 13,
+ badges: [
+ {
+ avatar: null,
+ begin: "2023-02-14T09:00:00Z",
+ description: "Visita o stand da IT Sector",
+ end: "2023-02-16T19:00:00Z",
+ id: 28,
+ name: "IT Sector",
+ tokens: 40,
+ type: 4,
+ },
+ {
+ avatar: null,
+ begin: "2023-02-15T09:00:00Z",
+ description: "Visita o stand da Continental",
+ end: "2023-02-17T17:00:00Z",
+ id: 29,
+ name: "Continental",
+ tokens: 40,
+ type: 4,
+ },
+ ],
+ course: 2,
+ cv: "https://sei24-staging.s3.amazonaws.com/uploads/attendee/cvs/f7c90ab0-aa79-4528-bff3-3ad6a811ac83/original.pdf?v=63872641922",
+ entries: 14,
+ id: "f7c90ab0-aa79-4528-bff3-3ad6a811ac83",
+ name: "attendee69",
+ nickname: "jmf",
+ prizes: [
+ {
+ avatar:
+ "https://sei24-staging.s3.amazonaws.com/uploads/prize/avatars/14/original.png?v=63870223172",
+ id: 14,
+ is_redeemable: false,
+ name: "Nada",
+ not_redeemed: 1,
+ },
+ ],
+ redeemables: [
+ {
+ id: 18,
+ image:
+ "https://sei24-staging.s3.amazonaws.com/uploads/redeemable/avatars/18/original.png?v=63870223178",
+ name: "Caneca SEI '23",
+ not_redeemed: 1,
+ price: 0,
+ quantity: 1,
+ },
+ ],
+ token_balance: 110,
+ },
+ redeemable: {
+ id: 18,
+ image:
+ "https://sei24-staging.s3.amazonaws.com/uploads/redeemable/avatars/18/original.png?v=63870223178",
+ name: "Caneca SEI '23",
+ not_redeemed: 1,
+ price: 0,
+ quantity: 1,
+ },
+ quantity: 1
+ };
+ const y:IRedeem = {
+ timestamp: "2023-02-16T15:34:27Z",
+ attendee: {
+ avatar:
+ "https://sei24-staging.s3.amazonaws.com/uploads/attendee/avatars/f7c90ab0-aa79-4528-bff3-3ad6a811ac83/original.png?v=63872645262",
+ badge_count: 13,
+ badges: [
+ {
+ avatar: null,
+ begin: "2023-02-14T09:00:00Z",
+ description: "Visita o stand da IT Sector",
+ end: "2023-02-16T19:00:00Z",
+ id: 28,
+ name: "IT Sector",
+ tokens: 40,
+ type: 4,
+ },
+ {
+ avatar: null,
+ begin: "2023-02-15T09:00:00Z",
+ description: "Visita o stand da Continental",
+ end: "2023-02-17T17:00:00Z",
+ id: 29,
+ name: "Continental",
+ tokens: 40,
+ type: 4,
+ },
+ ],
+ course: 2,
+ cv: "https://sei24-staging.s3.amazonaws.com/uploads/attendee/cvs/f7c90ab0-aa79-4528-bff3-3ad6a811ac83/original.pdf?v=63872641922",
+ entries: 14,
+ id: "f7c90ab0-aa79-4528-bff3-3ad6a811ac83",
+ name: "attendee69",
+ nickname: "jmf",
+ prizes: [
+ {
+ avatar:
+ "https://sei24-staging.s3.amazonaws.com/uploads/prize/avatars/14/original.png?v=63870223172",
+ id: 14,
+ is_redeemable: false,
+ name: "Nada",
+ not_redeemed: 1,
+ },
+ ],
+ redeemables: [
+ {
+ id: 18,
+ image:
+ "https://sei24-staging.s3.amazonaws.com/uploads/redeemable/avatars/18/original.png?v=63870223178",
+ name: "Caneca SEI '23",
+ not_redeemed: 1,
+ price: 0,
+ quantity: 1,
+ },
+ ],
+ token_balance: 110,
+ },
+ prize: {
+ avatar:
+ "https://sei24-staging.s3.amazonaws.com/uploads/prize/avatars/14/original.png?v=63870223172",
+ id: 69,
+ is_redeemable: true,
+ name: "Air Fryer",
+ not_redeemed: 1,
+ },
+ quantity: 1
+ };
+ const fetchRedeems = (query:string , offset:number, limit:number) => [x,y,x,x,y];
+
+ return (
+
+ fetchElems={fetchRedeems}
+ showElems={(ee) =>
+ elems={ee.map(redeemDetails)}>
+ header="Attendee" getter={(e) => } />
+ header="Type" getter={(e) => displayRedeemType(e.type) } />
+ header="Redeem" getter={(e) => e.name } />
+ header="Quantity" getter={(e) => e.quantity } />
+ header="Timestamp" getter={(e) => e.timestamp } />
+
+ }
+ limit={50}
+ />
+
+ );
+}
+
+export default withAuth(RedeemHistory);
diff --git a/layout/Admin/RedeemHistory/index.tsx b/layout/Admin/RedeemHistory/index.tsx
new file mode 100644
index 00000000..237da59a
--- /dev/null
+++ b/layout/Admin/RedeemHistory/index.tsx
@@ -0,0 +1 @@
+export { default } from "./RedeemHistory";
diff --git a/layout/Admin/Spotlight/Spotlight.tsx b/layout/Admin/Spotlight/Spotlight.tsx
new file mode 100644
index 00000000..b88cf7d2
--- /dev/null
+++ b/layout/Admin/Spotlight/Spotlight.tsx
@@ -0,0 +1,12 @@
+import Layout from "@components/Layout";
+import { withAuth } from "@context/Auth";
+
+export function Spotlight() {
+ return (
+
+ TODO
+
+ );
+}
+
+export default withAuth(Spotlight);
diff --git a/layout/Admin/Spotlight/index.tsx b/layout/Admin/Spotlight/index.tsx
new file mode 100644
index 00000000..81eb3ff5
--- /dev/null
+++ b/layout/Admin/Spotlight/index.tsx
@@ -0,0 +1 @@
+export { default } from "./Spotlight";
diff --git a/layout/Attendee/Wheel/Wheel.tsx b/layout/Attendee/Wheel/Wheel.tsx
index 9f49d12b..7b306118 100644
--- a/layout/Attendee/Wheel/Wheel.tsx
+++ b/layout/Attendee/Wheel/Wheel.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, ReactNode } from "react";
import { withAuth, useAuth, IAttendee } from "@context/Auth";
import Heading from "@components/Heading";
@@ -6,8 +6,6 @@ import Button from "@components/Button";
import Layout from "@components/Layout";
import {
- ListItem3Cols,
- ListItem4Cols,
WheelComponent,
WheelMessage,
} from "./components";
@@ -20,6 +18,23 @@ import {
getWheelLatestWins,
spinWheel,
} from "@lib/api";
+import Table, { Align, TableColumn } from "@components/Table";
+import AttendeeMention from "@components/AttendeeMention";
+
+interface IPrize {
+ avatar: string;
+ id: number;
+ max_amount_per_attendee: number;
+ name: string;
+ stock: number;
+}
+
+interface IWin {
+ attendee_name: string;
+ attendee_nickname: string;
+ date: string;
+ prize: IPrize;
+}
/*
Gets how long ago the given date/time was in a user friendly way (10 seconds ago, 1 minute ago, etc)
@@ -49,6 +64,10 @@ function displayTimeSince(dateString) {
return value < 0 ? "Now" : string;
}
+function displayQuantity(qt) {
+ return qt > 1000 ? "∞" : qt;
+}
+
function WheelPage() {
const defaultState = {
angle: 0,
@@ -62,12 +81,12 @@ function WheelPage() {
refetchUser: () => void;
};
- const [prizes, updatePrizes] = useState([]);
- const [price, updatePrice] = useState(null);
- const [latestWins, updateLatestWins] = useState([]);
- const [error, updateError] = useState(false);
- const [wheelMessage, updateWheelMessage] = useState(<>>);
- const [isSpinning, setIsSpinning] = useState(false);
+ const [prizes, updatePrizes] = useState([]);
+ const [price, updatePrice] = useState(null);
+ const [latestWins, updateLatestWins] = useState([]);
+ const [error, updateError] = useState(false);
+ const [wheelMessage, updateWheelMessage] = useState(<>>);
+ const [isSpinning, setIsSpinning] = useState(false);
const requestAllInfo = () => {
getWheelPrizes()
@@ -166,25 +185,6 @@ function WheelPage() {
if (st.speed > 0) setTimeout(changeState, 1000 / 60);
}, [st]);
- const prizeComponents = prizes.map((entry, id) => (
-
- ));
- const latestWinsComponents = latestWins.map((entry, id) => (
-
- ));
return (
@@ -224,26 +224,22 @@ function WheelPage() {
-
{latestWinsComponents}
+
+ elemAlign={Align.Left} getter={(w) => } />
+ elemAlign={Align.Right} getter={(w) => } />
+ elemAlign={Align.Left} colSpan={2} getter={(w) => {w.prize.name}
} />
+ elemAlign={Align.Right} elemPadding={4} getter={(w) => {displayTimeSince(w.date)}
} />
+
-
- {prizeComponents}
+
+ header="Image" getter={(p) => } />
+ header="Name" colSpan={3} headerAlign={Align.Left} elemAlign={Align.Left} getter={(p) => p.name } />
+ header="Qt/pax" getter={(p) => displayQuantity(p.max_amount_per_attendee) } />
+ header="Qt" getter={(p) => displayQuantity(p.stock) } />
+
diff --git a/layout/Attendee/Wheel/components/ListItem3Cols/index.tsx b/layout/Attendee/Wheel/components/ListItem3Cols/index.tsx
deleted file mode 100644
index 96759f71..00000000
--- a/layout/Attendee/Wheel/components/ListItem3Cols/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import Link from "next/link";
-
-export default function ListItem3Cols({
- user_name,
- user_nickname,
- prize,
- when,
- isLast = false,
- isFullBold = false,
-}) {
- if (!isLast) {
- var border = "border-b-solid border-b-2";
- }
- if (isFullBold) {
- }
- return (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/layout/Attendee/Wheel/components/ListItem4Cols/index.tsx b/layout/Attendee/Wheel/components/ListItem4Cols/index.tsx
deleted file mode 100644
index 562aa040..00000000
--- a/layout/Attendee/Wheel/components/ListItem4Cols/index.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-export default function ListItem4Cols({
- name,
- maxQuantity,
- quantity,
- img,
- isLast = false,
-}) {
- if (!isLast) {
- var border = "border-b-solid border-b-2";
- }
- return (
-
-
-
-
-
-
-
- {maxQuantity > 1000 ? "∞" : maxQuantity}
-
-
-
-
{quantity > 1000 ? "∞" : quantity}
-
-
- );
-}
diff --git a/layout/Attendee/Wheel/components/index.ts b/layout/Attendee/Wheel/components/index.ts
index eec93c9c..b06398f2 100644
--- a/layout/Attendee/Wheel/components/index.ts
+++ b/layout/Attendee/Wheel/components/index.ts
@@ -1,6 +1,4 @@
-import ListItem3Cols from "./ListItem3Cols";
-import ListItem4Cols from "./ListItem4Cols";
import WheelComponent from "./WheelComponent";
import WheelMessage from "./WheelMessage";
-export { ListItem3Cols, ListItem4Cols, WheelComponent, WheelMessage };
+export { WheelComponent, WheelMessage };
diff --git a/layout/Login/Login.tsx b/layout/Login/Login.tsx
index 012ef8f8..3db95e7e 100644
--- a/layout/Login/Login.tsx
+++ b/layout/Login/Login.tsx
@@ -14,15 +14,29 @@ import Title from "@layout/moonstone/authentication/Title";
import Text from "@layout/moonstone/authentication/Text";
import { LoginForm } from "./components";
+function decodePathname(from : string | string[]) {
+ if (from) {
+ const pathname : string = typeof from === "string" ? from : from[0];
+ try {
+ return decodeURIComponent(pathname as string).replace(/\[|\]/i,"");
+ } catch (e) {
+ if (!(e instanceof URIError)) {
+ throw e;
+ }
+ }
+ }
+
+ return "/";
+}
+
function Login() {
const router = useRouter();
const { user } = useAuth();
if (user) {
- router.replace(
- (router.query.from && decodeURIComponent(router.query.from as string)) ??
- "/"
- );
+ router.replace({
+ pathname: decodePathname(router.query.from)
+ });
return null;
}
diff --git a/layout/shared/Leaderboard/Leaderboard.tsx b/layout/shared/Leaderboard/Leaderboard.tsx
index 03f0d747..9c9b74ec 100644
--- a/layout/shared/Leaderboard/Leaderboard.tsx
+++ b/layout/shared/Leaderboard/Leaderboard.tsx
@@ -1,9 +1,8 @@
import { useState, useEffect } from "react";
-import { withAuth, useAuth } from "@context/Auth";
+import { withAuth, useAuth, IPublicAttendee } from "@context/Auth";
import Layout from "@components/Layout";
-import { Table } from "./components";
import Button from "@components/Button";
import Day from "@components/Schedule/Day";
@@ -13,6 +12,8 @@ import { getLeaderboard } from "@lib/api";
import scheduleData from "@data/schedule.json";
import dayjs from "dayjs";
+import Table, { Align, TableColumn } from "@components/Table";
+import AttendeeMention from "@components/AttendeeMention";
function leapYear(year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
@@ -74,6 +75,16 @@ function addDate(date, days) {
return year + "/" + month + "/" + day;
}
+interface IToShow {
+ index: number;
+ id: string;
+ avatar: string | null;
+ name: string;
+ nickname: string;
+ badges: number;
+ token_balance: number;
+}
+
function Leaderboard() {
/* Fetch first and last day of the event from schedule data */
const eventDates = scheduleData.map((day) => day.date).sort();
@@ -98,7 +109,7 @@ function Leaderboard() {
const requestLeaderboard = () => {
const args = hallOfFame ? "" : date.replaceAll("/", "-");
getLeaderboard(args)
- .then((response) => updateLeaderboard(response.data))
+ .then((response) => updateLeaderboard(response.data.map((l,i) => ({index: i, ...l}))))
.catch((_) => updateError(true));
};
@@ -114,6 +125,13 @@ function Leaderboard() {
updateDate(new_date);
};
+ const maxUsers = 5;
+ const userIndex = leaderboard.findIndex((l) => l.id === user.id);
+ const toShow: IToShow[] = leaderboard.slice(0, Math.min(maxUsers, leaderboard.length)).concat(userIndex >= maxUsers ? [leaderboard[userIndex]] : []);
+ console.log(toShow);
+ console.log(userIndex);
+
+
return (
{error && }
-
+
+
+ header="Rank" headerAlign={Align.Left} elemAlign={Align.Left} elemPadding={3} getter={(e) => e.index + 1 }/>
+ header="Name" headerAlign={Align.Center} elemAlign={Align.Center} getter={(e) => }/>
+ header="Badges" headerAlign={Align.Right} elemAlign={Align.Right} elemPadding={5} getter={(e) => e.badges }/>
+
diff --git a/layout/shared/Leaderboard/components/Table/index.tsx b/layout/shared/Leaderboard/components/Table/index.tsx
deleted file mode 100644
index 29ef8f17..00000000
--- a/layout/shared/Leaderboard/components/Table/index.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import Link from "next/link";
-
-export default function Table({ list, user, maxUsersToShow }) {
- const toShow = list.slice(0, Math.min(maxUsersToShow, list.length));
-
- const rows = toShow.map((entry, id) => (
-
-
- {id + 1}
-
-
- {entry.name}
-
-
- {entry.badges}
-
-
- ));
-
- let userId = 0;
- let userBadges = 0;
- let userName = "";
- for (var i = 0; i < list.length; i++) {
- if (list[i].id == user) {
- userId = i;
- userName = list[i].name;
- userBadges = list[i].badges;
- break;
- }
- }
-
- let extraUser =
- userId < maxUsersToShow ? (
- <>>
- ) : (
-
-
- {userId + 1}
-
-
- {userName}
-
-
- {userBadges}
-
-
- );
-
- return (
-
-
-
- Rank
-
-
- Name
-
-
- Badges
-
-
-
- {rows}
- {extraUser}
-
- );
-}
diff --git a/layout/shared/Leaderboard/components/index.ts b/layout/shared/Leaderboard/components/index.ts
deleted file mode 100644
index 0084055d..00000000
--- a/layout/shared/Leaderboard/components/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Table from "./Table";
-
-export { Table };
diff --git a/pages/safira.tsx b/pages/safira.tsx
new file mode 100644
index 00000000..3f838804
--- /dev/null
+++ b/pages/safira.tsx
@@ -0,0 +1,14 @@
+function Safira() {
+ return null;
+}
+
+export async function getServerSideProps() {
+ return {
+ redirect: {
+ destination: "https://www.youtube.com/watch?v=YvrWzbp7B4g",
+ permanent: false,
+ },
+ };
+}
+
+export default Safira;
diff --git a/pages/staff/badgehistory.tsx b/pages/staff/badgehistory.tsx
new file mode 100644
index 00000000..de919921
--- /dev/null
+++ b/pages/staff/badgehistory.tsx
@@ -0,0 +1 @@
+export { default } from "@layout/Admin/BadgeHistory";
diff --git a/pages/staff/redeemhistory.tsx b/pages/staff/redeemhistory.tsx
new file mode 100644
index 00000000..2cb655db
--- /dev/null
+++ b/pages/staff/redeemhistory.tsx
@@ -0,0 +1 @@
+export { default } from "@layout/Admin/RedeemHistory";
diff --git a/pages/staff/spotlight.tsx b/pages/staff/spotlight.tsx
new file mode 100644
index 00000000..20e8e629
--- /dev/null
+++ b/pages/staff/spotlight.tsx
@@ -0,0 +1 @@
+export { default } from "@layout/Admin/Spotlight";
diff --git a/public/algoritmos.html b/public/algoritmos.html
deleted file mode 100644
index 40aa15b9..00000000
--- a/public/algoritmos.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- Um meme da SEI
-
-
-
- Não passaste a algoritmos
-
-
diff --git a/tailwind.config.js b/tailwind.config.js
index bcb0fffd..ae39d22d 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,11 +1,32 @@
const defaultTheme = require("tailwindcss/defaultTheme");
+function fromTo(start, end) {
+ return Array.from({length: end-start+1}, (_, i) => i + start)
+}
+
+function concatFlatMap(first, ...args) {
+ const list = Array.isArray(first) ? first : [first];
+ var ans = list.map((i) => i.toString());
+
+ args.forEach((a) => {
+ const list = Array.isArray(a) ? a : [a];
+ ans = list.flatMap((i) => ans.map((s) => s + i.toString()));
+ });
+
+ return ans;
+}
+
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./layout/**/*.{js,ts,jsx,tsx}",
],
+ safelist: [
+ ...concatFlatMap(["grid-cols-", "col-span-"], fromTo(1,12)),
+ ...concatFlatMap(["px-", "pl-"], [0,0.5,1,1.5,2,2.5,3,3.5,4,5,6,7,8,9,10,11,12,14,16,20,24,28,32,36,40]),
+ ...concatFlatMap(["bg-", "text-"], ["primary", "secondary", "tertiary", "quaternary", "quinary", "white"]),
+ ],
theme: {
screens: {
xs: "360px",