From ddaa90528eb37f87974bfd848bdfb7b5af6e5554 Mon Sep 17 00:00:00 2001 From: Faris Ashai Date: Wed, 3 Jan 2024 22:42:19 -0800 Subject: [PATCH 1/4] Show Admin Link in Mobile Nav --- src/components/layout/Navbar/index.tsx | 8 +++++++- src/components/layout/Navbar/style.module.scss | 15 +++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/layout/Navbar/index.tsx b/src/components/layout/Navbar/index.tsx index 5b313bff..1a3a572f 100644 --- a/src/components/layout/Navbar/index.tsx +++ b/src/components/layout/Navbar/index.tsx @@ -126,11 +126,17 @@ const Navbar = ({ user }: NavbarProps) => { Store + {isAdmin ? ( + + + Admin Settings + + ) : null}
+
-
); }; diff --git a/src/components/layout/Navbar/style.module.scss b/src/components/layout/Navbar/style.module.scss index ea15b2ec..1bc87baf 100644 --- a/src/components/layout/Navbar/style.module.scss +++ b/src/components/layout/Navbar/style.module.scss @@ -1,7 +1,5 @@ @use 'src/styles/vars.scss' as vars; -$wainbow-height: 20rem; - .header { background-color: var(--theme-background); height: vars.$header-height; @@ -119,19 +117,19 @@ $wainbow-height: 20rem; font-size: 20px; font-weight: 700; gap: 0.25rem; - height: $wainbow-height; + height: fit-content; justify-content: center; line-height: 300%; - margin-top: #{$wainbow-height * -1}; - padding: 0 1rem 1rem; + padding: 0 1rem; position: relative; - transition: margin-top 0.3s cubic-bezier(0, 0.4, 0.1, 1), background-color 0.3s ease; + transform: translateY(calc(-100% + 0.25rem)); + transition: transform 0.3s cubic-bezier(0, 0.4, 0.1, 1), background-color 0.3s ease; width: 100vw; z-index: -1; &[data-open='true'] { - margin-top: 0; + transform: translateY(0%); } .mobileNavItem { @@ -158,7 +156,8 @@ $wainbow-height: 20rem; .wainbow { background: vars.$wainbow; height: 0.25rem; - width: 100vw; + margin: 0 -1rem; + width: calc(100% + 2rem); } } From 21e222a20f93bbd7f07686c80f1f6c6e266909e6 Mon Sep 17 00:00:00 2001 From: Faris Ashai Date: Wed, 3 Jan 2024 23:33:26 -0800 Subject: [PATCH 2/4] Create all Available Actions --- src/components/auth/SignInTitle/index.tsx | 10 ++- .../auth/SignInTitle/style.module.scss | 10 +++ .../auth/SignInTitle/style.module.scss.d.ts | 1 + src/components/common/LinkButton/index.tsx | 29 ++++++ .../common/LinkButton/style.module.scss | 62 +++++++++++++ .../common/LinkButton/style.module.scss.d.ts | 9 ++ src/components/common/index.ts | 1 + src/components/layout/Navbar/index.tsx | 2 +- src/lib/config.ts | 9 ++ src/lib/services/PermissionService.ts | 32 +++---- src/pages/admin/attendance.tsx | 89 +++++++++++++++++++ src/pages/admin/event/create.tsx | 2 +- src/pages/admin/event/edit/[uuid].tsx | 2 +- src/pages/admin/event/index.tsx | 2 +- src/pages/admin/index.tsx | 73 +++++++++++++-- src/pages/admin/milestone.tsx | 74 +++++++++++++++ src/pages/admin/points.tsx | 86 ++++++++++++++++++ 17 files changed, 463 insertions(+), 30 deletions(-) create mode 100644 src/components/common/LinkButton/index.tsx create mode 100644 src/components/common/LinkButton/style.module.scss create mode 100644 src/components/common/LinkButton/style.module.scss.d.ts create mode 100644 src/pages/admin/attendance.tsx create mode 100644 src/pages/admin/milestone.tsx create mode 100644 src/pages/admin/points.tsx diff --git a/src/components/auth/SignInTitle/index.tsx b/src/components/auth/SignInTitle/index.tsx index f6b1afe4..e3a485d7 100644 --- a/src/components/auth/SignInTitle/index.tsx +++ b/src/components/auth/SignInTitle/index.tsx @@ -2,10 +2,16 @@ import styles from './style.module.scss'; interface SignInTitleProps { text: string; + description?: string; } -const SignInTitle = ({ text }: SignInTitleProps) => { - return

{text}

; +const SignInTitle = ({ text, description }: SignInTitleProps) => { + return ( + <> +

{text}

+ {description ?

{description}

: null} + + ); }; export default SignInTitle; diff --git a/src/components/auth/SignInTitle/style.module.scss b/src/components/auth/SignInTitle/style.module.scss index 862d7a68..a2fa7061 100644 --- a/src/components/auth/SignInTitle/style.module.scss +++ b/src/components/auth/SignInTitle/style.module.scss @@ -8,3 +8,13 @@ margin: 0.5rem 0; text-align: center; } + +.subtitle { + align-items: center; + color: var(--theme-text-on-background-1); + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + margin-bottom: 0.5rem; + text-align: center; +} diff --git a/src/components/auth/SignInTitle/style.module.scss.d.ts b/src/components/auth/SignInTitle/style.module.scss.d.ts index f33a52ec..b92eba38 100644 --- a/src/components/auth/SignInTitle/style.module.scss.d.ts +++ b/src/components/auth/SignInTitle/style.module.scss.d.ts @@ -1,4 +1,5 @@ export type Styles = { + subtitle: string; title: string; }; diff --git a/src/components/common/LinkButton/index.tsx b/src/components/common/LinkButton/index.tsx new file mode 100644 index 00000000..565a690b --- /dev/null +++ b/src/components/common/LinkButton/index.tsx @@ -0,0 +1,29 @@ +import type { URL } from '@/lib/types'; +import Link from 'next/link'; +import { PropsWithChildren } from 'react'; +import style from './style.module.scss'; + +interface IProps { + variant?: 'primary' | 'secondary'; + destructive?: boolean; + href: URL; + size?: 'default' | 'small'; +} + +const LinkButton = (props: PropsWithChildren) => { + const { variant = 'primary', destructive = false, href, size = 'default', children } = props; + + return ( + + {children} + + ); +}; + +export default LinkButton; diff --git a/src/components/common/LinkButton/style.module.scss b/src/components/common/LinkButton/style.module.scss new file mode 100644 index 00000000..d51131a7 --- /dev/null +++ b/src/components/common/LinkButton/style.module.scss @@ -0,0 +1,62 @@ +@use '../../../styles/vars.scss' as vars; + +.button { + align-items: center; + background-color: #62b0ff; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25); + color: #fff; + cursor: pointer; + display: flex; + height: 2rem; + justify-content: center; + padding: 4px 1rem; + transition: 0.3s ease-in-out transform; + + width: fit-content; + + &[data-size='default'] { + height: 2.5rem; + } + + &[data-size='small'] { + height: 2rem; + } + + &[data-variant='primary'] { + background-color: #62b0ff; + border: 1px solid #fff; + color: #fff; + + &[data-destructive='true'] { + background-color: #ef626c; + border: 1px solid #fff; + color: #fff; + } + } + + &[data-variant='secondary'] { + background-color: #fff; + border: 1px solid #62b0ff; + color: #62b0ff; + + &[data-destructive='true'] { + background-color: #fff; + border: 1px solid #ef626c; + color: #ef626c; + } + } + + &:hover { + transform: scale(1.04); + + &:disabled { + cursor: wait; + transform: scale(1); + } + } + + &:disabled { + background-color: vars.$disabled !important; + } +} diff --git a/src/components/common/LinkButton/style.module.scss.d.ts b/src/components/common/LinkButton/style.module.scss.d.ts new file mode 100644 index 00000000..47f26573 --- /dev/null +++ b/src/components/common/LinkButton/style.module.scss.d.ts @@ -0,0 +1,9 @@ +export type Styles = { + button: string; +}; + +export type ClassNames = keyof Styles; + +declare const styles: Styles; + +export default styles; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index b98c0c50..0a8c998b 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -2,6 +2,7 @@ export { default as Button } from './Button'; export { default as Carousel } from './Carousel'; export { default as CommunityLogo } from './CommunityLogo'; export { default as Dropdown } from './Dropdown'; +export { default as LinkButton } from './LinkButton'; export { default as PaginationControls } from './PaginationControls'; export { default as SEO } from './SEO'; export { default as Typography } from './Typography'; diff --git a/src/components/layout/Navbar/index.tsx b/src/components/layout/Navbar/index.tsx index 1a3a572f..9ed905b7 100644 --- a/src/components/layout/Navbar/index.tsx +++ b/src/components/layout/Navbar/index.tsx @@ -62,7 +62,7 @@ const Navbar = ({ user }: NavbarProps) => { ); } - const isAdmin = PermissionService.canViewAdminPage().includes(user.accessType); + const isAdmin = PermissionService.canViewAdminPage.includes(user.accessType); return (
diff --git a/src/lib/config.ts b/src/lib/config.ts index 49ea3582..588bd287 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -81,6 +81,15 @@ const config = { itemRoute: '/store/item/', admin: { homeRoute: '/admin', + awardPoints: '/admin/points', + grantPastAttendance: '/admin/attendance', + awardMilestone: '/admin/milestone', + viewResumes: '/admin/resumes', + store: { + items: '/admin/store/items', + pickupEvents: '/admin/store/pickupEvents', + homeRoute: '/admin/store', + }, events: { homeRoute: '/admin/event', editRoute: '/admin/event/edit', diff --git a/src/lib/services/PermissionService.ts b/src/lib/services/PermissionService.ts index 768bb676..29b40d55 100644 --- a/src/lib/services/PermissionService.ts +++ b/src/lib/services/PermissionService.ts @@ -3,30 +3,26 @@ import { UserAccessType } from '@/lib/types/enums'; /** * Wrapper class to manage permissions by helping setting restrictions and validating permissions across the application */ -export const canEditMerchItems = (): UserAccessType[] => { - return [UserAccessType.ADMIN, UserAccessType.MERCH_STORE_MANAGER]; -}; +export const canEditMerchItems = [UserAccessType.ADMIN, UserAccessType.MERCH_STORE_MANAGER]; -export const canManageEvents = (): UserAccessType[] => { - return [UserAccessType.ADMIN, UserAccessType.MARKETING]; -}; +export const canManageEvents = [UserAccessType.ADMIN, UserAccessType.MARKETING]; -export const canViewAdminPage = (): UserAccessType[] => { - return [ - UserAccessType.ADMIN, - UserAccessType.MARKETING, - UserAccessType.MERCH_STORE_MANAGER, - UserAccessType.MERCH_STORE_DISTRIBUTOR, - ]; -}; +export const canAwardPoints = [UserAccessType.ADMIN]; + +// will add sponsorship role here soon +export const canViewResumes = [UserAccessType.ADMIN]; + +export const canViewAdminPage = [ + UserAccessType.ADMIN, + UserAccessType.MARKETING, + UserAccessType.MERCH_STORE_MANAGER, + UserAccessType.MERCH_STORE_DISTRIBUTOR, +]; /** * @returns Array of all possible user access types */ -export const allUserTypes = (): UserAccessType[] => { - const values = Object.values(UserAccessType) as UserAccessType[]; - return values; -}; +export const allUserTypes = () => Object.values(UserAccessType) as UserAccessType[]; /** * @param types to exclude from array diff --git a/src/pages/admin/attendance.tsx b/src/pages/admin/attendance.tsx new file mode 100644 index 00000000..6ea8ea12 --- /dev/null +++ b/src/pages/admin/attendance.tsx @@ -0,0 +1,89 @@ +import { SignInButton, SignInFormItem, SignInTitle } from '@/components/auth'; +import { VerticalForm } from '@/components/common'; +import { config } from '@/lib'; +import withAccessType from '@/lib/hoc/withAccessType'; +import { PermissionService, ValidationService } from '@/lib/services'; +import type { GetServerSideProps, NextPage } from 'next'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { AiOutlineMail } from 'react-icons/ai'; +import { VscLock } from 'react-icons/vsc'; + +interface FormValues { + email: string; + description: string; + points: number; +} +const AwardPointsPage: NextPage = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const onSubmit: SubmitHandler = () => { + // TODO + }; + + return ( + + + } + element="input" + name="email" + type="email" + placeholder="User Email (user@ucsd.edu)" + formRegister={register('email', { + validate: email => { + const validation = ValidationService.isValidEmail(email); + return validation.valid || validation.error; + }, + })} + error={errors.email} + /> + } + element="input" + name="description" + type="text" + placeholder="Description" + formRegister={register('description', { + required: 'Required', + })} + error={errors.description} + /> + } + name="points" + element="input" + type="number" + placeholder="Point Value" + formRegister={register('points', { + required: 'Required', + })} + error={errors.points} + /> + + + ); +}; + +export default AwardPointsPage; + +const getServerSidePropsFunc: GetServerSideProps = async () => ({ + props: {}, +}); + +export const getServerSideProps = withAccessType( + getServerSidePropsFunc, + PermissionService.canAwardPoints, + config.admin.homeRoute +); diff --git a/src/pages/admin/event/create.tsx b/src/pages/admin/event/create.tsx index 28e5ecf5..35f3e5f1 100644 --- a/src/pages/admin/event/create.tsx +++ b/src/pages/admin/event/create.tsx @@ -31,6 +31,6 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ req, res, query }) = export const getServerSideProps = withAccessType( getServerSidePropsFunc, - PermissionService.canManageEvents(), + PermissionService.canManageEvents, config.admin.homeRoute ); diff --git a/src/pages/admin/event/edit/[uuid].tsx b/src/pages/admin/event/edit/[uuid].tsx index 8ebf06ea..baac34ab 100644 --- a/src/pages/admin/event/edit/[uuid].tsx +++ b/src/pages/admin/event/edit/[uuid].tsx @@ -43,6 +43,6 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) export const getServerSideProps = withAccessType( getServerSidePropsFunc, - PermissionService.canManageEvents(), + PermissionService.canManageEvents, config.admin.homeRoute ); diff --git a/src/pages/admin/event/index.tsx b/src/pages/admin/event/index.tsx index 73dca979..c5e406cc 100644 --- a/src/pages/admin/event/index.tsx +++ b/src/pages/admin/event/index.tsx @@ -93,6 +93,6 @@ const getServerSidePropsFunc: GetServerSideProps = async () => { export const getServerSideProps = withAccessType( getServerSidePropsFunc, - PermissionService.canManageEvents(), + PermissionService.canManageEvents, config.admin.homeRoute ); diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index a7e1287d..b7f987c8 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -1,14 +1,75 @@ +import { LinkButton, Typography } from '@/components/common'; import { config } from '@/lib'; import withAccessType from '@/lib/hoc/withAccessType'; import { PermissionService } from '@/lib/services'; -import { GetServerSideProps, NextPage } from 'next'; -import Link from 'next/link'; +import type { PrivateProfile } from '@/lib/types/apiResponses'; +import type { GetServerSideProps } from 'next'; -const AdminPage: NextPage = () => { +interface AdminProps { + user: PrivateProfile; +} + +const AdminPage = ({ user: { accessType } }: AdminProps) => { return (
-

Portal Admin Page

- Manage Events + Admin Actions +
+ Events +
+ Manage Events +
+
+ Store +
+ Manage Store Merchandise + Manage Pickup Events +
+
+ User Points +
+ {PermissionService.canAwardPoints.includes(accessType) ? ( + <> + Award Bonus Points + Grant Past Attendance + Create Portal Milestone + + ) : ( + 'Restricted Access' + )} +
+ + User Accounts +
+ {PermissionService.canViewResumes.includes(accessType) ? ( + View User Resumes + ) : ( + 'Restricted Access' + )} +
); }; @@ -23,6 +84,6 @@ const getServerSidePropsFunc: GetServerSideProps = async () => { export const getServerSideProps = withAccessType( getServerSidePropsFunc, - PermissionService.canViewAdminPage(), + PermissionService.canViewAdminPage, config.homeRoute ); diff --git a/src/pages/admin/milestone.tsx b/src/pages/admin/milestone.tsx new file mode 100644 index 00000000..cebd646b --- /dev/null +++ b/src/pages/admin/milestone.tsx @@ -0,0 +1,74 @@ +import { SignInButton, SignInFormItem, SignInTitle } from '@/components/auth'; +import { VerticalForm } from '@/components/common'; +import { config } from '@/lib'; +import withAccessType from '@/lib/hoc/withAccessType'; +import { PermissionService } from '@/lib/services'; +import type { GetServerSideProps, NextPage } from 'next'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { AiOutlineMail } from 'react-icons/ai'; +import { VscLock } from 'react-icons/vsc'; + +interface FormValues { + name: string; + points: number; +} +const AwardPointsPage: NextPage = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const onSubmit: SubmitHandler = () => { + // TODO + }; + + return ( + + + } + element="input" + name="name" + type="text" + placeholder="Milestone Name" + formRegister={register('name', { + required: 'Required', + })} + error={errors.name} + /> + } + name="points" + element="input" + type="number" + placeholder="Point Value" + formRegister={register('points', { + required: 'Required', + })} + error={errors.points} + /> + + + ); +}; + +export default AwardPointsPage; + +const getServerSidePropsFunc: GetServerSideProps = async () => ({ + props: {}, +}); + +export const getServerSideProps = withAccessType( + getServerSidePropsFunc, + PermissionService.canAwardPoints, + config.admin.homeRoute +); diff --git a/src/pages/admin/points.tsx b/src/pages/admin/points.tsx new file mode 100644 index 00000000..afab8a93 --- /dev/null +++ b/src/pages/admin/points.tsx @@ -0,0 +1,86 @@ +import { SignInButton, SignInFormItem, SignInTitle } from '@/components/auth'; +import { VerticalForm } from '@/components/common'; +import { config } from '@/lib'; +import withAccessType from '@/lib/hoc/withAccessType'; +import { PermissionService, ValidationService } from '@/lib/services'; +import type { GetServerSideProps, NextPage } from 'next'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { AiOutlineMail } from 'react-icons/ai'; +import { VscLock } from 'react-icons/vsc'; + +interface FormValues { + email: string; + description: string; + points: number; +} +const AwardPointsPage: NextPage = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const onSubmit: SubmitHandler = () => { + // TODO + }; + + return ( + + + } + element="input" + name="email" + type="email" + placeholder="User Email (user@ucsd.edu)" + formRegister={register('email', { + validate: email => { + const validation = ValidationService.isValidEmail(email); + return validation.valid || validation.error; + }, + })} + error={errors.email} + /> + } + element="input" + name="description" + type="text" + placeholder="Description" + formRegister={register('description', { + required: 'Required', + })} + error={errors.description} + /> + } + name="points" + element="input" + type="number" + placeholder="Point Value" + formRegister={register('points', { + required: 'Required', + })} + error={errors.points} + /> + + + ); +}; + +export default AwardPointsPage; + +const getServerSidePropsFunc: GetServerSideProps = async () => ({ + props: {}, +}); + +export const getServerSideProps = withAccessType( + getServerSidePropsFunc, + PermissionService.canAwardPoints, + config.admin.homeRoute +); From 1f7518ef220ea2b29c0c7356e12c4b9498d13a27 Mon Sep 17 00:00:00 2001 From: Faris Ashai Date: Thu, 4 Jan 2024 22:00:45 -0800 Subject: [PATCH 3/4] Delete admin store page --- src/pages/store/admin.tsx | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/pages/store/admin.tsx diff --git a/src/pages/store/admin.tsx b/src/pages/store/admin.tsx deleted file mode 100644 index 74302e20..00000000 --- a/src/pages/store/admin.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import withAccessType from '@/lib/hoc/withAccessType'; -import { PermissionService } from '@/lib/services'; -import type { GetServerSideProps } from 'next'; - -const StoreAdminPage = () => { - return

Store Admin Page

; -}; - -export default StoreAdminPage; - -const getServerSidePropsFunc: GetServerSideProps = async () => ({ - props: {}, -}); - -export const getServerSideProps = withAccessType( - getServerSidePropsFunc, - PermissionService.allUserTypes() -); From a9c6dd70ec241faeb1014f729c31ddd6ef252cd7 Mon Sep 17 00:00:00 2001 From: Faris Ashai Date: Sat, 6 Jan 2024 20:19:03 -0800 Subject: [PATCH 4/4] Update src/pages/admin/milestone.tsx Co-authored-by: Alex Zhang --- src/pages/admin/milestone.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/admin/milestone.tsx b/src/pages/admin/milestone.tsx index cebd646b..89e632cc 100644 --- a/src/pages/admin/milestone.tsx +++ b/src/pages/admin/milestone.tsx @@ -27,7 +27,7 @@ const AwardPointsPage: NextPage = () => { }