diff --git a/.eslintrc.json b/.eslintrc.json index 9b07d282..74ecd12f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -38,5 +38,10 @@ } ] }, - "ignorePatterns": "src/**/*.d.ts" + "ignorePatterns": [ + "src/**/*.d.ts", + "**/public/sw.js", + "**/public/workbox-*.js", + "**/public/worker-*.js" + ] } diff --git a/.gitignore b/.gitignore index b947a678..885edd4c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,11 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# PWA files +**/public/sw.js +**/public/workbox-*.js +**/public/worker-*.js +**/public/sw.js.map +**/public/workbox-*.js.map +**/public/worker-*.js.map \ No newline at end of file diff --git a/next.config.js b/next.config.js index 704d5d37..bb725fef 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,13 @@ const env = process.env.NODE_ENV; const isDevelopment = env !== 'production'; +const withPWA = require('next-pwa')({ + dest: 'public', + register: true, + skipWaiting: true, + disable: isDevelopment, +}); + /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { @@ -37,4 +44,4 @@ const nextConfig = { }, }; -module.exports = nextConfig; +module.exports = withPWA(nextConfig); diff --git a/package.json b/package.json index d3480b4e..1247b198 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "ics": "^3.7.2", "luxon": "^3.3.0", "next": "^13.2.5-canary.30", + "next-pwa": "^5.6.0", "next-themes": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/public/icon-192x192.png b/public/icon-192x192.png new file mode 100644 index 00000000..749be3be Binary files /dev/null and b/public/icon-192x192.png differ diff --git a/public/icon-256x256.png b/public/icon-256x256.png new file mode 100644 index 00000000..fe18f759 Binary files /dev/null and b/public/icon-256x256.png differ diff --git a/public/icon-384x384.png b/public/icon-384x384.png new file mode 100644 index 00000000..f7ff182a Binary files /dev/null and b/public/icon-384x384.png differ diff --git a/public/icon-512x512.png b/public/icon-512x512.png new file mode 100644 index 00000000..1c7ad55d Binary files /dev/null and b/public/icon-512x512.png differ diff --git a/public/manifest.json b/public/manifest.json index 95e7adcc..3566a7f2 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,15 +1,32 @@ { - "short_name": "ACM Membership Portal", - "name": "Association for Computing Machinery at UC San Diego Student Membership Portal", + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone", + "scope": "/", + "start_url": "/", + "name": "ACM at UCSD Membership Portal", + "short_name": "ACM Portal", + "description": "Association for Computing Machinery at UC San Diego Student Membership Portal", "icons": [ { - "src": "favicon.ico", - "sizes": "64x64 32x32 16x16", - "type": "image/x-icon" + "src": "/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" + ] } diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx index 92a4c3a8..600a4d2c 100644 --- a/src/components/common/Carousel/index.tsx +++ b/src/components/common/Carousel/index.tsx @@ -1,11 +1,7 @@ -import type { ReactNode } from 'react'; +import type { PropsWithChildren } from 'react'; import styles from './style.module.scss'; -interface CarouselProps { - children: ReactNode[]; -} - -const Carousel = ({ children }: CarouselProps) => { +const Carousel = ({ children }: PropsWithChildren) => { return
{children}
; }; diff --git a/src/components/common/CommunityLogo/index.tsx b/src/components/common/CommunityLogo/index.tsx index 7338220c..205ca002 100644 --- a/src/components/common/CommunityLogo/index.tsx +++ b/src/components/common/CommunityLogo/index.tsx @@ -1,29 +1,22 @@ +import { communityLogos } from '@/lib/constants/communityLogos'; +import { Community } from '@/lib/types/enums'; +import { capitalize } from '@/lib/utils'; import Image from 'next/image'; -import AILogo from '@/public/assets/acm-logos/communities/ai.png'; -import CyberLogo from '@/public/assets/acm-logos/communities/cyber.png'; -import DesignLogo from '@/public/assets/acm-logos/communities/design.png'; -import HackLogo from '@/public/assets/acm-logos/communities/hack.png'; -import ACMLogo from '@/public/assets/acm-logos/general/light-mode.png'; - interface CommunityLogoProps { community: string; size: number; } const CommunityLogo = ({ community, size }: CommunityLogoProps) => { - switch (community.toLowerCase()) { - case 'hack': - return ACM Hack Logo; - case 'ai': - return ACM AI Logo; - case 'cyber': - return ACM Cyber Logo; - case 'design': - return ACM Design Logo; - default: - return ACM General Logo; - } + const formattedName = capitalize(community) as Community; + + if (!Object.values(Community).includes(formattedName)) + return ACM General Logo; + + return ( + {`ACM + ); }; export default CommunityLogo; diff --git a/src/components/common/Dropdown/index.tsx b/src/components/common/Dropdown/index.tsx index fbe4d317..68703e65 100644 --- a/src/components/common/Dropdown/index.tsx +++ b/src/components/common/Dropdown/index.tsx @@ -7,10 +7,12 @@ interface Option { label: string; } +export const DIVIDER = '----'; + interface DropdownProps { name: string; ariaLabel: string; - options: (Option | '---')[]; + options: (Option | typeof DIVIDER)[]; value: string; onChange: (value: string) => void; } @@ -30,7 +32,7 @@ const Dropdown = ({ name, ariaLabel, options, value, onChange }: DropdownProps) const optionButtons: ReactNode[] = []; let dividers = 0; options.forEach(option => { - if (option === '---') { + if (option === DIVIDER) { optionButtons.push(
); dividers += 1; } else { @@ -78,7 +80,7 @@ const Dropdown = ({ name, ariaLabel, options, value, onChange }: DropdownProps) aria-label={ariaLabel} > {options.map(option => - option !== '---' ? ( + option !== DIVIDER ? ( diff --git a/src/components/events/EventDisplay/style.module.scss b/src/components/events/EventDisplay/style.module.scss index 641eb607..50cadbe5 100644 --- a/src/components/events/EventDisplay/style.module.scss +++ b/src/components/events/EventDisplay/style.module.scss @@ -1,5 +1,7 @@ .container { - display: flex; - flex-wrap: wrap; - gap: 1rem; + column-gap: 1rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + place-items: center; + row-gap: 2rem; } diff --git a/src/components/home/Hero/style.module.scss b/src/components/home/Hero/style.module.scss index 77973d2c..974f55a3 100644 --- a/src/components/home/Hero/style.module.scss +++ b/src/components/home/Hero/style.module.scss @@ -132,6 +132,7 @@ } .image { + margin: 0 auto; max-height: 1080px; max-width: 1920px; object-fit: cover; diff --git a/src/components/layout/Navbar/index.tsx b/src/components/layout/Navbar/index.tsx index e073b796..10bb55f3 100644 --- a/src/components/layout/Navbar/index.tsx +++ b/src/components/layout/Navbar/index.tsx @@ -2,7 +2,7 @@ import ThemeToggle from '@/components/common/ThemeToggle'; import { config } from '@/lib'; import { useWindowSize } from '@/lib/hooks/useWindowSize'; import { PermissionService } from '@/lib/services'; -import type { PrivateProfile } from '@/lib/types/apiResponses'; +import { UserAccessType } from '@/lib/types/enums'; import LightModeLogo from '@/public/assets/acm-logos/general/light-mode.png'; import CalendarIcon from '@/public/assets/icons/calendar-icon.svg'; import HomeIcon from '@/public/assets/icons/home-icon.svg'; @@ -16,9 +16,9 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react'; import styles from './style.module.scss'; interface NavbarProps { - user?: PrivateProfile; + accessType?: UserAccessType; } -const Navbar = ({ user }: NavbarProps) => { +const Navbar = ({ accessType }: NavbarProps) => { const size = useWindowSize(); const headerRef = useRef(null); @@ -47,7 +47,7 @@ const Navbar = ({ user }: NavbarProps) => { if (!isMobile) setMenuOpen(false); }, [isMobile]); - if (!user) { + if (!accessType) { return (
@@ -62,7 +62,7 @@ const Navbar = ({ user }: NavbarProps) => { ); } - const isAdmin = PermissionService.canViewAdminPage.includes(user.accessType); + const isAdmin = PermissionService.canViewAdminPage.includes(accessType); return (
diff --git a/src/components/layout/Navbar/style.module.scss b/src/components/layout/Navbar/style.module.scss index 3f9663d0..ac7fb7d5 100644 --- a/src/components/layout/Navbar/style.module.scss +++ b/src/components/layout/Navbar/style.module.scss @@ -164,7 +164,7 @@ background: vars.$wainbow; height: 0.25rem; margin: 0 -1rem; - width: calc(100% + 2rem); + width: 100vw; } } diff --git a/src/components/layout/PageLayout/index.tsx b/src/components/layout/PageLayout/index.tsx index 91b93dd1..c905fcde 100644 --- a/src/components/layout/PageLayout/index.tsx +++ b/src/components/layout/PageLayout/index.tsx @@ -1,15 +1,15 @@ import Navbar from '@/components/layout/Navbar'; -import type { PrivateProfile } from '@/lib/types/apiResponses'; +import { UserAccessType } from '@/lib/types/enums'; import { PropsWithChildren } from 'react'; import styles from './style.module.scss'; interface LayoutProps { - user?: PrivateProfile; + accessType?: UserAccessType; } -const PageLayout = ({ user, children }: PropsWithChildren) => ( +const PageLayout = ({ accessType, children }: PropsWithChildren) => ( <> - +
{children}
); diff --git a/src/components/store/HelpModal/index.tsx b/src/components/store/HelpModal/index.tsx index 92fae155..d61f7b4f 100644 --- a/src/components/store/HelpModal/index.tsx +++ b/src/components/store/HelpModal/index.tsx @@ -5,14 +5,14 @@ import Step3 from '@/public/assets/graphics/store/step3.svg'; import Step4 from '@/public/assets/graphics/store/step4.svg'; import ArrowLeftIcon from '@/public/assets/icons/arrow-left.svg'; import ArrowRightIcon from '@/public/assets/icons/arrow-right.svg'; -import { ReactNode } from 'react'; +import type { PropsWithChildren } from 'react'; import styles from './style.module.scss'; interface StepProps { step: number; - children?: ReactNode; } -const Step = ({ step, children }: StepProps) => { + +const Step = ({ step, children }: PropsWithChildren) => { const Left = step > 1 ? 'a' : 'span'; const Right = step < 4 ? 'a' : 'span'; return ( diff --git a/src/lib/constants/communityLogos.ts b/src/lib/constants/communityLogos.ts new file mode 100644 index 00000000..10feb827 --- /dev/null +++ b/src/lib/constants/communityLogos.ts @@ -0,0 +1,16 @@ +/* eslint-disable import/prefer-default-export */ +import { Community } from '@/lib/types/enums'; +import AILogo from '@/public/assets/acm-logos/communities/ai.png'; +import CyberLogo from '@/public/assets/acm-logos/communities/cyber.png'; +import DesignLogo from '@/public/assets/acm-logos/communities/design.png'; +import HackLogo from '@/public/assets/acm-logos/communities/hack.png'; +import ACMLogo from '@/public/assets/acm-logos/general/light-mode.png'; +import type { StaticImageData } from 'next/image'; + +export const communityLogos: Record = { + AI: AILogo, + Cyber: CyberLogo, + Design: DesignLogo, + Hack: HackLogo, + General: ACMLogo, +}; diff --git a/src/lib/services/CookieService.ts b/src/lib/services/CookieService.ts index 9a58e7e5..2c48e8c9 100644 --- a/src/lib/services/CookieService.ts +++ b/src/lib/services/CookieService.ts @@ -1,3 +1,4 @@ +import { CookieType } from '@/lib/types/enums'; import { deleteCookie, getCookie, setCookie } from 'cookies-next'; import type { OptionsType } from 'cookies-next/lib/types'; @@ -33,3 +34,11 @@ export const deleteClientCookie = (key: string): void => { export const deleteServerCookie = (key: string, options: OptionsType): void => { deleteCookie(key, options); }; + +export const clearClientCookies = (): void => { + Object.keys(CookieType).forEach(key => deleteClientCookie(key)); +}; + +export const clearServerCookies = (options: OptionsType): void => { + Object.keys(CookieType).forEach(key => deleteServerCookie(key, options)); +}; diff --git a/src/lib/services/PermissionService.ts b/src/lib/services/PermissionService.ts index 29b40d55..42d74d70 100644 --- a/src/lib/services/PermissionService.ts +++ b/src/lib/services/PermissionService.ts @@ -22,7 +22,7 @@ export const canViewAdminPage = [ /** * @returns Array of all possible user access types */ -export const allUserTypes = () => Object.values(UserAccessType) as UserAccessType[]; +export const allUserTypes = Object.values(UserAccessType); /** * @param types to exclude from array @@ -32,3 +32,8 @@ export const allUserTypesExcept = (types: UserAccessType[]): UserAccessType[] => const values = Object.keys(UserAccessType) as UserAccessType[]; return values.filter(value => !types.includes(value)); }; + +/** + * @returns Valid logged in user types + */ +export const loggedInUser = allUserTypesExcept([UserAccessType.RESTRICTED]); diff --git a/src/lib/types/enums.ts b/src/lib/types/enums.ts index c0794cac..0f21d6b6 100644 --- a/src/lib/types/enums.ts +++ b/src/lib/types/enums.ts @@ -3,6 +3,14 @@ export enum CookieType { ACCESS_TOKEN = 'ACCESS_TOKEN', } +export enum Community { + HACK = 'Hack', + AI = 'AI', + CYBER = 'Cyber', + DESIGN = 'Design', + GENERAL = 'General', +} + export enum UserAccessType { RESTRICTED = 'RESTRICTED', STANDARD = 'STANDARD', diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 4d0dd14d..2ebf498e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -236,14 +236,17 @@ export const getDateRange = (sort: string | number) => { } case 'past-week': { from = now - DAY_SECONDS * 7; + to = now; break; } case 'past-month': { from = now - DAY_SECONDS * 28; + to = now; break; } case 'past-year': { from = now - DAY_SECONDS * 365; + to = now; break; } case 'all-time': { diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 820dd6b8..3951b70e 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,24 +1,12 @@ import { SignInButton } from '@/components/auth'; import { VerticalForm } from '@/components/common'; -import { Navbar } from '@/components/store'; -import { config } from '@/lib'; -import { PrivateProfile } from '@/lib/types/apiResponses'; import Cat404 from '@/public/assets/graphics/cat404.png'; import styles from '@/styles/pages/404.module.scss'; import Image from 'next/image'; -import { useRouter } from 'next/router'; -interface PageNotFoundProps { - user?: PrivateProfile; -} - -const PageNotFound = ({ user }: PageNotFoundProps) => { - const router = useRouter(); +const PageNotFound = () => { return (
- {user && router.asPath.startsWith(config.storeRoute) && ( - - )}

Whoops, we ended up on the wrong page!

Sad Cat diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a32900d9..a816cc0d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -6,37 +6,14 @@ import 'react-toastify/dist/ReactToastify.css'; import { SEO } from '@/components/common'; import { PageLayout } from '@/components/layout'; -import { CookieService } from '@/lib/services'; -import type { PrivateProfile } from '@/lib/types/apiResponses'; -import { CookieType } from '@/lib/types/enums'; -import { NextPageContext } from 'next'; import { ThemeProvider } from 'next-themes'; import type { AppProps } from 'next/app'; import { DM_Sans as DMSans } from 'next/font/google'; -import { useEffect, useState } from 'react'; import { ToastContainer } from 'react-toastify'; -interface InitialPropInterface { - user?: PrivateProfile; -} const dmSans = DMSans({ subsets: ['latin'], weight: ['400', '500', '700'] }); -export default function MyApp({ Component, pageProps }: AppProps) { - const [user, setUser] = useState(pageProps?.user); - - // For 404 page: Try getting user from cookie on client side (because 404 - // pages in Next.js must be a static page) - useEffect(() => { - if (pageProps?.user) { - setUser(pageProps?.user); - return; - } - const userCookie = CookieService.getClientCookie(CookieType.USER); - if (userCookie) { - setUser(JSON.parse(userCookie)); - } - }, [pageProps?.user]); - +export default function MyApp({ Component, pageProps }: AppProps) { return ( <>