diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 58fd786b..545ffcd6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -27,6 +27,7 @@ jobs: SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} DIRECTORY_SPREADSHEET_ID: ${{ secrets.DIRECTORY_SPREADSHEET_ID }} TOWNHALL_SPREADSHEET_ID: ${{ secrets.TOWNHALL_SPREADSHEET_ID }} + GM_SPREADSHEET_ID: ${{ secrets.GM_SPREADSHEET_ID }} steps: - uses: actions/checkout@v2 @@ -42,4 +43,6 @@ jobs: - run: npm run thp + - run: npm run gmp + - run: npm run lint diff --git a/.github/workflows/node-build.yml b/.github/workflows/node-build.yml index bee51ce7..1fe86625 100644 --- a/.github/workflows/node-build.yml +++ b/.github/workflows/node-build.yml @@ -27,6 +27,7 @@ jobs: SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} DIRECTORY_SPREADSHEET_ID: ${{ secrets.DIRECTORY_SPREADSHEET_ID }} TOWNHALL_SPREADSHEET_ID: ${{ secrets.TOWNHALL_SPREADSHEET_ID }} + GM_SPREADSHEET_ID: ${{ secrets.GM_SPREADSHEET_ID }} steps: - uses: actions/checkout@v2 @@ -42,6 +43,8 @@ jobs: - run: npm run thp + - run: npm run gmp + - run: npm run build - run: npm test diff --git a/.gitignore b/.gitignore index bfe68405..177a41d9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ output.json offoutput.json townhall.json past-townhall.json +gmData.json diff --git a/components/Footer.js b/components/Footer.js index c459b183..137cb921 100644 --- a/components/Footer.js +++ b/components/Footer.js @@ -7,7 +7,7 @@ import SocialMedia from './SocialMedia'; const footerACMLinks = [ { title: 'About', path: '/about' }, { title: 'Events', path: '/events' }, - { title: 'General Meeting', path: '/gm/w24' }, + { title: 'General Meeting', path: '/gm' }, { title: 'CS Town Hall', path: '/town-hall' }, { title: 'Internship Program', path: '/internship' }, { title: 'Dev Team', path: '/dev'}, diff --git a/netlify.toml b/netlify.toml index 9697970a..aaee1b45 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,5 +1,5 @@ [build] - command = "npm run ofp; npm run thp; npm run build" + command = "npm run ofp; npm run thp; npm run gmp; npm run build" publish = "build" [[plugins]] diff --git a/package.json b/package.json index af38bacd..11cdb009 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "scripts": { "start": "next dev", "start-production": "next start", - "build": "npm run ofp && npm run thp && next build", + "build": "npm run ofp && npm run thp && npm run gmp && next build", "lint-js": "eslint \"**/*.js\"", "lint-js-fix": "eslint --fix \"**/*.js\"", "lint-css": "stylelint \"**/*.css\" \"**/*.scss\"", @@ -35,6 +35,7 @@ "seg": "node scripts/single-event-generator.mjs", "ofp": "node scripts/officer-parser.mjs", "thp": "node scripts/town-hall-generator.mjs", + "gmp": "node scripts/gm-generator.mjs", "test": "jest --env=jsdom" }, "eslintConfig": { diff --git a/pages/gm.js b/pages/gm.js new file mode 100644 index 00000000..0b214e50 --- /dev/null +++ b/pages/gm.js @@ -0,0 +1,280 @@ +import { NextSeo } from 'next-seo'; +import Image from 'next/image'; +import Link from 'next/link'; +import React from 'react'; +import Countdown from 'react-countdown'; + +import Banner from '../components/Banner'; +import Layout from '../components/Layout'; +import gmData from '../gmData.json'; + +import aiLogo from '../public/images/committees/ai_wordmark.svg'; +import boardLogo from '../public/images/committees/board_wordmark.svg'; +import cyberLogo from '../public/images/committees/cyber_wordmark.svg'; +import designLogo from '../public/images/committees/design_wordmark.svg'; +import hackLogo from '../public/images/committees/hack_wordmark.svg'; +import icpcLogo from '../public/images/committees/icpc_wordmark.svg'; +import studioLogo from '../public/images/committees/studio_wordmark.svg'; +import teachlaLogo from '../public/images/committees/teachLA_wordmark.svg'; +import wLogo from '../public/images/committees/w_wordmark.svg'; +import googleSlideLogo from '../public/images/slides.png'; +import winterGMgraphic from '../public/images/Winter_GM_2024_graphic.png'; + + +const dayToName = (day) => { + switch (day) { + case 0: + return 'Sunday'; + case 1: + return 'Monday'; + case 2: + return 'Tuesday'; + case 3: + return 'Wednesday'; + case 4: + return 'Thursday'; + case 5: + return 'Friday'; + default: + return 'Saturday'; + } +}; + +const getDateWithSuffix = (date) => { + let suffix = ''; + switch (date % 10) { + case 1: + suffix = 'st'; + break; + case 2: + suffix = 'nd'; + break; + case 3: + suffix = 'rd'; + break; + default: + suffix = 'th'; + } + return date.toString() + suffix; +}; + +const calculateTimeStrings = ({days, hours, minutes, seconds}) => { + let dayString = 'Day'; + let hourString = 'Hour'; + let minuteString = 'Minute'; + let secondString = 'Second'; + if(days !== 1){ dayString += 's'; } + if(hours !== 1){ hourString += 's'; } + if(minutes !== 1){ minuteString += 's'; } + if(seconds !== 1){ secondString += 's'; } + + return {dayString, hourString, minuteString, secondString}; +}; + +const parseGMData = (jsonContent) => { + const data = {}; + + for (const row of jsonContent) { + data[row.name] = row.description; + } + + const startTime = new Date(data?.gm_start_time); + + return { + gm_start_time: startTime, + gm_end_time: new Date(data?.gm_end_time), + rsvp_link: data?.rsvp_link, + quarter: data?.quarter, + slides_link: data?.slides_link, + location: data?.location, + day_of_week: dayToName(startTime.getDay()), + date_with_suffix: getDateWithSuffix(startTime.getDate()), + pres: data?.pres, + ivp: data?.ivp, + evp: data?.evp, + studio: data?.studio, + icpc: data?.icpc, + design: data?.design, + cyber: data?.cyber, + teachLA: data?.teachLA, + w: data?.w, + ai: data?.ai, + hack: data?.hack, + initiatives: data?.initiatives.split(';'), + }; +}; + +const GMCountdown = (props) => { + return ( + <> +
+
+
+
{props.days}
+
{props.dayString}
+
+
+
+
{props.hours}
+
{props.hourString}
+
+
+
+
{props.minutes}
+
{props.minuteString}
+
+
+
+
{props.seconds}
+
{props.secondString}
+
+
+
+

{props.data.quarter} {props.data.gm_start_time.getFullYear()} General Meeting

+ + + RSVP Now! + + +
+ + ); +}; + +function gm() { + const data = parseGMData(gmData); + function countdownRenderer({ days, hours, minutes, seconds, completed }) { + const {dayString, hourString, minuteString, secondString} = + calculateTimeStrings({days, hours, minutes, seconds}); + if (completed) { + return ( +
+

+ ACM's {data.quarter} GM {data.gm_start_time.getFullYear()} happened on the {data.date_with_suffix}! +

+ +
+ ); + } + return ; + } + return ( + + + +
+ {`${data.quarter} +
+ +
+
+

Relevant information

+
+
+ +
+
+

How to get there

+

{data.quarter} GM will be hosted in {data.location}.

+

+

What to bring

+

Encouraged: Phone to scan QR codes, excitement to learn about ACM!

+
+
+

+ Don't hesitate to contact us at acm@ucla.edu if you any accessiblity concerns for {data.quarter} GM. +

+ +
+ +
+

Program

+
+
+

Welcome

+

An introduction to ACM by our president {data.pres}.

+
+
+
+
+

Committee Presentations

+

Learn what ACM's eight committees have planned for {data.quarter} quarter.

+
+

ACM studio {data.studio}

+

ACM icpc {data.icpc}

+

ACM design {data.design}

+

ACM cyber {data.cyber}

+

ACM teachLA {data.teachLA}

+

ACM w {data.w}

+

ACM ai {data.ai}

+

ACM hack {data.hack}

+
+
+
+

ACM Board

+

How to get more involved with ACM beyond attending workshops and events

+
+

ACM board  External: {data.evp}

+

ACM board  Internal: {data.ivp}

+
+
+
+

ACM Initatives

+

See exciting new programs that ACM is trying out

+
+ {data.initiatives.map(item =>

{item}

)} +
+
+
+

Tabling and Social

+

Interact with ACM's officers and walk away with new friends!

+
+

All ACM officers

+
+
+
+
+ ); +} + +export default gm; diff --git a/scripts/gm-generator.mjs b/scripts/gm-generator.mjs new file mode 100644 index 00000000..04929c6c --- /dev/null +++ b/scripts/gm-generator.mjs @@ -0,0 +1,71 @@ +import fs from 'fs'; +import dotenv from 'dotenv'; +import { google } from 'googleapis'; + +// .env config +dotenv.config(); +const SPREADSHEET_ID = process.env?.GM_SPREADSHEET_ID; +const SERVICE_ACCOUNT = process.env?.SERVICE_ACCOUNT; + +// Output json file +writeAllOutputs(); + +// Read data from Google sheets +async function getGoogleSheetData(range) { + const sheets = google.sheets({ version: 'v4' }); + + // Get JWT Token to access sheet + const service_account = JSON.parse(SERVICE_ACCOUNT); + const jwtClient = new google.auth.JWT( + service_account.client_email, + null, // or undefined, or an empty string (depends on your use case) + service_account.private_key, + ['https://www.googleapis.com/auth/spreadsheets'], + ); + + // Authorize the client + await jwtClient.authorize(); + + // Get data from Google spreadsheets + const res = await sheets.spreadsheets.values.get({ + auth: jwtClient, + spreadsheetId: SPREADSHEET_ID, + range: range, + }); + + const rows = res?.data.values; + if (!rows || rows.length == 0) { + // eslint-disable-next-line no-console + console.log('Error: no data found'); + return []; + } + + return rows; +} + +async function fetchGMData() { + const data = await getGoogleSheetData('CurrentGM!A:B'); + + // Format the rows into an array of objects + const formattedData = data.map((row) => ({ + name: row[0], + description: row[1], + })); + + return formattedData; +} + +// Write data from sheets to a json file +async function writeToOutput(name, formattedData) { + const output = JSON.stringify(formattedData); + fs.writeFile(name, output, (err) => { + if (err) throw err; + // eslint-disable-next-line no-console + console.log('Saved output:', name); + }); +} + +// Outputs all necessary json files +async function writeAllOutputs() { + writeToOutput('gmData.json', await fetchGMData()); +}