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 qrcode; 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)}

} /> +
-
-
-

Image

-
-
-

Name

-
-
-

Qt/pax

-
-
-

Qt

-
-
- {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 ( -
-
- -

{user_name}

- -
-
- -
-
-

{prize.name}

-
-
-

- {when} -

-
-
- ); -} 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 ( -
-
- -
-
-

{name}

-
-
-

- {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",