├── frontend ├── .nvmrc ├── .prettierignore ├── public │ ├── favicon.ico │ ├── robots.txt │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── site.webmanifest │ └── manifest.json ├── src │ ├── assets │ │ ├── chaos.png │ │ └── LandingPage │ │ │ ├── janestreet.png │ │ │ └── tiktok.svg │ ├── types │ │ └── util.ts │ ├── pages │ │ ├── admin │ │ │ ├── CSESoc_logo.jpeg │ │ │ ├── SECSoc_logo.jpeg │ │ │ ├── AdminContent │ │ │ │ ├── CSESoc_logo.jpeg │ │ │ │ ├── AdminCampaignContent.tsx │ │ │ │ └── adminContent.styled.ts │ │ │ ├── admin.styled.ts │ │ │ ├── review │ │ │ │ ├── marking │ │ │ │ │ ├── ApplicationsListTabPanel │ │ │ │ │ │ ├── applicationsListTabPanel.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ApplicationsList │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── rankings │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── PassBar │ │ │ │ │ │ ├── passBar.styled.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FinalRatingCandidateCard │ │ │ │ │ │ ├── finalRatingCandidateCard.styled.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FinalRatingApplicationComments │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── RankingsToolbar │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── finalise_candidates │ │ │ │ │ └── email_templates.ts │ │ │ │ ├── RolesSidebar.tsx │ │ │ │ └── index.tsx │ │ │ ├── OrgContext.ts │ │ │ ├── types.ts │ │ │ └── AdminLoading.tsx │ │ ├── dashboard │ │ │ ├── director.jpg │ │ │ ├── project-lead.jpg │ │ │ ├── project-team.png │ │ │ ├── CampaignGrid │ │ │ │ ├── CampaignLoading.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── application_page │ │ │ ├── director.jpg │ │ │ ├── types.ts │ │ │ ├── RolesSidebar.tsx │ │ │ ├── ApplicationForm.tsx │ │ │ ├── CampaignDetails.tsx │ │ │ └── ApplicationPageLoading.tsx │ │ ├── create_campaign │ │ │ ├── Preview │ │ │ │ ├── director.jpg │ │ │ │ ├── reviewTab.styled.ts │ │ │ │ └── index.tsx │ │ │ ├── createCampaign.styled.ts │ │ │ ├── Campaign │ │ │ │ └── campaignTab.styled.ts │ │ │ ├── Roles │ │ │ │ ├── Question.tsx │ │ │ │ ├── CreateRoleForm.tsx │ │ │ │ ├── RoleListItem.tsx │ │ │ │ ├── SelectFromExistingMenu.tsx │ │ │ │ └── rolesTab.styled.ts │ │ │ └── types.ts │ │ ├── landing │ │ │ ├── components │ │ │ │ ├── Campaigns │ │ │ │ │ ├── compclub.png │ │ │ │ │ ├── CSESoc_logo.jpeg │ │ │ │ │ ├── Campaign.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── SponsorLogos.tsx │ │ │ │ ├── DashboardButton.tsx │ │ │ │ └── Waves.tsx │ │ │ └── index.tsx │ │ ├── signup │ │ │ ├── SignupGenderSelection.tsx │ │ │ └── signup.styled.ts │ │ └── auth_success │ │ │ └── index.tsx │ ├── components │ │ ├── Container.tsx │ │ ├── CampaignCard │ │ │ ├── types.ts │ │ │ ├── CampaignStatus.tsx │ │ │ ├── Popup.tsx │ │ │ └── index.tsx │ │ ├── CreateOrganisationForm │ │ │ ├── new_image_icon.png │ │ │ ├── createOrganisationForm.styled.ts │ │ │ └── index.tsx │ │ ├── Link.tsx │ │ ├── ReviewerStepper │ │ │ ├── reviewerStepper.styled.ts │ │ │ └── index.tsx │ │ ├── Card.tsx │ │ ├── LoadingIndicator │ │ │ └── index.tsx │ │ ├── MessagePopup │ │ │ └── index.tsx │ │ ├── ApplicationPreviewer │ │ │ ├── applicationPreviewer.styled.ts │ │ │ └── index.tsx │ │ ├── NavCard │ │ │ ├── NavItem.tsx │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── Textarea.tsx │ │ ├── BackgroundWrapper │ │ │ └── index.tsx │ │ ├── Button.tsx │ │ ├── Dropdown │ │ │ ├── index.tsx │ │ │ └── DropdownOption.tsx │ │ ├── MarkChip │ │ │ ├── index.tsx │ │ │ └── markChip.styled.ts │ │ ├── PrivateRoute │ │ │ └── index.tsx │ │ ├── Input.tsx │ │ ├── PulsingBar.tsx │ │ ├── Dropzone.tsx │ │ ├── Tabs.tsx │ │ ├── Transition │ │ │ └── index.tsx │ │ ├── AdminSideBar │ │ │ └── adminSidebar.styled.ts │ │ ├── Toast.tsx │ │ ├── NavBar │ │ │ └── AvatarButton.tsx │ │ ├── ApplicationForm │ │ │ └── applicationForm.styled.ts │ │ ├── InputPopup │ │ │ └── index.tsx │ │ └── Modal │ │ │ └── index.tsx │ ├── utils │ │ ├── constants.ts │ │ └── index.tsx │ ├── contexts │ │ ├── SetNavbarTitleContext.ts │ │ └── MessagePopupContext.ts │ ├── index.tsx │ ├── styles │ │ ├── globalStyles.ts │ │ └── animations.ts │ ├── hooks │ │ └── useQuery.ts │ ├── routes.tsx │ ├── App.tsx │ └── api │ │ └── api.ts ├── babel-plugin-macros.config.js ├── postcss.config.js ├── babel.config.json ├── tsconfig.node.json ├── .env.development ├── .nginx │ └── nginx.conf ├── stitches.config.ts ├── README.md ├── .gitignore ├── prettier.config.js ├── tsconfig.json ├── vite.config.ts ├── Dockerfile ├── tailwind.config.js ├── index.html ├── package.json └── .eslintrc.json ├── backend ├── .dockerignore ├── .gitignore ├── migrations │ ├── 2022-05-30-145617_questions │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-114906_roles │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-115832_answers │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-120033_ratings │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-112918_create_users │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-114733_campaigns │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-115409_questions │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-115931_comments │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-114233_create_organisations │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-09-20-052638_user_pronouns │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-115608_applications │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-12-07-114404_create_organisation_users │ │ ├── down.sql │ │ └── up.sql │ ├── 2022-11-21-043159_application_private_statuses │ │ ├── down.sql │ │ └── up.sql │ ├── 2023-06-07-042751_user_gender │ │ ├── down.sql │ │ └── up.sql │ ├── 2022-12-04-065103_image_files │ │ ├── up.sql │ │ └── down.sql │ └── 00000000000000_diesel_initial_setup │ │ ├── down.sql │ │ └── up.sql ├── Cargo.toml ├── server │ ├── src │ │ ├── schema.jpg │ │ ├── schema.png │ │ ├── diesel.toml │ │ ├── database │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── cors.rs │ │ ├── static_resources.rs │ │ ├── admin.rs │ │ ├── state │ │ │ └── mod.rs │ │ ├── comment.rs │ │ ├── user.rs │ │ ├── images.rs │ │ ├── guard │ │ │ └── mod.rs │ │ └── question.rs │ └── Cargo.toml ├── seed_data │ ├── assets │ │ ├── 180DC.png │ │ ├── csesoc_logo.png │ │ └── csesoc_peer_mentoring.jpg │ ├── src │ │ └── main.rs │ └── Cargo.toml ├── scripts │ ├── seed.sh │ └── become_super_user.sh ├── Dockerfile └── README.md ├── CODEOWNERS ├── README.md ├── .pre-commit-config.yaml ├── renovate.json ├── .github └── workflows │ └── rust.yml └── LICENSE /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.0 2 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | target 3 | Dockerfile* -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | target 3 | images 4 | -------------------------------------------------------------------------------- /backend/migrations/2022-05-30-145617_questions/down.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114906_roles/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE roles; 2 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115832_answers/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE answers; 2 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-120033_ratings/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE ratings; 2 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-112918_create_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; 2 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114733_campaigns/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE campaigns; 2 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115409_questions/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE questions; 2 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115931_comments/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE comments; 2 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114233_create_organisations/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE organisations; 2 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "seed_data", 5 | "server" 6 | ] 7 | -------------------------------------------------------------------------------- /backend/migrations/2023-09-20-052638_user_pronouns/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | DROP COLUMN pronouns; 3 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /backend/server/src/schema.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/backend/server/src/schema.jpg -------------------------------------------------------------------------------- /backend/server/src/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/backend/server/src/schema.png -------------------------------------------------------------------------------- /frontend/src/assets/chaos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/assets/chaos.png -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115608_applications/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE applications; 2 | DROP TYPE application_status; 3 | -------------------------------------------------------------------------------- /backend/seed_data/assets/180DC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/backend/seed_data/assets/180DC.png -------------------------------------------------------------------------------- /frontend/babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | twin: { 3 | preset: "stitches", 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114404_create_organisation_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE organisation_users; 2 | DROP TYPE admin_level; 3 | -------------------------------------------------------------------------------- /backend/migrations/2023-09-20-052638_user_pronouns/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN pronouns TEXT DEFAULT '' NOT NULL; 3 | -------------------------------------------------------------------------------- /backend/seed_data/assets/csesoc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/backend/seed_data/assets/csesoc_logo.png -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/src/types/util.ts: -------------------------------------------------------------------------------- 1 | export type DropFirst = T extends [unknown, ...infer U] 2 | ? U 3 | : never; 4 | -------------------------------------------------------------------------------- /backend/migrations/2022-11-21-043159_application_private_statuses/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE applications 2 | DROP COLUMN private_status; 3 | -------------------------------------------------------------------------------- /backend/migrations/2023-06-07-042751_user_gender/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | DROP COLUMN gender; 3 | 4 | DROP TYPE user_gender; 5 | -------------------------------------------------------------------------------- /frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/src/pages/admin/CSESoc_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/admin/CSESoc_logo.jpeg -------------------------------------------------------------------------------- /frontend/src/pages/admin/SECSoc_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/admin/SECSoc_logo.jpeg -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/director.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/dashboard/director.jpg -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/project-lead.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/dashboard/project-lead.jpg -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/project-team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/dashboard/project-team.png -------------------------------------------------------------------------------- /backend/seed_data/src/main.rs: -------------------------------------------------------------------------------- 1 | mod seed; 2 | 3 | use dotenv; 4 | 5 | fn main() { 6 | dotenv::dotenv().ok(); 7 | seed::seed(); 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/assets/LandingPage/janestreet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/assets/LandingPage/janestreet.png -------------------------------------------------------------------------------- /frontend/src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import tw from "twin.macro"; 2 | 3 | export default tw.div`mx-auto flex max-w-7xl flex-1 flex-col p-4`; 4 | -------------------------------------------------------------------------------- /backend/seed_data/assets/csesoc_peer_mentoring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/backend/seed_data/assets/csesoc_peer_mentoring.jpg -------------------------------------------------------------------------------- /frontend/src/components/CampaignCard/types.ts: -------------------------------------------------------------------------------- 1 | export type Position = { 2 | id: number | string; 3 | name: string; 4 | number: number; 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/src/pages/application_page/director.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/application_page/director.jpg -------------------------------------------------------------------------------- /frontend/src/pages/admin/AdminContent/CSESoc_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/admin/AdminContent/CSESoc_logo.jpeg -------------------------------------------------------------------------------- /frontend/src/pages/application_page/types.ts: -------------------------------------------------------------------------------- 1 | export type RoleQuestions = { 2 | [role: number]: { 3 | id: number; 4 | text: string; 5 | }[]; 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/Preview/director.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/create_campaign/Preview/director.jpg -------------------------------------------------------------------------------- /frontend/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["babel-plugin-macros"], 4 | "targets": "defaults" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/pages/landing/components/Campaigns/compclub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/landing/components/Campaigns/compclub.png -------------------------------------------------------------------------------- /frontend/src/components/CreateOrganisationForm/new_image_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/components/CreateOrganisationForm/new_image_icon.png -------------------------------------------------------------------------------- /frontend/src/pages/landing/components/Campaigns/CSESoc_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoc-unsw/chaos/HEAD/frontend/src/pages/landing/components/Campaigns/CSESoc_logo.jpeg -------------------------------------------------------------------------------- /frontend/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const SIGNUP_REQUIRED = "SignupRequired"; 2 | export const HTTP_STATUS_OKAY = 200; 3 | export const COLOURS = { 4 | lightGrey: "#e2e6ed", 5 | }; 6 | -------------------------------------------------------------------------------- /backend/server/src/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/database/schema.rs" 6 | -------------------------------------------------------------------------------- /backend/server/src/database/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod schema; 3 | 4 | use rocket_sync_db_pools::database; 5 | 6 | #[database("database")] 7 | pub struct Database(diesel::PgConnection); 8 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /backend/migrations/2022-12-04-065103_image_files/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE organisations 2 | ALTER COLUMN logo TYPE TEXT USING NULL; 3 | 4 | ALTER TABLE campaigns 5 | ALTER COLUMN cover_image TYPE TEXT USING NULL; 6 | -------------------------------------------------------------------------------- /backend/migrations/2022-12-04-065103_image_files/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE organisations 2 | ALTER COLUMN logo TYPE BYTEA USING NULL; 3 | 4 | ALTER TABLE campaigns 5 | ALTER COLUMN cover_image TYPE BYTEA USING NULL; 6 | -------------------------------------------------------------------------------- /backend/migrations/2023-06-07-042751_user_gender/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE user_gender AS ENUM ('Female', 'Male', 'Unspecified'); 2 | 3 | ALTER TABLE users 4 | ADD COLUMN gender user_gender DEFAULT 'Unspecified' NOT NULL; 5 | -------------------------------------------------------------------------------- /frontend/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import tw from "twin.macro"; 2 | 3 | const Link = tw.a`text-brand-600 underline decoration-transparent transition hover:text-brand-700 hover:decoration-current`; 4 | 5 | export default Link; 6 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/admin.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | 3 | export const AdminContainer = styled("div")` 4 | display: flex; 5 | margin: 0px; 6 | width: 100%; 7 | flex-wrap: wrap; 8 | `; 9 | -------------------------------------------------------------------------------- /frontend/src/components/ReviewerStepper/reviewerStepper.styled.ts: -------------------------------------------------------------------------------- 1 | import { Stepper } from "@mui/material"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | export const NavStepper = styled(Stepper)` 5 | margin: 3rem 0; 6 | `; 7 | -------------------------------------------------------------------------------- /frontend/src/contexts/SetNavbarTitleContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-empty-function 4 | export const SetNavBarTitleContext = createContext((_title: string) => {}); 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | backend/ @csesoc/chaos-leads 2 | frontend/ @csesoc/chaos-leads 3 | README.md @csesoc/chaos-leads 4 | 5 | .github/ @csesoc/technical 6 | renovate.json @csesoc/technical 7 | CODEOWNERS @csesoc/technical 8 | Dockerfile @csesoc/technical 9 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/review/marking/ApplicationsListTabPanel/applicationsListTabPanel.ts: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material/"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | export const RatingChips = styled(Grid)` 5 | margin-bottom: 1rem; 6 | `; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Development 4 | 5 | Frontend/Backend setup docs are in their respective folders. 6 | 7 | ### Repo-wide git hooks 8 | 9 | Install `pre-commit` via the instructions at [https://pre-commit.com/](https://pre-commit.com) 10 | 11 | Run `pre-commit install` 12 | -------------------------------------------------------------------------------- /backend/migrations/2022-11-21-043159_application_private_statuses/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE applications 2 | ADD COLUMN private_status application_status; 3 | 4 | UPDATE applications 5 | SET private_status = status; 6 | 7 | ALTER TABLE applications 8 | ALTER COLUMN status 9 | SET NOT NULL; 10 | -------------------------------------------------------------------------------- /backend/seed_data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "seed_data" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | server = { path="../server" } 10 | diesel = "1.4.8" 11 | chrono = "0.4.23" 12 | dotenv = "0.15.0" 13 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_OAUTH_CALLBACK_URL=https://accounts.google.com/o/oauth2/v2/auth?client_id=985448402284-al4vuqpokkhgv6h952lhu6iasg1lupug.apps.googleusercontent.com&redirect_uri=http://localhost:3000/auth/callback&response_type=code&scope=profile email&access_type=online 2 | VITE_API_BASE_URL=http://localhost:8000 3 | BROWSER=none 4 | -------------------------------------------------------------------------------- /frontend/.nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | server { 7 | listen 80; 8 | root /usr/share/nginx/html; 9 | include /etc/nginx/mime.types; 10 | 11 | location / { 12 | try_files $uri /index.html; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /frontend/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | const Card = styled.div({ 4 | ...tw`flex flex-col rounded bg-white p-4 shadow`, 5 | 6 | variants: { 7 | hoverable: { 8 | true: tw`transition hover:-translate-y-1 hover:shadow-lg`, 9 | }, 10 | }, 11 | }); 12 | 13 | export default Card; 14 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114233_create_organisations/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE organisations ( 2 | id SERIAL PRIMARY KEY, 3 | name TEXT NOT NULL, 4 | logo BYTEA, 5 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 7 | ); 8 | 9 | SELECT diesel_manage_updated_at('organisations'); 10 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from "./App"; 5 | import globalStyles from "./styles/globalStyles"; 6 | 7 | globalStyles(); 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/stitches.config.ts: -------------------------------------------------------------------------------- 1 | import { createStitches } from "@stitches/react"; 2 | 3 | export type { CSS } from "@stitches/react/types/css-util"; 4 | 5 | export const stitches = createStitches({ 6 | prefix: "", 7 | theme: {}, 8 | utils: {}, 9 | }); 10 | 11 | export const { css, styled, globalCss, theme, keyframes, getCssText } = 12 | stitches; 13 | -------------------------------------------------------------------------------- /frontend/src/components/LoadingIndicator/index.tsx: -------------------------------------------------------------------------------- 1 | import { LinearProgress } from "@mui/material"; 2 | import "twin.macro"; 3 | 4 | const LoadingIndicator = () => ( 5 |
6 | 7 |
Loading...
8 |
9 | ); 10 | 11 | export default LoadingIndicator; 12 | -------------------------------------------------------------------------------- /frontend/src/contexts/MessagePopupContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export type Message = { 4 | type: "error" | "warning" | "success"; 5 | message: string; 6 | id: number; 7 | }; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-empty-function 10 | export const MessagePopupContext = createContext( 11 | (_message: Omit) => {} 12 | ); 13 | -------------------------------------------------------------------------------- /backend/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/OrgContext.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { createContext } from "react"; 3 | 4 | import type { Organisation } from "./types"; 5 | 6 | export const OrgContext = createContext({ 7 | orgSelected: -1, 8 | setOrgSelected: (_orgSelected: number) => {}, 9 | orgList: [] as Organisation[], 10 | setOrgList: (_orgList: Organisation[]) => {}, 11 | }); 12 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-120033_ratings/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE ratings ( 2 | id SERIAL PRIMARY KEY, 3 | application_id INTEGER NOT NULL REFERENCES applications (id), 4 | rater_user_id INTEGER NOT NULL REFERENCES users (id), 5 | rating INTEGER NOT NULL, 6 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 8 | ); 9 | 10 | SELECT diesel_manage_updated_at('ratings'); 11 | -------------------------------------------------------------------------------- /backend/server/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | pub mod admin; 5 | pub mod application; 6 | pub mod auth; 7 | pub mod campaigns; 8 | pub mod comment; 9 | pub mod cors; 10 | pub mod database; 11 | pub mod error; 12 | pub mod guard; 13 | pub mod images; 14 | pub mod organisation; 15 | pub mod permissions; 16 | pub mod question; 17 | pub mod role; 18 | pub mod state; 19 | pub mod static_resources; 20 | pub mod user; 21 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | - Install [fnm](https://github.com/Schniz/fnm) (drop-in replacement for nvm that's significantly faster - highly recommended) or [nvm](https://github.com/nvm-sh/nvm) 4 | - run `fnm use` (or `nvm use`) to make sure your computer uses the most recent node version 5 | - install yarn 6 | - download and place the frontend `.env.development` file in the root of the frontend folder 7 | - run `yarn` 8 | - run `yarn start` 9 | -------------------------------------------------------------------------------- /frontend/src/components/MessagePopup/index.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | const MessagePopup = styled.div({ 4 | ...tw`w-72 rounded-md p-2 shadow-md drop-shadow-lg`, 5 | variants: { 6 | type: { 7 | error: tw`bg-red-300 text-red-900`, 8 | warning: tw`bg-yellow-100 text-yellow-900`, 9 | success: tw`bg-lime-200 text-lime-900`, 10 | }, 11 | }, 12 | }); 13 | 14 | export default MessagePopup; 15 | -------------------------------------------------------------------------------- /frontend/src/styles/globalStyles.ts: -------------------------------------------------------------------------------- 1 | import tw, { globalStyles } from "twin.macro"; 2 | 3 | import { globalCss } from "../../stitches.config"; 4 | 5 | import type { TwStyle } from "twin.macro"; 6 | 7 | const customStyles = { 8 | body: tw`font-sans! antialiased`, 9 | }; 10 | 11 | const styles = () => { 12 | globalCss(customStyles)(); 13 | globalCss(globalStyles as Record)(); 14 | }; 15 | 16 | export default styles; 17 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115832_answers/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE answers ( 2 | id SERIAL PRIMARY KEY, 3 | application_id INTEGER NOT NULL REFERENCES applications (id), 4 | question_id INTEGER NOT NULL REFERENCES questions (id), 5 | description TEXT NOT NULL, 6 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 8 | ); 9 | 10 | SELECT diesel_manage_updated_at('answers'); 11 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115931_comments/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE comments ( 2 | id SERIAL PRIMARY KEY, 3 | application_id INTEGER NOT NULL REFERENCES applications (id), 4 | commenter_user_id INTEGER NOT NULL REFERENCES users (id), 5 | description TEXT NOT NULL, 6 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 8 | ); 9 | 10 | SELECT diesel_manage_updated_at('comments'); 11 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /frontend/src/styles/animations.ts: -------------------------------------------------------------------------------- 1 | import { keyframes } from "@stitches/react"; 2 | import tw, { styled } from "twin.macro"; 3 | 4 | export const pulse = keyframes({ 5 | "50%": tw`opacity-50`, 6 | }); 7 | 8 | export const pulseCss = { 9 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 10 | animation: `${pulse} 2s cubic-bezier(0.4, 0, 0.6, 1) infinite`, 11 | }; 12 | 13 | export const Pulse = styled("div", pulseCss); 14 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115409_questions/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE questions ( 2 | id SERIAL PRIMARY KEY, 3 | role_id INTEGER NOT NULL REFERENCES roles (id), 4 | title TEXT NOT NULL, 5 | description TEXT, 6 | max_bytes INTEGER NOT NULL, 7 | required BOOLEAN NOT NULL, 8 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | 12 | SELECT diesel_manage_updated_at('questions'); 13 | -------------------------------------------------------------------------------- /frontend/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/hooks/useQuery.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | // stolen from 4 | // https://v5.reactrouter.com/web/example/query-parameters 5 | 6 | // A custom hook that builds on useLocation to parse 7 | // the query string for you. 8 | const useQuery = () => { 9 | const { search } = useLocation(); 10 | return useMemo(() => new URLSearchParams(search), [search]); 11 | }; 12 | 13 | export default useQuery; 14 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-112918_create_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id SERIAL PRIMARY KEY, 3 | email TEXT NOT NULL, 4 | zid TEXT NOT NULL, 5 | display_name TEXT NOT NULL, 6 | degree_name TEXT NOT NULL, 7 | degree_starting_year INTEGER NOT NULL, 8 | superuser BOOLEAN NOT NULL, 9 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | SELECT diesel_manage_updated_at('users'); 14 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/review/rankings/types.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationWithQuestions } from "pages/admin/types"; 2 | import type { ApplicationStatus } from "types/api"; 3 | 4 | export type Ranking = { 5 | name: string; 6 | id: number; 7 | status: ApplicationStatus; 8 | ratings: { rater: string; rating: number }[]; 9 | }; 10 | 11 | export type Rankings = { 12 | [k: string]: Ranking[]; 13 | }; 14 | 15 | export type Applications = { 16 | [id: number]: ApplicationWithQuestions; 17 | }; 18 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114906_roles/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE roles ( 2 | id SERIAL PRIMARY KEY, 3 | campaign_id INTEGER NOT NULL REFERENCES campaigns (id), 4 | name TEXT NOT NULL, 5 | description TEXT, 6 | min_available INTEGER NOT NULL, 7 | max_available INTEGER NOT NULL, 8 | finalised BOOLEAN NOT NULL DEFAULT FALSE, 9 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | SELECT diesel_manage_updated_at('roles'); 14 | -------------------------------------------------------------------------------- /backend/scripts/seed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "WARNING - this will WIPE your local database" 3 | echo "WARNING - this will WIPE your local changes to database/schema.rs" 4 | echo "Are you sure you want to continue? (y/n)" 5 | read answer 6 | if [ "$answer" != "y" ]; then 7 | echo "Aborting" 8 | exit 1 9 | fi 10 | 11 | echo "Deleting images directory" 12 | rm -rf images 13 | 14 | cd server/src && diesel database reset 15 | 16 | cd ../../seed_data && cargo run --bin seed_data 17 | 18 | mv images .. 19 | -------------------------------------------------------------------------------- /frontend/src/components/ApplicationPreviewer/applicationPreviewer.styled.ts: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material/"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | export const Question = styled(Typography)` 5 | font-weight: bold; 6 | `; 7 | 8 | export const Answer = styled(Typography)` 9 | margin-bottom: 1rem; 10 | `; 11 | 12 | export const NoAnswer = styled(Typography)` 13 | font-style: italic; 14 | margin-bottom: 1rem; 15 | color: ${(props) => props.theme.palette.grey[600]}; 16 | `; 17 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-115608_applications/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE application_status AS ENUM ('Pending', 'Rejected', 'Success'); 2 | 3 | CREATE TABLE applications ( 4 | id SERIAL PRIMARY KEY, 5 | user_id INTEGER NOT NULL REFERENCES users (id), 6 | role_id INTEGER NOT NULL REFERENCES roles (id), 7 | status application_status NOT NULL, 8 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | 12 | SELECT diesel_manage_updated_at('applications'); 13 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114733_campaigns/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE campaigns ( 2 | id SERIAL PRIMARY KEY, 3 | organisation_id INTEGER NOT NULL REFERENCES organisations (id), 4 | name TEXT NOT NULL, 5 | cover_image BYTEA, 6 | description TEXT NOT NULL, 7 | starts_at TIMESTAMP NOT NULL, 8 | ends_at TIMESTAMP NOT NULL, 9 | published BOOLEAN NOT NULL, 10 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 12 | ); 13 | 14 | SELECT diesel_manage_updated_at('campaigns'); 15 | -------------------------------------------------------------------------------- /frontend/prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("prettier").Config} 3 | */ 4 | module.exports = { 5 | trailingComma: "es5", 6 | tabWidth: 2, 7 | semi: true, 8 | singleQuote: false, 9 | printWidth: 80, 10 | useTabs: false, 11 | quoteProps: "as-needed", 12 | jsxSingleQuote: false, 13 | bracketSpacing: true, 14 | bracketSameLine: false, 15 | arrowParens: "always", 16 | 17 | plugins: ["prettier-plugin-tailwindcss"], 18 | tailwindAttributes: ["className", "class", "tw"], 19 | tailwindFunctions: ["tw"], 20 | }; 21 | -------------------------------------------------------------------------------- /backend/server/src/error.rs: -------------------------------------------------------------------------------- 1 | use rocket::http::Status; 2 | use rocket::request::Request; 3 | use rocket::response::{self, Responder}; 4 | use rocket::serde::{json::Json, Serialize}; 5 | 6 | pub struct JsonErr(pub T, pub Status); 7 | 8 | impl<'r, T: Serialize> Responder<'r, 'r> for JsonErr { 9 | fn respond_to(self, r: &Request) -> response::Result<'r> { 10 | let json_self = Json(self.0); 11 | json_self.respond_to(r).map(|mut r| { 12 | r.set_status(self.1); 13 | r 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/migrations/2021-12-07-114404_create_organisation_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE admin_level AS ENUM ('ReadOnly', 'Director', 'Admin'); 2 | 3 | CREATE TABLE organisation_users ( 4 | id SERIAL PRIMARY KEY, 5 | user_id INTEGER NOT NULL REFERENCES users (id), 6 | organisation_id INTEGER NOT NULL REFERENCES organisations (id), 7 | admin_level admin_level NOT NULL, 8 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | 12 | SELECT diesel_manage_updated_at('organisation_users'); 13 | -------------------------------------------------------------------------------- /frontend/src/pages/signup/SignupGenderSelection.tsx: -------------------------------------------------------------------------------- 1 | import tw from "twin.macro"; 2 | 3 | import type { ReactNode } from "react"; 4 | import type React from "react"; 5 | 6 | const Select = tw.select`hocus:border-blue-300 form-select w-96 rounded-md border-gray-300 shadow-sm transition focus:ring focus:ring-blue-200/50`; 7 | 8 | const Label = tw.label`flex flex-col`; 9 | const LabelText: React.FC<{ children: ReactNode }> = ({ children }) => ( 10 | {children} 11 | ); 12 | 13 | export default Object.assign(Select, { Label, LabelText }); 14 | -------------------------------------------------------------------------------- /frontend/src/components/NavCard/NavItem.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | const NavItem = styled.div({ 4 | ...tw`block w-full rounded px-3 py-1.5 text-left font-normal`, 5 | 6 | ...tw`relative z-0 hover:before:opacity-20`, 7 | "&::before": { 8 | content: "", 9 | ...tw`absolute inset-0 z-[-1] rounded bg-gradient-to-r from-blue-300 to-violet-300 opacity-0 transition-opacity`, 10 | }, 11 | 12 | variants: { 13 | active: { 14 | true: tw`before:opacity-30! shadow-sm`, 15 | }, 16 | }, 17 | }); 18 | 19 | export default NavItem; 20 | -------------------------------------------------------------------------------- /frontend/src/components/NavCard/index.tsx: -------------------------------------------------------------------------------- 1 | import "twin.macro"; 2 | 3 | import Card from "components/Card"; 4 | 5 | import type { ComponentProps, PropsWithChildren, ReactNode } from "react"; 6 | 7 | type Props = { 8 | title: ReactNode; 9 | } & Omit, "title">; 10 | 11 | const NavCard = ({ title, children, ...props }: PropsWithChildren) => ( 12 | 13 |

{title}

14 |
    {children}
15 |
16 | ); 17 | 18 | export default NavCard; 19 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/createCampaign.styled.ts: -------------------------------------------------------------------------------- 1 | import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; 2 | import { Button } from "@mui/material"; 3 | import { styled } from "@mui/material/styles"; 4 | 5 | export const NextWrapper = styled("div")` 6 | display: flex; 7 | justify-content: flex-end; 8 | margin-bottom: 50px; 9 | margin-right: 100px; 10 | `; 11 | 12 | export const NextButton = styled(Button)` 13 | font-size: 20px; 14 | `; 15 | 16 | export const ArrowIcon = styled(ArrowForwardIosIcon)` 17 | font-size: 1rem; 18 | margin-left: 8px; 19 | `; 20 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/review/rankings/PassBar/passBar.styled.ts: -------------------------------------------------------------------------------- 1 | import { Divider } from "@mui/material"; 2 | import { grey } from "@mui/material/colors"; 3 | import { styled } from "@mui/material/styles"; 4 | 5 | // export const PassBarDivider = styled(Divider) 6 | 7 | export const PassBarDivider = styled(Divider)(({ theme }) => ({ 8 | color: `${grey[400]}`, 9 | transition: theme.transitions.create("color", { 10 | duration: theme.transitions.duration.standard, 11 | }), 12 | "&:hover": { 13 | color: `${grey[600]}`, 14 | transition: theme.transitions.create("color", { 15 | duration: theme.transitions.duration.standard, 16 | }), 17 | }, 18 | })); 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.1.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/doublify/pre-commit-rust 12 | rev: v1.0 13 | hooks: 14 | - id: fmt 15 | files: ^backend/ 16 | args: ['--manifest-path', 'backend/server/Cargo.toml', '--'] 17 | - id: cargo-check 18 | args: ['--manifest-path', 'backend/server/Cargo.toml', '--'] 19 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/review/finalise_candidates/email_templates.ts: -------------------------------------------------------------------------------- 1 | const Success = `Hi {name}, 2 | 3 | Thank you for applying for {role} and attending the interview :) We had many applicants this year and your application really stood out! Congratulations - we want you on the team for 2023! 4 | 5 | {organisation}`; 6 | 7 | const Rejected = `Hi {name}, 8 | 9 | Thank you for applying for {role}. Unfortunately, we regret to inform you that you have been unsuccessful with your application. 10 | 11 | Get owned loser :omegalul: 12 | 13 | {organisation}`; 14 | 15 | const templates: { [k: string]: string } = { 16 | Success, 17 | Rejected, 18 | }; 19 | 20 | export default templates; 21 | -------------------------------------------------------------------------------- /backend/migrations/2022-05-30-145617_questions/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE IF EXISTS questions 2 | DROP CONSTRAINT questions_role_id_fkey; 3 | 4 | ALTER TABLE IF EXISTS questions 5 | ALTER COLUMN role_id TYPE INTEGER[] 6 | USING array[role_id]::INTEGER[]; 7 | 8 | ALTER TABLE IF EXISTS questions 9 | RENAME COLUMN role_id TO role_ids; 10 | 11 | CREATE TABLE IF NOT EXISTS questions ( 12 | id SERIAL PRIMARY KEY, 13 | role_ids INTEGER[] NOT NULL, 14 | title TEXT NOT NULL, 15 | description TEXT, 16 | max_bytes INTEGER NOT NULL, 17 | required BOOLEAN NOT NULL, 18 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 20 | ); 21 | -------------------------------------------------------------------------------- /frontend/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import BackgroundWrapper from "./BackgroundWrapper"; 2 | import CampaignCard from "./CampaignCard"; 3 | import LoadingIndicator from "./LoadingIndicator"; 4 | import MarkChip from "./MarkChip"; 5 | import MessagePopup from "./MessagePopup"; 6 | import Modal from "./Modal"; 7 | import NavBar from "./NavBar"; 8 | import PrivateRoute from "./PrivateRoute"; 9 | import ReviewerStepper from "./ReviewerStepper"; 10 | import Transition from "./Transition"; 11 | 12 | export { 13 | BackgroundWrapper, 14 | CampaignCard, 15 | LoadingIndicator, 16 | MarkChip, 17 | MessagePopup, 18 | Modal, 19 | NavBar, 20 | PrivateRoute, 21 | ReviewerStepper, 22 | Transition, 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/components/ReviewerStepper/index.tsx: -------------------------------------------------------------------------------- 1 | import { Step, StepLabel } from "@mui/material"; 2 | 3 | import { NavStepper } from "./reviewerStepper.styled"; 4 | 5 | type Props = { 6 | activeStep: number; 7 | }; 8 | const ReviewerStepper = ({ activeStep }: Props) => ( 9 | 10 | 11 | Mark candidates individually 12 | 13 | 14 | Choose candidates to progress to the next stage 15 | 16 | 17 | Notify candidates of their results 18 | 19 | 20 | ); 21 | 22 | export default ReviewerStepper; 23 | -------------------------------------------------------------------------------- /backend/server/src/cors.rs: -------------------------------------------------------------------------------- 1 | use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; 2 | 3 | pub fn cors() -> Cors { 4 | let cors = rocket_cors::CorsOptions { 5 | allowed_origins: AllowedOrigins::All, 6 | allowed_methods: { 7 | // why do i have to do this, honestly 8 | use rocket::http::Method::*; 9 | vec![Get, Put, Post, Delete, Options, Head, Trace, Connect, Patch] 10 | .into_iter() 11 | .map(From::from) 12 | .collect() 13 | }, 14 | allowed_headers: AllowedHeaders::All, 15 | ..Default::default() 16 | } 17 | .to_cors() 18 | .expect("Failed to create CORS options"); 19 | 20 | cors 21 | } 22 | -------------------------------------------------------------------------------- /backend/scripts/become_super_user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting the script" 4 | echo "====================" 5 | echo "what is the email for the account?" 6 | read SUPERUSER_EMAIL 7 | 8 | 9 | echo "make $SUPERUSER_EMAIL a superuser? (y/n)" 10 | read SUPERUSER_ANSWER 11 | if [ "$SUPERUSER_ANSWER" == "n" ]; then 12 | echo "skipping superuser creation" 13 | exit 14 | fi 15 | 16 | 17 | 18 | # expose env variables from .env 19 | if [ -f .env ] 20 | then 21 | export $(cat .env | sed 's/#.*//g' | xargs) 22 | else 23 | echo "no .env file found" 24 | exit 25 | fi 26 | 27 | echo "db url is $DATABASE_URL" 28 | 29 | psql $DATABASE_URL << EOF 30 | 31 | UPDATE users SET superuser = true WHERE email = '$SUPERUSER_EMAIL'; 32 | 33 | EOF 34 | -------------------------------------------------------------------------------- /frontend/src/components/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | const styles = { 4 | ...tw`form-textarea block w-full rounded-md border-gray-300 shadow-sm transition hover:border-blue-300 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50`, 5 | 6 | variants: { 7 | size: { 8 | md: tw`min-h-48`, 9 | lg: tw`min-h-64`, 10 | }, 11 | }, 12 | }; 13 | 14 | const Textarea = styled("textarea", styles); 15 | const Wrapper = styled("label", { 16 | ...styles, 17 | ...tw`overflow-hidden p-0`, 18 | }); 19 | const Header = tw.header`flex items-center border-b border-gray-200 bg-gray-100 px-3 py-2`; 20 | 21 | export default Object.assign(Textarea, { 22 | Wrapper, 23 | Header, 24 | }); 25 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest-rust-1.68.0 AS chef 2 | WORKDIR /app 3 | 4 | FROM chef AS planner 5 | COPY . . 6 | RUN cargo chef prepare --recipe-path recipe.json 7 | 8 | FROM chef AS builder 9 | COPY --from=planner /app/recipe.json recipe.json 10 | RUN cargo chef cook --release --recipe-path recipe.json 11 | COPY . . 12 | RUN cargo build --release --bin server 13 | 14 | FROM ubuntu 15 | RUN apt update 16 | RUN apt install -y wget libpq5 17 | RUN wget https://mirrors.edge.kernel.org/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_amd64.deb \ 18 | && dpkg -i libssl1.1_1.1.0g-2ubuntu4_amd64.deb \ 19 | && rm libssl1.1_1.1.0g-2ubuntu4_amd64.deb 20 | 21 | COPY --from=builder /app/target/release/server / 22 | CMD ["./server"] 23 | -------------------------------------------------------------------------------- /frontend/src/components/BackgroundWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, useTheme } from "@mui/system"; 2 | 3 | import type { PropsWithChildren } from "react"; 4 | 5 | const BackgroundWrapper = ({ children }: PropsWithChildren) => { 6 | const theme = useTheme(); 7 | return ( 8 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default BackgroundWrapper; 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "baseUrl": "src", 5 | "useDefineForClassFields": true, 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "types": ["vite/client"] 20 | }, 21 | "types": ["types"], 22 | "include": ["src", "types"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /backend/server/src/static_resources.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use rocket::{ 4 | fs::NamedFile, 5 | get, 6 | response::{self, Responder}, 7 | Request, Response, 8 | }; 9 | 10 | pub struct CachedFile(NamedFile); 11 | 12 | impl<'r> Responder<'r, 'r> for CachedFile { 13 | fn respond_to(self, req: &Request) -> response::Result<'r> { 14 | Response::build_from(self.0.respond_to(req)?) 15 | .raw_header("Cache-control", "max-age=86400") // 24h (24*60*60) 16 | .ok() 17 | } 18 | } 19 | 20 | #[get("/images/")] 21 | pub async fn files(file: PathBuf) -> Option { 22 | NamedFile::open(Path::new("images/").join(file)) 23 | .await 24 | .ok() 25 | .map(|nf| CachedFile(nf)) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | const Button = styled.button({ 4 | ...tw`flex items-center gap-1 rounded px-3 py-1.5 shadow outline-none transition focus:ring disabled:cursor-not-allowed disabled:opacity-50`, 5 | 6 | variants: { 7 | color: { 8 | primary: tw`bg-brand-500 text-white ring-brand-500/40 hover:bg-brand-600 active:bg-brand-700`, 9 | danger: tw`bg-red-600 text-white ring-red-600/40 hover:bg-red-700 active:bg-red-800`, 10 | white: tw`border border-brand-300 bg-white text-gray-900 ring-brand-300/40 hover:border-brand-400 hover:bg-brand-50 hover:text-brand-950 active:bg-brand-75`, 11 | }, 12 | }, 13 | 14 | defaultVariants: { 15 | color: "primary", 16 | }, 17 | }); 18 | 19 | export default Button; 20 | -------------------------------------------------------------------------------- /frontend/src/components/Dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from "@headlessui/react"; 2 | import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; 3 | import tw from "twin.macro"; 4 | 5 | import Card from "components/Card"; 6 | 7 | import type { ReactNode } from "react"; 8 | 9 | type Props = { 10 | children: ReactNode; 11 | }; 12 | 13 | const Dropdown = ({ children }: Props) => ( 14 | 15 |
16 | 17 | 19 | 20 | 21 | {children} 22 | 23 | 24 |
25 |
26 | ); 27 | 28 | export default Dropdown; 29 | -------------------------------------------------------------------------------- /frontend/src/components/CampaignCard/CampaignStatus.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | const CampaignStatus = styled.button({ 4 | ...tw`ml-auto flex items-center gap-1 rounded-[0.2rem] px-2 py-1.5 text-white shadow transition hover:shadow-md`, 5 | 6 | variants: { 7 | status: { 8 | pending: tw`bg-[hsl(220, 60%, 90%)] shadow-[hsl(220, 60%, 90%)]! text-black`, 9 | open: tw`bg-[hsl(220, 93%, 60%)] shadow-[hsla(220, 93%, 60%, 50%)]!`, 10 | closed: tw`bg-gray-100 text-black`, 11 | offered: tw`shadow-green-200! bg-green-200 text-green-900 hover:bg-green-300`, 12 | rejected: tw`shadow-red-200! bg-red-200 text-red-900`, 13 | }, 14 | }, 15 | 16 | defaultVariants: { 17 | status: "open", 18 | }, 19 | }); 20 | 21 | export default CampaignStatus; 22 | -------------------------------------------------------------------------------- /frontend/src/components/MarkChip/index.tsx: -------------------------------------------------------------------------------- 1 | import { ColoredChip } from "./markChip.styled"; 2 | 3 | import type { ComponentProps, MouseEventHandler } from "react"; 4 | 5 | type Props = { 6 | mark: number; 7 | decimal?: boolean; 8 | colored: boolean; 9 | variant?: ComponentProps["variant"]; 10 | clickable?: boolean; 11 | onClick?: MouseEventHandler; 12 | }; 13 | 14 | const MarkChip = ({ 15 | mark, 16 | decimal, 17 | colored, 18 | variant = "filled", 19 | clickable, 20 | onClick, 21 | }: Props) => ( 22 | 30 | ); 31 | 32 | export default MarkChip; 33 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AdminLevel, 3 | CampaignInfo, 4 | OrganisationUserInfo, 5 | } from "../../types/api"; 6 | 7 | export type Organisation = { 8 | id: number; 9 | icon: string; 10 | orgName: string; 11 | campaigns: CampaignInfo[]; 12 | members: OrganisationUserInfo[]; 13 | }; 14 | 15 | export type Campaign = { 16 | id: number; 17 | image: string; 18 | title: string; 19 | startDate: string; 20 | endDate: string; 21 | }; 22 | 23 | export type Member = { 24 | id: number; 25 | name: string; 26 | role: AdminLevel; 27 | }; 28 | 29 | type Question = { 30 | question: string; 31 | answer?: string; 32 | }; 33 | 34 | export type ApplicationWithQuestions = { 35 | applicationId: number; 36 | zId: string; 37 | mark?: number; 38 | questions: Question[]; 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import tsconfigPaths from "vite-tsconfig-paths"; 3 | import react from "@vitejs/plugin-react"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | react({ 8 | babel: { 9 | plugins: ["babel-plugin-macros"], 10 | }, 11 | }), 12 | tsconfigPaths(), 13 | ], 14 | optimizeDeps: { 15 | esbuildOptions: { 16 | target: "es2020", 17 | }, 18 | }, 19 | esbuild: { 20 | // i don't remember what this is for but it's for something 21 | logOverride: { "this-is-undefined-in-esm": "silent" }, 22 | }, 23 | server: { 24 | port: 3000, 25 | proxy: { 26 | "/api": { 27 | target: "http://127.0.0.1:8000", 28 | rewrite: (path) => path.replace(/^\/api/, ""), 29 | }, 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /frontend/src/components/PrivateRoute/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Route, useNavigate } from "react-router-dom"; 5 | 6 | import { isLoggedIn } from "../../utils"; 7 | 8 | import type { ComponentProps } from "react"; 9 | 10 | const PrivateRoute = (props: ComponentProps) => { 11 | const [loading, setLoading] = useState(true); 12 | const navigate = useNavigate(); 13 | 14 | useEffect(() => { 15 | function getLoggedIn() { 16 | if (!isLoggedIn()) { 17 | navigate("/"); 18 | } 19 | setLoading(false); 20 | } 21 | 22 | getLoggedIn(); 23 | }, []); 24 | 25 | if (loading) { 26 | return
; 27 | } 28 | 29 | return ; 30 | }; 31 | 32 | export default PrivateRoute; 33 | -------------------------------------------------------------------------------- /frontend/src/pages/application_page/RolesSidebar.tsx: -------------------------------------------------------------------------------- 1 | import "twin.macro"; 2 | 3 | import NavCard from "components/NavCard"; 4 | import NavItem from "components/NavCard/NavItem"; 5 | 6 | import type { Role } from "types/api"; 7 | 8 | type Props = { 9 | roles: Role[]; 10 | rolesSelected: number[]; 11 | toggleRole: (_roleId: number) => void; 12 | }; 13 | const RolesSidebar = ({ roles, rolesSelected, toggleRole }: Props) => ( 14 | 15 | {roles.map((role) => ( 16 |
  • 17 | toggleRole(role.id)} 21 | > 22 | {role.name} 23 | 24 |
  • 25 | ))} 26 |
    27 | ); 28 | 29 | export default RolesSidebar; 30 | -------------------------------------------------------------------------------- /frontend/src/pages/signup/signup.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/system"; 2 | 3 | export const StyledForm = styled("form")((props) => ({ 4 | display: "flex", 5 | flexDirection: "column", 6 | alignItems: "center", 7 | justifyContent: "center", 8 | width: "100%", 9 | margin: "0 auto", 10 | padding: "0 20px", 11 | boxSizing: "border-box", 12 | borderRadius: "5px", 13 | backgroundColor: "#fff", 14 | boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)", 15 | "& > *": { 16 | margin: "10px 0", 17 | }, 18 | "& > button": { 19 | marginTop: "20px", 20 | }, 21 | maxWidth: "300px", 22 | [props.theme.breakpoints.up("sm")]: { 23 | maxWidth: "350px", 24 | }, 25 | [props.theme.breakpoints.up("md")]: { 26 | maxWidth: "400px", 27 | }, 28 | [props.theme.breakpoints.up("lg")]: { 29 | maxWidth: "600px", 30 | }, 31 | })); 32 | -------------------------------------------------------------------------------- /frontend/src/components/CreateOrganisationForm/createOrganisationForm.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | 3 | export const FormContainer = styled("div")(() => ({ 4 | display: "flex", 5 | padding: "16px 9px", 6 | })); 7 | 8 | export const ImageUploadWrapper = styled("label")(() => ({ 9 | display: "block", 10 | minWidth: "60px", 11 | height: "60px", 12 | lineHeight: "60px", 13 | margin: "0px", 14 | })); 15 | 16 | export const TextInput = styled("input")(() => ({ 17 | height: "30px", 18 | width: "133px", 19 | margin: "15px", 20 | borderRadius: "12px", 21 | borderColor: "black", 22 | borderWidth: "1px", 23 | padding: "10px", 24 | })); 25 | 26 | export const UploadButton = styled("button")(() => ({ 27 | height: "30px", 28 | width: "30px", 29 | marginTop: "15px", 30 | borderRadius: "12px", 31 | borderColor: "black", 32 | borderWidth: "1px", 33 | })); 34 | -------------------------------------------------------------------------------- /frontend/src/pages/landing/components/SponsorLogos.tsx: -------------------------------------------------------------------------------- 1 | import janeStreetLogo from "assets/LandingPage/janestreet.png"; 2 | import tiktokLogo from "assets/LandingPage/tiktok.svg"; 3 | 4 | import "twin.macro"; 5 | 6 | const SponsorLogos = () => ( 7 |
    8 |
    9 |

    Sponsored by:

    10 | 18 |
    19 |
    20 | ); 21 | 22 | export default SponsorLogos; 23 | -------------------------------------------------------------------------------- /frontend/src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import tw from "twin.macro"; 2 | 3 | import type { PropsWithChildren } from "react"; 4 | 5 | const Input = tw.input`hocus:border-blue-300 form-input w-96 rounded-md border-gray-300 shadow-sm transition invalid:border-red-300 invalid:text-red-600 invalid:ring-red-200/50 invalid:hover:border-red-400 focus:ring focus:ring-blue-200/50`; 6 | const Label = tw.label`flex flex-col`; 7 | 8 | type LabelTextProps = { 9 | /** 10 | * Whether this form control is required. 11 | * If this is true, a red asterisk is shown next to the label text 12 | */ 13 | required?: true; 14 | }; 15 | const LabelText = ({ 16 | required, 17 | children, 18 | }: PropsWithChildren) => ( 19 | 20 | {children} 21 | {required && *} 22 | 23 | ); 24 | 25 | export default Object.assign(Input, { Label, LabelText }); 26 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/review/rankings/FinalRatingCandidateCard/finalRatingCandidateCard.styled.ts: -------------------------------------------------------------------------------- 1 | import { Card, Chip, Grid } from "@mui/material"; 2 | import { green, red } from "@mui/material/colors"; 3 | import { styled } from "@mui/material/styles"; 4 | 5 | export const CandidateCard = styled(Card, { 6 | shouldForwardProp: (prop) => prop !== "reject", 7 | })<{ reject: boolean }>(({ reject }) => ({ 8 | margin: "0.5rem 0", 9 | // backgroundColor: reject ? red[50] : green[50], 10 | borderLeft: `5px solid ${reject ? red[500] : green[500]}`, 11 | cursor: "pointer", 12 | })); 13 | 14 | export const GridCandidateName = styled(Grid)` 15 | flex-grow: 1; 16 | margin-left: 1rem; 17 | `; 18 | 19 | export const RatingChip = styled(Chip)` 20 | margin: 0 0.2rem; 21 | font-size: 14px; 22 | background-color: white; 23 | `; 24 | 25 | export const BullSpan = styled("span")` 26 | margin: 0 0.5rem; 27 | `; 28 | -------------------------------------------------------------------------------- /frontend/src/components/PulsingBar.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | import { Pulse } from "styles/animations"; 4 | 5 | import type { ComponentProps } from "react"; 6 | 7 | const Bar = styled(Pulse, { 8 | ...tw`h-3 rounded-sm`, 9 | 10 | variants: { 11 | color: { 12 | black: tw`bg-black`, 13 | red: tw`bg-red-600`, 14 | }, 15 | standalone: { 16 | true: tw`bg-opacity-10!`, 17 | false: tw`(bg-opacity-5 first:bg-opacity-[0.15])!`, 18 | }, 19 | }, 20 | 21 | defaultVariants: { 22 | standalone: false, 23 | color: "black", 24 | }, 25 | }); 26 | 27 | const PulsingBar = ({ 28 | animationDelay = 0, 29 | ...props 30 | }: ComponentProps & { animationDelay?: number }) => ( 31 | 36 | ); 37 | 38 | export default PulsingBar; 39 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/review/RolesSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useParams } from "react-router-dom"; 2 | import "twin.macro"; 3 | 4 | import NavCard from "components/NavCard"; 5 | import NavItem from "components/NavCard/NavItem"; 6 | 7 | import type { Role } from "types/api"; 8 | 9 | type Props = { 10 | roles: Role[]; 11 | }; 12 | const RolesSidebar = ({ roles }: Props) => { 13 | const roleId = Number(useParams().roleId); 14 | return ( 15 | 19 | {roles.map((role) => ( 20 |
  • 21 | 26 | {role.name} 27 | 28 |
  • 29 | ))} 30 |
    31 | ); 32 | }; 33 | 34 | export default RolesSidebar; 35 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Grab the latest Node base image 2 | FROM node:18.16.0-alpine as builder 3 | 4 | # Set the current working directory inside the container 5 | WORKDIR /app 6 | 7 | COPY package.json yarn.lock ./ 8 | RUN yarn install --frozen-lockfile 9 | 10 | ARG VITE_API_BASE_URL 11 | ARG VITE_OAUTH_CALLBACK_URL 12 | ENV VITE_API_BASE_URL $VITE_API_BASE_URL 13 | ENV VITE_OAUTH_CALLBACK_URL $VITE_OAUTH_CALLBACK_URL 14 | 15 | COPY . . 16 | 17 | RUN yarn run build 18 | 19 | # nginx state for serving content 20 | FROM nginx:1.25.0-alpine 21 | COPY ./.nginx/nginx.conf /etc/nginx/nginx.conf 22 | # Set working directory to nginx asset directory 23 | WORKDIR /usr/share/nginx/html 24 | # Remove default nginx static assets 25 | RUN rm -rf ./* 26 | # Copy static assets from builder stage 27 | COPY --from=builder /app/dist . 28 | 29 | EXPOSE 80 30 | 31 | # Containers run nginx with global directives and daemon off 32 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 33 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "lockFileMaintenance": { "enabled": true, "automerge": true }, 4 | "prHourlyLimit": 2, 5 | "labels": ["dependencies"], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["patch", "pin", "digest"], 9 | "automerge": true, 10 | "automergeType": "branch" 11 | }, 12 | { 13 | "matchPackageNames": ["node"], 14 | "groupName": "node", 15 | "ignoreUnstable": false 16 | }, 17 | { 18 | "matchUpdateTypes": ["patch"], 19 | "groupName": "weekly patch updates", 20 | "schedule": ["before 5am every monday"], 21 | "addLabels": ["deps: patches"] 22 | }, 23 | { 24 | "matchUpdateTypes": ["minor"], 25 | "groupName": "weekly minor updates", 26 | "schedule": ["before 5am every monday"], 27 | "addLabels": ["deps: minor"] 28 | }, 29 | { 30 | "matchUpdateTypes": ["major"], 31 | "addLabels": ["deps: major"] 32 | }, 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/Preview/reviewTab.styled.ts: -------------------------------------------------------------------------------- 1 | import { Button, Grid } from "@mui/material"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | import { COLOURS } from "../../../utils/constants"; 5 | 6 | export const SubmitWrapper = styled("div")` 7 | display: flex; 8 | justify-content: center; 9 | margin-bottom: 50px; 10 | `; 11 | 12 | export const PublishButton = styled(Button)` 13 | font-size: 20px; 14 | margin-left: 10px; 15 | `; 16 | 17 | export const CreateDraftButton = styled(Button)` 18 | font-size: 20px; 19 | margin-right: 10px; 20 | `; 21 | 22 | export const InfoTextBox = styled("div")` 23 | display: flex; 24 | background-color: ${COLOURS.lightGrey}; 25 | padding: 3%; 26 | flex-direction: column; 27 | `; 28 | 29 | export const InfoText = styled("div")` 30 | text-align: center; 31 | padding: 5px; 32 | `; 33 | 34 | export const CampaignCardGrid = styled(Grid)` 35 | padding-top: 30px; 36 | margin-bottom: 50px; 37 | margin-top: 0px; 38 | `; 39 | -------------------------------------------------------------------------------- /frontend/src/components/Dropdown/DropdownOption.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from "@headlessui/react"; 2 | import tw from "twin.macro"; 3 | 4 | import type { ComponentProps, MouseEvent, ReactElement } from "react"; 5 | 6 | type Props = { 7 | name: string; 8 | onClick: () => void; 9 | icon: ReactElement; 10 | } & ComponentProps; 11 | 12 | const DropdownOption = ({ name, onClick, icon, ...props }: Props) => ( 13 | 14 | {({ close }) => ( 15 | 28 | )} 29 | 30 | ); 31 | 32 | export default DropdownOption; 33 | -------------------------------------------------------------------------------- /frontend/src/components/ApplicationPreviewer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Typography } from "@mui/material"; 2 | import { Fragment } from "react"; 3 | 4 | import { Answer, NoAnswer, Question } from "./applicationPreviewer.styled"; 5 | 6 | import type { ApplicationWithQuestions } from "pages/admin/types"; 7 | 8 | type Props = { 9 | application: ApplicationWithQuestions; 10 | }; 11 | 12 | const ApplicationPreviewer = ({ application }: Props) => ( 13 | 14 | 15 | {application.zId} 16 | 17 | 18 | {application.questions.map((question, idx) => ( 19 | // eslint-disable-next-line react/no-array-index-key 20 | 21 | {question.question} 22 | 23 | {question.answer ? ( 24 | {question.answer} 25 | ) : ( 26 | No answer provided. 27 | )} 28 | 29 | ))} 30 | 31 | ); 32 | 33 | export default ApplicationPreviewer; 34 | -------------------------------------------------------------------------------- /frontend/src/components/MarkChip/markChip.styled.ts: -------------------------------------------------------------------------------- 1 | import { Chip } from "@mui/material"; 2 | import { 3 | deepOrange, 4 | green, 5 | grey, 6 | lightGreen, 7 | red, 8 | yellow, 9 | } from "@mui/material/colors"; 10 | import { styled } from "@mui/material/styles"; 11 | import { darken } from "@mui/system"; 12 | 13 | const chipColor = (mark: number) => { 14 | if (mark <= 1) return red[700]; 15 | if (mark <= 2) return deepOrange[500]; 16 | if (mark <= 3) return yellow[800]; 17 | if (mark <= 4) return lightGreen[500]; 18 | if (mark < 5) return green[500]; 19 | return green[700]; 20 | }; 21 | 22 | export const ColoredChip = styled(Chip, { 23 | shouldForwardProp: (prop) => prop !== "mark" && prop !== "colored", 24 | })<{ mark: number; colored: boolean }>(({ mark, colored }) => ({ 25 | backgroundColor: colored ? chipColor(mark) : grey[200], 26 | fontSize: "14px", 27 | fontWeight: colored ? "bold" : "normal", 28 | color: colored ? "white" : "black", 29 | "&:hover": { 30 | backgroundColor: colored ? darken(chipColor(mark), 0.1) : grey[300], 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/AdminContent/AdminCampaignContent.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from "react-router-dom"; 2 | import "twin.macro"; 3 | 4 | import { CampaignCard } from "components"; 5 | 6 | import type { Campaign } from "../types"; 7 | import type { Dispatch, SetStateAction } from "react"; 8 | 9 | type Props = { 10 | campaigns: Campaign[]; 11 | setCampaigns: Dispatch>; 12 | orgLogo: string; 13 | }; 14 | 15 | const AdminCampaignContent = ({ campaigns, setCampaigns, orgLogo }: Props) => ( 16 |
    17 | {campaigns.map((c) => ( 18 | 32 | ))} 33 |
    34 | ); 35 | 36 | export default AdminCampaignContent; 37 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/review/rankings/FinalRatingApplicationComments/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogContent, DialogTitle, Divider } from "@mui/material"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | import ApplicationPreviewer from "components/ApplicationPreviewer"; 5 | 6 | import { useRoles } from "../.."; 7 | 8 | import type { ApplicationWithQuestions } from "pages/admin/types"; 9 | 10 | type Props = { 11 | name: string; 12 | open: boolean; 13 | handleClose: () => void; 14 | application: ApplicationWithQuestions; 15 | }; 16 | 17 | const FinalRatingApplicationComments = ({ 18 | name, 19 | open, 20 | handleClose, 21 | application, 22 | }: Props) => { 23 | const roles = useRoles(); 24 | const roleId = Number(useParams().roleId); 25 | 26 | return ( 27 | 28 | {`${name}'s Application for ${roles[roleId]?.name}`} 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default FinalRatingApplicationComments; 38 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/Campaign/campaignTab.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { Container, TextField } from "@mui/material"; 3 | import { styled as muiStyled } from "@mui/material/styles"; 4 | 5 | export const CampaignContainer = muiStyled(Container)(() => ({ 6 | display: "flex", 7 | flexDirection: "column", 8 | width: "800px", 9 | minHeight: "600px", 10 | })); 11 | 12 | export const CampaignTextField = muiStyled(TextField)(() => ({ 13 | margin: "1% 0", 14 | })); 15 | 16 | export const CoverImage = styled.img` 17 | max-width: 100%; 18 | `; 19 | 20 | export const CampaignRowDiv = styled.div` 21 | display: flex; 22 | flex-direction: row; 23 | justify-content: space-around; 24 | margin: 1% 0; 25 | width: 100%; 26 | padding-left: 16.5%; 27 | padding-right: 16.5%; 28 | `; 29 | 30 | export const SwitchRowDiv = muiStyled(CampaignRowDiv)` 31 | justify-content: center; 32 | padding-bottom: 10px; 33 | `; 34 | 35 | export const CampaignDropzone = styled.div` 36 | text-align: center; 37 | padding: 20px; 38 | border: 3px dashed #eeeeee; 39 | background-color: #fafafa; 40 | color: #bdbdbd; 41 | `; 42 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/Roles/Question.tsx: -------------------------------------------------------------------------------- 1 | import ClearIcon from "@mui/icons-material/Clear"; 2 | import { IconButton, TextField } from "@mui/material"; 3 | 4 | import { 5 | QuestionContent as Content, 6 | QuestionTitle as Title, 7 | } from "./rolesTab.styled"; 8 | 9 | import type { Question as IQuestion } from "../types"; 10 | import type { ChangeEvent, MouseEventHandler } from "react"; 11 | 12 | type Props = { 13 | questionNumber: number; 14 | question: IQuestion; 15 | handleQuestionInput: ( 16 | e: ChangeEvent, 17 | qId: number 18 | ) => void; 19 | onQuestionDelete: MouseEventHandler; 20 | }; 21 | 22 | const Question = ({ 23 | questionNumber, 24 | question, 25 | handleQuestionInput, 26 | onQuestionDelete, 27 | }: Props) => ( 28 | <> 29 | Question {questionNumber} 30 | 31 | handleQuestionInput(e, question.id)} 37 | /> 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | 45 | export default Question; 46 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: ["Nunito", ...defaultTheme.fontFamily.sans], 10 | }, 11 | colors: { 12 | brand: { 13 | 50: "hsl(220, 97%, 97%)", 14 | 75: "hsl(220, 97%, 95%)", 15 | 100: "hsl(220, 96%, 93%)", 16 | 200: "hsl(220, 95%, 85%)", 17 | 300: "hsl(220, 95%, 78%)", 18 | 400: "hsl(220, 94%, 70%)", 19 | 500: "hsl(220, 94%, 60%)", 20 | 600: "hsl(220, 93%, 54%)", 21 | 700: "hsl(220, 93%, 48%)", 22 | 800: "hsl(220, 85%, 40%)", 23 | 900: "hsl(220, 70%, 30%)", 24 | 950: "hsl(220, 50%, 20%)", 25 | }, 26 | }, 27 | minWidth: defaultTheme.width, 28 | maxWidth: defaultTheme.width, 29 | minHeight: defaultTheme.height, 30 | }, 31 | }, 32 | plugins: [ 33 | require("tailwindcss-gradient"), 34 | require("@tailwindcss/forms")({ 35 | strategy: "class", 36 | }), 37 | require("@tailwindcss/aspect-ratio"), 38 | require("@headlessui/tailwindcss"), 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/Roles/CreateRoleForm.tsx: -------------------------------------------------------------------------------- 1 | import AddIcon from "@mui/icons-material/Add"; 2 | import { IconButton } from "@mui/material"; 3 | 4 | import { 5 | CreateRoleFormControl as FormControl, 6 | CreateRoleFormGroup as FormGroup, 7 | Name, 8 | Quantity, 9 | } from "./rolesTab.styled"; 10 | 11 | import type { Dispatch, SetStateAction } from "react"; 12 | 13 | type Props = { 14 | newRoleQty: number; 15 | setNewRoleQty: Dispatch>; 16 | newRoleName: string; 17 | setNewRoleName: Dispatch>; 18 | addRole: () => void; 19 | }; 20 | 21 | const CreateRoleForm = ({ 22 | newRoleQty, 23 | setNewRoleQty, 24 | newRoleName, 25 | setNewRoleName, 26 | addRole, 27 | }: Props) => ( 28 | 29 | 30 | setNewRoleQty(Number(e.target.value))} 34 | min={1} 35 | /> 36 | setNewRoleName(e.currentTarget.value)} 40 | /> 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | 48 | export default CreateRoleForm; 49 | -------------------------------------------------------------------------------- /backend/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /frontend/src/pages/landing/components/DashboardButton.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronRightIcon } from "@heroicons/react/24/solid"; 2 | import tw, { styled } from "twin.macro"; 3 | 4 | import type { ComponentProps, ElementType, PropsWithChildren } from "react"; 5 | 6 | const Button = tw.button`relative flex w-max items-center justify-center rounded border-0 px-3 py-2 font-normal text-[#191d24] outline-none ring-blue-500 transition hover:text-black focus-visible:text-black focus-visible:ring`; 7 | 8 | const Bg = tw.div`absolute inset-0 z-[-1] rounded from-fuchsia-200 to-indigo-200 transition bg-gradient-120`; 9 | 10 | const ButtonShadow = styled(Bg, { 11 | ...tw`blur-sm filter group-hover:translate-y-0.5 group-hover:blur group-focus-visible:blur group-active:blur-sm`, 12 | }); 13 | 14 | const ButtonBg = styled(Bg, { 15 | ...tw`opacity-50 group-hover:opacity-100 group-focus-visible:opacity-100`, 16 | }); 17 | 18 | type Props = ComponentProps & { 19 | as?: T; 20 | } & ComponentProps; 21 | 22 | const DashboardButton = ({ 23 | children, 24 | ...props 25 | }: PropsWithChildren>) => ( 26 | 32 | ); 33 | 34 | export default DashboardButton; 35 | -------------------------------------------------------------------------------- /frontend/src/components/Dropzone.tsx: -------------------------------------------------------------------------------- 1 | import BaseDropzone from "react-dropzone"; 2 | import tw from "twin.macro"; 3 | 4 | import type { ComponentProps, PropsWithChildren } from "react"; 5 | 6 | const Dropzone = ({ 7 | children, 8 | ...props 9 | }: PropsWithChildren< 10 | Omit, "children"> 11 | >) => ( 12 | 24 | {({ getRootProps, getInputProps, isDragActive }) => ( 25 |
    26 |
    32 | {/* eslint-disable-next-line react/jsx-props-no-spreading -- this *should* be fine here */} 33 | 34 | {children} 35 |
    36 |
    37 | )} 38 |
    39 | ); 40 | 41 | export default Dropzone; 42 | -------------------------------------------------------------------------------- /backend/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | 7 | [lib] 8 | name = "backend" 9 | path = "src/lib.rs" 10 | 11 | [[bin]] 12 | name = "server" 13 | path = "src/bin.rs" 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | rocket = { version = "0.5.0-rc.2", features = ["json"] } 18 | rocket_sync_db_pools = { version = "0.1.0-rc.2", features = ["diesel_postgres_pool"] } 19 | # pull rocket_cors from git master until crates.io artifact builds on stable 20 | rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" } 21 | diesel = { version = "1.4.8", features = ["postgres", "r2d2", "chrono"] } 22 | diesel-derive-enum = { version = "1", features = ["postgres"] } 23 | dotenv = "0.15.0" 24 | dotenv_codegen = "0.15.0" 25 | reqwest = { version = "0.11.13", features = ["json"] } 26 | jsonwebtoken = "8.3.0" 27 | serde = {version = "1.0", features = ["derive"] } 28 | serde_json = "1.0.89" 29 | chrono = { version = "0.4", features = ["serde"] } 30 | itertools = "0.10.5" 31 | once_cell = "1.18.0" 32 | diesel_migrations = "1.4.0" 33 | figment = { version = "0.10", features = ["env", "toml", "json"] } 34 | image = "0.24.4" 35 | strum = { version = "0.24", features = ["derive"] } 36 | webp = "0.2" 37 | 38 | [dependencies.uuid] 39 | version = "1.3.3" 40 | features = ["v4", "fast-rng", "macro-diagnostics"] 41 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/types.ts: -------------------------------------------------------------------------------- 1 | import type { Dispatch, SetStateAction } from "react"; 2 | 3 | export type Role = { 4 | id: number; 5 | title: string; 6 | quantity: number; 7 | }; 8 | 9 | export type Question = { 10 | id: number; 11 | text: string; 12 | roles: Set; 13 | required?: boolean; 14 | }; 15 | 16 | export type Answers = { 17 | [id: number]: string; 18 | }; 19 | 20 | export type Campaign = { 21 | tab: number; 22 | setTab: Dispatch>; 23 | campaignName: string; 24 | setCampaignName: Dispatch>; 25 | startDate: Date; 26 | setStartDate: Dispatch>; 27 | endDate: Date; 28 | setEndDate: Dispatch>; 29 | description: string; 30 | setDescription: Dispatch>; 31 | interviewStage: boolean; 32 | setInterviewStage: Dispatch>; 33 | scoringStage: boolean; 34 | setScoringStage: Dispatch>; 35 | cover: File | null; 36 | setCover: Dispatch>; 37 | error: string | null; 38 | setError: Dispatch>; 39 | roles: Role[]; 40 | setRoles: Dispatch>; 41 | roleSelected: number; 42 | setRoleSelected: Dispatch>; 43 | questions: Question[]; 44 | setQuestions: Dispatch>; 45 | answers: Answers; 46 | setAnswers: Dispatch>; 47 | }; 48 | -------------------------------------------------------------------------------- /backend/server/src/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::database::{ 2 | models::{AdminInfoResponse, OrganisationInfo, OrganisationUser, SuperUser, User}, 3 | Database, 4 | }; 5 | use crate::error::JsonErr; 6 | use rocket::{get, http::Status, post, serde::json::Json}; 7 | 8 | #[get("/")] 9 | pub async fn get(user: User, db: Database) -> Json { 10 | Json(AdminInfoResponse { 11 | organisations: db 12 | .run(move |conn| { 13 | user.get_all_org_ids_belonging(conn) 14 | .into_iter() 15 | .filter(|org_id| { 16 | OrganisationUser::organisation_admin_level(*org_id, user.id, conn) 17 | .is_at_least_director() 18 | .check() 19 | .is_ok() 20 | }) 21 | .map(|org| OrganisationInfo::new(org, conn)) 22 | .collect::>() 23 | }) 24 | .await, 25 | }) 26 | } 27 | 28 | #[post("/make_superuser", data = "")] 29 | pub async fn make_superuser( 30 | _user: SuperUser, 31 | db: Database, 32 | email: Json, 33 | ) -> Result<(), JsonErr<()>> { 34 | db.run(move |conn| { 35 | User::get_from_email(conn, &email.into_inner()) 36 | .map(|user| user.make_superuser(conn).ok()) 37 | .flatten() 38 | }) 39 | .await 40 | .ok_or(JsonErr((), Status::BadRequest)) 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { Tab } from "@headlessui/react"; 2 | import { Fragment } from "react"; 3 | import tw, { styled } from "twin.macro"; 4 | 5 | import type { ComponentProps, ReactNode } from "react"; 6 | 7 | const TabList = styled(Tab.List, { 8 | ...tw`flex gap-1`, 9 | 10 | variants: { 11 | /** 12 | * Display the tab list in a vertical column rather than a row. 13 | * Make sure to include the `vertical` prop on the containing `Tab.Group` for accessibility if this is true. 14 | */ 15 | vertical: { 16 | true: tw`flex-col`, 17 | }, 18 | }, 19 | }); 20 | 21 | const TabButton = styled("button", { 22 | ...tw`rounded border border-transparent px-3 py-2 outline-none ring-blue-600 ring-opacity-30 transition focus:border-indigo-500 focus:ring`, 23 | 24 | variants: { 25 | active: { 26 | true: tw`border-gray-300 bg-white text-indigo-600 shadow hover:border-indigo-600 hover:text-indigo-700`, 27 | false: tw`text-gray-700 hover:text-indigo-600`, 28 | }, 29 | }, 30 | }); 31 | 32 | type Props = ComponentProps & { 33 | tabs: { id: number; contents: ReactNode }[]; 34 | }; 35 | const Tabs = ({ tabs, vertical, ...props }: Props) => ( 36 | 37 | {tabs.map(({ id, contents }) => ( 38 | 39 | {({ selected }) => {contents}} 40 | 41 | ))} 42 | 43 | ); 44 | 45 | export default Tabs; 46 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/CampaignGrid/CampaignLoading.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | import PulsingBar from "components/PulsingBar"; 4 | import { Pulse, pulseCss } from "styles/animations"; 5 | 6 | const Button = styled(Pulse, { 7 | ...tw`ml-auto rounded-[0.2rem] px-2 py-1.5 text-transparent`, 8 | ...pulseCss, 9 | 10 | variants: { 11 | status: { 12 | pending: tw`bg-[hsl(220, 60%, 90%)]`, 13 | open: tw`bg-[hsl(220, 93%, 60%)]`, 14 | closed: tw`hidden`, 15 | }, 16 | }, 17 | 18 | defaultVariants: { 19 | status: "open", 20 | }, 21 | }); 22 | 23 | const Bars = tw.div`flex flex-col gap-1`; 24 | 25 | type Props = { 26 | status?: "pending" | "open" | "closed"; 27 | animationDelay?: number; 28 | }; 29 | const CampaignLoading = ({ status, animationDelay = 0 }: Props) => ( 30 |
    31 |
    32 | 33 | 34 | 35 | 36 | 37 | 40 |
    41 |
    42 |
    43 | ); 44 | 45 | export default CampaignLoading; 46 | -------------------------------------------------------------------------------- /frontend/src/components/CreateOrganisationForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { OrgIconImage } from "../AdminSideBar/adminSidebar.styled"; 2 | 3 | import { 4 | FormContainer, 5 | ImageUploadWrapper, 6 | TextInput, 7 | UploadButton, 8 | } from "./createOrganisationForm.styled"; 9 | import newImageIcon from "./new_image_icon.png"; 10 | 11 | import type { ChangeEventHandler, Dispatch, SetStateAction } from "react"; 12 | 13 | type Props = { 14 | uploadedImage: { image: File | null; url: string | null }; 15 | onFileChange: ChangeEventHandler; 16 | inputText: string; 17 | setInputText: Dispatch>; 18 | onUpload: () => void; 19 | }; 20 | 21 | const CreateOrganisationForm = ({ 22 | uploadedImage, 23 | onFileChange, 24 | inputText, 25 | setInputText, 26 | onUpload, 27 | }: Props) => ( 28 | 29 | 30 | 35 | 41 | 42 | setInputText(e.target.value)} 47 | /> 48 | + 49 | 50 | ); 51 | 52 | export default CreateOrganisationForm; 53 | -------------------------------------------------------------------------------- /frontend/src/pages/create_campaign/Roles/RoleListItem.tsx: -------------------------------------------------------------------------------- 1 | import ClearIcon from "@mui/icons-material/Clear"; 2 | import EditIcon from "@mui/icons-material/Edit"; 3 | import { Divider, IconButton, ListItemIcon, ListItemText } from "@mui/material"; 4 | 5 | import { 6 | RoleListItemButton as ListItemButton, 7 | RoleListItemContent as ListItemContent, 8 | RoleQuantity, 9 | } from "./rolesTab.styled"; 10 | 11 | import type { Campaign, Role } from "../types"; 12 | import type { MouseEventHandler } from "react"; 13 | 14 | type Props = { 15 | role: Role; 16 | roleSelected: Campaign["roleSelected"]; 17 | setRoleSelected: Campaign["setRoleSelected"]; 18 | onRoleDelete: MouseEventHandler; 19 | }; 20 | 21 | const RoleListItem = ({ 22 | role, 23 | roleSelected, 24 | setRoleSelected, 25 | onRoleDelete, 26 | }: Props) => ( 27 | <> 28 | 29 | setRoleSelected(role.id)}> 30 | {role.quantity} 31 | {role.title} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | 48 | export default RoleListItem; 49 | -------------------------------------------------------------------------------- /frontend/src/components/CampaignCard/Popup.tsx: -------------------------------------------------------------------------------- 1 | import tw, { styled } from "twin.macro"; 2 | 3 | import Modal from "components/Modal"; 4 | 5 | import type { Position } from "./types"; 6 | import type { CampaignWithRoles } from "types/api"; 7 | 8 | const PositionItem = styled.li({ 9 | ...tw`rounded px-2 py-1.5`, 10 | 11 | variants: { 12 | status: { 13 | Draft: tw`bg-gray-50`, 14 | Pending: tw`bg-gray-100`, 15 | Rejected: tw`bg-red-100`, 16 | Success: tw`bg-green-100`, 17 | undefined: tw`bg-gray-200`, 18 | }, 19 | }, 20 | }); 21 | 22 | type Props = { 23 | appliedFor: CampaignWithRoles["applied_for"]; 24 | positions: Position[]; 25 | open: boolean; 26 | closeModal: () => void; 27 | }; 28 | const Popup = ({ appliedFor, positions, open, closeModal }: Props) => { 29 | const positionsMap = Object.fromEntries( 30 | positions.map(({ id, ...position }) => [id, position]) 31 | ); 32 | const positionStatuses = Object.fromEntries( 33 | appliedFor.map(([id, status]) => [ 34 | id, 35 | { position: positionsMap[id].name, status }, 36 | ]) 37 | ); 38 | 39 | return ( 40 | 46 |
      47 | {positions.map((pos) => ( 48 | 49 | {pos.name} 50 | 51 | ))} 52 |
    53 |
    54 | ); 55 | }; 56 | 57 | export default Popup; 58 | -------------------------------------------------------------------------------- /frontend/src/pages/admin/AdminLoading.tsx: -------------------------------------------------------------------------------- 1 | import "twin.macro"; 2 | 3 | import Container from "components/Container"; 4 | import PulsingBar from "components/PulsingBar"; 5 | import CampaignLoading from "pages/dashboard/CampaignGrid/CampaignLoading"; 6 | 7 | const AdminLoading = () => ( 8 |
    9 |
    10 | {Array(4) 11 | .fill(null) 12 | .map((_, i) => ( 13 | 20 | ))} 21 |
    22 | 23 |
    24 |
    25 | 26 | 27 |
    28 |
    29 | 30 | 31 | 32 |
    33 |
    34 |
    35 | {Array(5) 36 | .fill(null) 37 | .map((_, i) => ( 38 | 44 | ))} 45 |
    46 |
    47 |
    48 | ); 49 | 50 | export default AdminLoading; 51 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | branches: [main, "renovate/*"] 6 | push: 7 | branches: ["renovate/*"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Mock an env file 18 | run: | 19 | echo "DATABASE_URL=test_url" >> backend/.env 20 | echo "JWT_SECRET=test_secret" >> backend/.env 21 | echo "GOOGLE_CLIENT_ID=test" >> backend/.env 22 | echo "GOOGLE_CLIENT_SECRET=test" >> backend/.env 23 | echo "GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback" >> backend/.env 24 | echo "ROCKET_DATABASES='{}'" >> backend/.env 25 | # selecting a toolchain either by action or manual `rustup` calls should happen 26 | # before the plugin, as it uses the current rustc version as its cache key 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | - uses: Swatinem/rust-cache@v2 32 | with: 33 | working-directory: backend/ 34 | - name: Build 35 | run: cargo build --manifest-path backend/server/Cargo.toml 36 | - name: Cargo Clippy 37 | run: cargo clippy --manifest-path backend/server/Cargo.toml 38 | - name: RustFmt 39 | run: cargo fmt --manifest-path backend/server/Cargo.toml 40 | - name: Build 41 | run: cargo build --manifest-path backend/seed_data/Cargo.toml 42 | - name: Cargo Clippy 43 | run: cargo clippy --manifest-path backend/seed_data/Cargo.toml 44 | - name: RustFmt 45 | run: cargo fmt --manifest-path backend/seed_data/Cargo.toml 46 | -------------------------------------------------------------------------------- /frontend/src/pages/application_page/ApplicationForm.tsx: -------------------------------------------------------------------------------- 1 | import "twin.macro"; 2 | 3 | import { ChevronRightIcon } from "@heroicons/react/20/solid"; 4 | 5 | import Button from "components/Button"; 6 | import Card from "components/Card"; 7 | import Textarea from "components/Textarea"; 8 | 9 | import type { RoleQuestions } from "./types"; 10 | import type { Role } from "types/api"; 11 | 12 | type Props = { 13 | roles: Role[]; 14 | rolesSelected: number[]; 15 | roleQuestions: RoleQuestions; 16 | answers: { [question: number]: string }; 17 | setAnswer: (_question: number, _answer: string) => void; 18 | onSubmit: () => void; 19 | }; 20 | const ApplicationForm = ({ 21 | roles, 22 | rolesSelected, 23 | roleQuestions, 24 | answers, 25 | setAnswer, 26 | onSubmit, 27 | }: Props) => ( 28 | 29 |

    Questions

    30 | {rolesSelected.map((role) => ( 31 |
    32 |

    {roles.find((r) => r.id === role)?.name}

    33 | {roleQuestions[role].map(({ id, text }) => ( 34 |