diff --git a/.env.example b/.env.example index 342cd8ba..c28a9263 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -NEXT_PUBLIC_ACM_API_URL="https://api.acmucsd.com/api/v2" \ No newline at end of file +NEXT_PUBLIC_ACM_API_URL="https://api.acmucsd.com/api/v2" +NEXT_PUBLIC_KLEFKI_API_URL="" +NEXT_PUBLIC_TOTP_KEY="" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index e8965a14..9b07d282 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,22 +1,10 @@ { "parser": "@typescript-eslint/parser", - "extends": [ - "airbnb", - "prettier", - "next", - "next/core-web-vitals", - "plugin:cypress/recommended" - ], - "plugins": [ - "prettier", - "node", - "@typescript-eslint", - "import", - "jsdoc" - ], + "extends": ["airbnb", "prettier", "next", "next/core-web-vitals", "plugin:cypress/recommended"], + "plugins": ["prettier", "node", "@typescript-eslint", "import", "jsdoc"], "rules": { "prettier/prettier": "error", - "no-unused-vars": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_$" }], "no-console": "error", "func-names": "off", "no-process-exit": "off", @@ -27,38 +15,28 @@ "import/extensions": "off", "no-shadow": "off", "react/require-default-props": "off", + "jsx-a11y/control-has-associated-label": "off", + "jsx-a11y/label-has-associated-control": [2, { "assert": "either" }], "jsx-a11y/anchor-is-valid": [ "error", { - "components": [ - "Link" - ], - "specialLink": [ - "to" - ] + "components": ["Link"], + "specialLink": ["to"] } ], "react/jsx-filename-extension": [ 2, { - "extensions": [ - ".js", - ".jsx", - ".ts", - ".tsx" - ] + "extensions": [".js", ".jsx", ".ts", ".tsx"] } ], "react/function-component-definition": [ 2, { - "namedComponents": [ - "arrow-function", - "function-declaration" - ], + "namedComponents": ["arrow-function", "function-declaration"], "unnamedComponents": "arrow-function" } ] }, "ignorePatterns": "src/**/*.d.ts" -} \ No newline at end of file +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4b170ba9..eeaccc6b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -- @farisashai +* @farisashai @raymosun @trevorkw7 @sheeptester @alexzhang1618 diff --git a/.github/workflows/label_ready_pr_to_merge.yml b/.github/workflows/label_ready_pr_to_merge.yml deleted file mode 100644 index 8b0e3ffc..00000000 --- a/.github/workflows/label_ready_pr_to_merge.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: 'Add Label to Fully-Reviewed PR' -on: pull_request_review -jobs: - labelWhenApproved: - runs-on: ubuntu-latest - steps: - - name: Add approval label - uses: pullreminders/label-when-approved-action@master - env: - APPROVALS: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ADD_LABEL: 'PR: Good to Merge' - REMOVE_LABEL: 'PR: Needs Review' diff --git a/next.config.js b/next.config.js index ecdbf7dc..704d5d37 100644 --- a/next.config.js +++ b/next.config.js @@ -1,10 +1,27 @@ +const env = process.env.NODE_ENV; +const isDevelopment = env !== 'production'; + /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { dirs: ['.'], }, + i18n: { + locales: ['en'], + defaultLocale: 'en', + }, images: { - domains: ['acmucsd.s3.us-west-1.amazonaws.com'], + domains: [ + 'acmucsd.s3-us-west-1.amazonaws.com', + 'acmucsd.s3.us-west-1.amazonaws.com', + // This one's for Sumeet Bansal + 'acmucsd.s3-us-west-1.amazonaws.com', + 'acmucsd-membership-portal.s3.us-west-1.amazonaws.com', + // The dev backend test data uses image URLs outside the allowlist + ...(isDevelopment + ? ['i.imgur.com', 'i.pinimg.com', 'i.etsystatic.com', 'www.google.com'] + : []), + ], }, poweredByHeader: false, trailingSlash: false, diff --git a/package.json b/package.json index 86e983c0..c17e7f92 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,12 @@ "lint:css": "stylelint \"**/*.scss\"", "lint": "yarn lint:js && yarn lint:css", "lint:fix": "yarn prettier && eslint --fix \"src/**/*.+(js|jsx|ts|tsx)\" && stylelint --fix \"**/*.scss\"", - "type-css": "yarn typed-scss-modules src/ --exportType default", + "type-css": "yarn typed-scss-modules src/ --exportType default --logLevel silent --watch", "dev": "next dev", "run": "yarn type-css & yarn lint && next dev", "build": "next build", "start": "next start", - "prod": "yarn type-css && yarn lint && next build && next start", + "prod": "next build && next start", "test": "yarn cypress run", "test:ui": "yarn cypress open" }, @@ -29,11 +29,12 @@ "@mui/material": "^5.11.6", "@next/env": "^13.2.4", "@svgr/webpack": "^6.5.1", - "axios": "^1.1.3", + "axios": "^1.6.0", "axios-middleware": "^0.3.1", "cookies-next": "^2.1.1", - "cypress": "^12.9.0", - "lodash": "^4.17.21", + "cypress": "^13.2.0", + "ics": "^3.7.2", + "luxon": "^3.3.0", "next": "^13.2.5-canary.30", "next-themes": "^0.2.1", "react": "18.2.0", @@ -42,12 +43,15 @@ "react-icons": "^4.4.0", "react-toastify": "^9.0.8", "sass": "^1.55.0", - "sharp": "^0.31.3", + "sharp": "^0.32.6", + "totp-generator": "^0.0.14", "typescript": "^4.8.4", "validator": "^13.9.0" }, "devDependencies": { + "@types/totp-generator": "^0.0.5", "@types/lodash": "^4.14.191", + "@types/luxon": "^3.3.0", "@types/node": "18.8.1", "@types/react": "18.0.21", "@types/validator": "^13.7.14", @@ -63,9 +67,10 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", + "postcss": "^8.4.31", "prettier": "^2.7.1", - "stylelint": "^14.13.0", - "stylelint-config-sass-guidelines": "^9.0.1", + "stylelint": "^15.10.1", + "stylelint-config-sass-guidelines": "^10.0.0", "stylelint-order": "^5.0.0", "typed-scss-modules": "^7.0.2" } diff --git a/public/assets/graphics/portal/raccoon-hero.svg b/public/assets/graphics/portal/raccoon-hero.svg new file mode 100644 index 00000000..80791128 --- /dev/null +++ b/public/assets/graphics/portal/raccoon-hero.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/graphics/portal/waves.svg b/public/assets/graphics/portal/waves.svg new file mode 100644 index 00000000..1cfea445 --- /dev/null +++ b/public/assets/graphics/portal/waves.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/graphics/store/hero-deco1.svg b/public/assets/graphics/store/hero-deco1.svg new file mode 100644 index 00000000..4b9740c7 --- /dev/null +++ b/public/assets/graphics/store/hero-deco1.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/assets/graphics/store/hero-deco2.svg b/public/assets/graphics/store/hero-deco2.svg new file mode 100644 index 00000000..5c4f118e --- /dev/null +++ b/public/assets/graphics/store/hero-deco2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/graphics/store/hero-photo.jpg b/public/assets/graphics/store/hero-photo.jpg new file mode 100644 index 00000000..23eaad38 Binary files /dev/null and b/public/assets/graphics/store/hero-photo.jpg differ diff --git a/public/assets/graphics/store/step1.svg b/public/assets/graphics/store/step1.svg new file mode 100644 index 00000000..dda5d8b4 --- /dev/null +++ b/public/assets/graphics/store/step1.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/graphics/store/step2.svg b/public/assets/graphics/store/step2.svg new file mode 100644 index 00000000..e322c69a --- /dev/null +++ b/public/assets/graphics/store/step2.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/graphics/store/step3.svg b/public/assets/graphics/store/step3.svg new file mode 100644 index 00000000..49b24343 --- /dev/null +++ b/public/assets/graphics/store/step3.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/graphics/store/step4.svg b/public/assets/graphics/store/step4.svg new file mode 100644 index 00000000..eddaa9a5 --- /dev/null +++ b/public/assets/graphics/store/step4.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/acm-icon.svg b/public/assets/icons/acm-icon.svg index 2bb9b531..027a701b 100644 --- a/public/assets/icons/acm-icon.svg +++ b/public/assets/icons/acm-icon.svg @@ -1,11 +1,16 @@ - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/icons/applecalendar.svg b/public/assets/icons/applecalendar.svg new file mode 100644 index 00000000..8b5326c2 --- /dev/null +++ b/public/assets/icons/applecalendar.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/assets/icons/arrow-left.svg b/public/assets/icons/arrow-left.svg new file mode 100644 index 00000000..16b35dea --- /dev/null +++ b/public/assets/icons/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/arrow-right.svg b/public/assets/icons/arrow-right.svg new file mode 100644 index 00000000..e9746fd6 --- /dev/null +++ b/public/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/back-arrow.svg b/public/assets/icons/back-arrow.svg index 7f0878fd..9248d961 100644 --- a/public/assets/icons/back-arrow.svg +++ b/public/assets/icons/back-arrow.svg @@ -1,10 +1,3 @@ - - - - - - - - - + + diff --git a/public/assets/icons/calendar-icon.svg b/public/assets/icons/calendar-icon.svg new file mode 100644 index 00000000..d9066fb4 --- /dev/null +++ b/public/assets/icons/calendar-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/check-mark.svg b/public/assets/icons/check-mark.svg new file mode 100644 index 00000000..af0c2498 --- /dev/null +++ b/public/assets/icons/check-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/close-icon.svg b/public/assets/icons/close-icon.svg new file mode 100644 index 00000000..bd525ea0 --- /dev/null +++ b/public/assets/icons/close-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/dashboard-icon.svg b/public/assets/icons/dashboard-icon.svg index 09a6a0d7..51582453 100644 --- a/public/assets/icons/dashboard-icon.svg +++ b/public/assets/icons/dashboard-icon.svg @@ -1,10 +1,5 @@ - + - + - - - - - diff --git a/public/assets/icons/devpost-icon.svg b/public/assets/icons/devpost-icon.svg new file mode 100644 index 00000000..eb07bb9e --- /dev/null +++ b/public/assets/icons/devpost-icon.svg @@ -0,0 +1,12 @@ + + + + diff --git a/public/assets/icons/discord-icon.svg b/public/assets/icons/discord-icon.svg index fa01be3f..82e2de25 100644 --- a/public/assets/icons/discord-icon.svg +++ b/public/assets/icons/discord-icon.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/public/assets/icons/download-icon.svg b/public/assets/icons/download-icon.svg new file mode 100644 index 00000000..eb73df24 --- /dev/null +++ b/public/assets/icons/download-icon.svg @@ -0,0 +1,6 @@ + + + diff --git a/public/assets/icons/dropdown-arrow-1.svg b/public/assets/icons/dropdown-arrow-1.svg index 9b60363e..6ff05730 100644 --- a/public/assets/icons/dropdown-arrow-1.svg +++ b/public/assets/icons/dropdown-arrow-1.svg @@ -1,3 +1,3 @@ - + diff --git a/public/assets/icons/dropdown-arrows.svg b/public/assets/icons/dropdown-arrows.svg new file mode 100644 index 00000000..95d4f336 --- /dev/null +++ b/public/assets/icons/dropdown-arrows.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/assets/icons/facebook-icon.svg b/public/assets/icons/facebook-icon.svg new file mode 100644 index 00000000..d3f40bca --- /dev/null +++ b/public/assets/icons/facebook-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/globe-icon.svg b/public/assets/icons/globe-icon.svg new file mode 100644 index 00000000..f4124489 --- /dev/null +++ b/public/assets/icons/globe-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/googlecalendar.svg b/public/assets/icons/googlecalendar.svg new file mode 100644 index 00000000..6b151e1b --- /dev/null +++ b/public/assets/icons/googlecalendar.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/assets/icons/home-icon.svg b/public/assets/icons/home-icon.svg new file mode 100644 index 00000000..474857b1 --- /dev/null +++ b/public/assets/icons/home-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/ig-icon.svg b/public/assets/icons/ig-icon.svg new file mode 100644 index 00000000..6fcb443d --- /dev/null +++ b/public/assets/icons/ig-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/info-icon.svg b/public/assets/icons/info-icon.svg new file mode 100644 index 00000000..0b0db8f2 --- /dev/null +++ b/public/assets/icons/info-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/instagram.svg b/public/assets/icons/instagram.svg new file mode 100644 index 00000000..c0985960 --- /dev/null +++ b/public/assets/icons/instagram.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/assets/icons/leaderboard-icon.svg b/public/assets/icons/leaderboard-icon.svg index 7e06f4f1..b8bdc0a4 100644 --- a/public/assets/icons/leaderboard-icon.svg +++ b/public/assets/icons/leaderboard-icon.svg @@ -1,10 +1,10 @@ - + - + diff --git a/public/assets/icons/link.svg b/public/assets/icons/link.svg new file mode 100644 index 00000000..25ef3ccb --- /dev/null +++ b/public/assets/icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/monitor.svg b/public/assets/icons/monitor.svg new file mode 100644 index 00000000..4dbbd58f --- /dev/null +++ b/public/assets/icons/monitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/moon.svg b/public/assets/icons/moon.svg new file mode 100644 index 00000000..7fd3e3b0 --- /dev/null +++ b/public/assets/icons/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/my-position-icon.svg b/public/assets/icons/my-position-icon.svg new file mode 100644 index 00000000..59253337 --- /dev/null +++ b/public/assets/icons/my-position-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/page-left-icon.svg b/public/assets/icons/page-left-icon.svg new file mode 100644 index 00000000..bd1bc8ec --- /dev/null +++ b/public/assets/icons/page-left-icon.svg @@ -0,0 +1,10 @@ + + + + diff --git a/public/assets/icons/page-right-icon.svg b/public/assets/icons/page-right-icon.svg new file mode 100644 index 00000000..f27659f6 --- /dev/null +++ b/public/assets/icons/page-right-icon.svg @@ -0,0 +1,10 @@ + + + + diff --git a/public/assets/icons/setting-icon.svg b/public/assets/icons/setting-icon.svg index de3d4733..5e856b40 100644 --- a/public/assets/icons/setting-icon.svg +++ b/public/assets/icons/setting-icon.svg @@ -1,3 +1,3 @@ - + diff --git a/public/assets/icons/shop-icon.svg b/public/assets/icons/shop-icon.svg index ad7ea282..3cac0e9e 100644 --- a/public/assets/icons/shop-icon.svg +++ b/public/assets/icons/shop-icon.svg @@ -1,4 +1,4 @@ - + diff --git a/public/assets/icons/sun.svg b/public/assets/icons/sun.svg new file mode 100644 index 00000000..7ec4d466 --- /dev/null +++ b/public/assets/icons/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/admin/event/EventDetailsForm/index.tsx b/src/components/admin/event/EventDetailsForm/index.tsx new file mode 100644 index 00000000..3d65f439 --- /dev/null +++ b/src/components/admin/event/EventDetailsForm/index.tsx @@ -0,0 +1,354 @@ +import EventDetailsFormItem from '@/components/admin/event/EventDetailsFormItem'; +import NotionAutofill from '@/components/admin/event/NotionAutofill'; +import { Button, Cropper } from '@/components/common'; +import { config, showToast } from '@/lib'; +import { KlefkiAPI } from '@/lib/api'; +import { AdminEventManager } from '@/lib/managers'; +import { CookieService } from '@/lib/services'; +import { FillInLater } from '@/lib/types'; +import { Event } from '@/lib/types/apiRequests'; +import { NotionEventDetails, NotionEventPreview, PublicEvent } from '@/lib/types/apiResponses'; +import { CookieType } from '@/lib/types/enums'; +import { getMessagesFromError, useObjectUrl } from '@/lib/utils'; +import { DateTime } from 'luxon'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import style from './style.module.scss'; + +interface IProps { + editing?: boolean; + defaultData?: Partial; +} + +const EventDetailsForm = (props: IProps) => { + const { editing, defaultData } = props; + const router = useRouter(); + const initialValues: Partial = { + title: defaultData?.title || '', + committee: defaultData?.committee || 'General', + location: defaultData?.location || '', + pointValue: defaultData?.pointValue || 10, + start: defaultData?.start || '', + end: defaultData?.end || '', + attendanceCode: defaultData?.attendanceCode || '', + description: defaultData?.description || '', + cover: defaultData?.cover, + uuid: defaultData?.uuid, + }; + + const { + register, + handleSubmit, + setValue, + reset, + formState: { errors }, + } = useForm({ defaultValues: initialValues }); + + const [loading, setLoading] = useState(false); + const [upcomingEvents, setUpcomingEvents] = useState([]); + + // Load in the Notion event options for NotionAutofill using Klefki. + useEffect(() => { + setLoading(true); + KlefkiAPI.getFutureEventsPreview().then(notionUpcomingEvents => { + setUpcomingEvents(notionUpcomingEvents); + setLoading(false); + }); + }, []); + + const setFieldsFromAutofill = (data: NotionEventDetails) => { + const { checkin, title, description, start, end, location, community } = data; + setValue('attendanceCode', checkin, { shouldValidate: true }); + setValue('title', title, { shouldValidate: true }); + setValue('description', description, { shouldValidate: true }); + setValue('start', DateTime.fromISO(start).toFormat("yyyy-MM-dd'T'HH:mm") ?? '', { + shouldValidate: true, + }); + setValue('end', DateTime.fromISO(end).toFormat("yyyy-MM-dd'T'HH:mm") ?? '', { + shouldValidate: true, + }); + setValue('location', location, { shouldValidate: true }); + setValue('committee', community); + }; + + const resetForm = () => reset(initialValues); + + const [selectedCover, setSelectedCover] = useState(null); + const [cover, setCover] = useState(null); + const coverUrl = useObjectUrl(cover); + + const createEvent: SubmitHandler = formData => { + if (!cover) { + showToast('Event cover is required.'); + return; + } + + setLoading(true); + const { start: isoStart, end: isoEnd, ...event } = formData; + + const start = new Date(isoStart).toISOString(); + const end = new Date(isoEnd).toISOString(); + + const AUTH_TOKEN = CookieService.getClientCookie(CookieType.ACCESS_TOKEN); + + AdminEventManager.createNewEvent({ + token: AUTH_TOKEN, + event: { + ...event, + start, + end, + }, + cover, + onSuccessCallback: event => { + setLoading(false); + showToast('Event Created Successfully!', '', [ + { + text: 'View Live Event Page', + onClick: () => router.push(`https://acmucsd.com/events/${event.uuid}`), + }, + ]); + router.push(config.admin.events.homeRoute); + }, + onFailCallback: error => { + setLoading(false); + showToast('Unable to create event', getMessagesFromError(error).join()); + }, + }); + }; + + const editEvent: SubmitHandler = formData => { + setLoading(true); + const { start: isoStart, end: isoEnd, ...event } = formData; + + const start = new Date(isoStart).toISOString(); + const end = new Date(isoEnd).toISOString(); + + const AUTH_TOKEN = CookieService.getClientCookie(CookieType.ACCESS_TOKEN); + + AdminEventManager.editEvent({ + token: AUTH_TOKEN, + uuid: defaultData?.uuid ?? '', + event: { + ...event, + start, + end, + }, + onSuccessCallback: event => { + setLoading(false); + showToast('Event Details Saved!', '', [ + { + text: 'View Event', + onClick: () => router.push(`https://acmucsd.com/events/${event.uuid}`), + }, + ]); + router.push(config.admin.events.homeRoute); + }, + onFailCallback: error => { + setLoading(false); + showToast('Unable to create event', getMessagesFromError(error).join()); + }, + }); + }; + + const deleteEvent = () => { + setLoading(true); + const AUTH_TOKEN = CookieService.getClientCookie(CookieType.ACCESS_TOKEN); + AdminEventManager.deleteEvent({ + event: initialValues.uuid ?? '', + token: AUTH_TOKEN, + onSuccessCallback: () => { + setLoading(false); + router.push(config.admin.events.homeRoute); + }, + }); + }; + + return ( +
+ + Back + + +

{editing ? 'Modify' : 'Create'} Event

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +