├── .nvmrc
├── apps
├── docs
│ ├── static
│ │ ├── .nojekyll
│ │ └── img
│ │ │ ├── favicon.ico
│ │ │ └── docusaurus.png
│ ├── babel.config.js
│ ├── docs
│ │ ├── 04-web
│ │ │ ├── _category_.json
│ │ │ ├── ui.mdx
│ │ │ └── webapp.mdx
│ │ ├── 03-backend
│ │ │ ├── _category_.json
│ │ │ └── api.mdx
│ │ └── 05-next-steps.mdx
│ ├── tsconfig.json
│ ├── sidebars.ts
│ ├── src
│ │ ├── css
│ │ │ └── custom.css
│ │ └── pages
│ │ │ └── index.md
│ └── package.json
├── web
│ ├── postcss.config.cjs
│ ├── public
│ │ ├── favicon.ico
│ │ ├── fonts
│ │ │ └── hanken-grotesk
│ │ │ │ ├── HankenGrotesk-Black.ttf
│ │ │ │ ├── HankenGrotesk-Bold.ttf
│ │ │ │ ├── HankenGrotesk-Light.ttf
│ │ │ │ ├── HankenGrotesk-Thin.ttf
│ │ │ │ ├── HankenGrotesk-Italic.ttf
│ │ │ │ ├── HankenGrotesk-Medium.ttf
│ │ │ │ ├── HankenGrotesk-Regular.ttf
│ │ │ │ ├── HankenGrotesk-SemiBold.ttf
│ │ │ │ ├── HankenGrotesk-BoldItalic.ttf
│ │ │ │ ├── HankenGrotesk-ExtraBold.ttf
│ │ │ │ ├── HankenGrotesk-ExtraLight.ttf
│ │ │ │ ├── HankenGrotesk-ThinItalic.ttf
│ │ │ │ ├── HankenGrotesk-BlackItalic.ttf
│ │ │ │ ├── HankenGrotesk-LightItalic.ttf
│ │ │ │ ├── HankenGrotesk-MediumItalic.ttf
│ │ │ │ ├── HankenGrotesk-ExtraBoldItalic.ttf
│ │ │ │ ├── HankenGrotesk-SemiBoldItalic.ttf
│ │ │ │ └── HankenGrotesk-ExtraLightItalic.ttf
│ │ ├── svg
│ │ │ ├── checkmark.svg
│ │ │ ├── toastX.svg
│ │ │ ├── perkCheck.svg
│ │ │ ├── toastCheck.svg
│ │ │ ├── perkCross.svg
│ │ │ ├── bookmark.svg
│ │ │ ├── filledBookmark.svg
│ │ │ ├── hoverBookmark.svg
│ │ │ ├── burgerMenu.svg
│ │ │ ├── compareAdd.svg
│ │ │ ├── compareRole.svg
│ │ │ ├── reviewReport.svg
│ │ │ ├── star.svg
│ │ │ ├── apartment.svg
│ │ │ ├── magnifyingGlass.svg
│ │ │ ├── work.svg
│ │ │ ├── xSymbol.svg
│ │ │ ├── verified.svg
│ │ │ ├── defaultProfile.svg
│ │ │ └── logoOutline.svg
│ │ └── logo.svg
│ ├── test
│ │ └── app.test.ts
│ ├── src
│ │ ├── app
│ │ │ ├── _components
│ │ │ │ ├── form
│ │ │ │ │ ├── form-card.tsx
│ │ │ │ │ ├── form-section.tsx
│ │ │ │ │ └── sections
│ │ │ │ │ │ └── index.ts
│ │ │ │ ├── auth
│ │ │ │ │ ├── actions.ts
│ │ │ │ │ ├── login-button-client.tsx
│ │ │ │ │ ├── logout-button.tsx
│ │ │ │ │ └── login-button.tsx
│ │ │ │ ├── themed
│ │ │ │ │ └── onboarding
│ │ │ │ │ │ ├── input.tsx
│ │ │ │ │ │ └── form.tsx
│ │ │ │ ├── cooper-logo.tsx
│ │ │ │ ├── body-logo.tsx
│ │ │ │ ├── back-button.tsx
│ │ │ │ ├── onboarding
│ │ │ │ │ ├── post-onboarding
│ │ │ │ │ │ ├── coop-prompt.tsx
│ │ │ │ │ │ ├── browse-around-prompt.tsx
│ │ │ │ │ │ └── welcome-dialog.tsx
│ │ │ │ │ ├── onboarding-wrapper.tsx
│ │ │ │ │ └── dialog.tsx
│ │ │ │ ├── loading-results.tsx
│ │ │ │ ├── companies
│ │ │ │ │ ├── company-about.tsx
│ │ │ │ │ ├── all-company-roles.tsx
│ │ │ │ │ └── company-reviews.tsx
│ │ │ │ ├── reviews
│ │ │ │ │ ├── info-card.tsx
│ │ │ │ │ ├── review-search-bar.tsx
│ │ │ │ │ ├── new-role-card.tsx
│ │ │ │ │ ├── bar-graph.tsx
│ │ │ │ │ ├── collapsable-info.tsx
│ │ │ │ │ └── round-bar-graph.tsx
│ │ │ │ ├── header
│ │ │ │ │ ├── header-layout.tsx
│ │ │ │ │ ├── mobile-header-button.tsx
│ │ │ │ │ └── header-layout-client.tsx
│ │ │ │ ├── screen-size-indicator.tsx
│ │ │ │ ├── shared
│ │ │ │ │ └── favorite-button.tsx
│ │ │ │ ├── search
│ │ │ │ │ └── simple-search-bar.tsx
│ │ │ │ ├── no-results.tsx
│ │ │ │ ├── profile
│ │ │ │ │ ├── profile-button-client.tsx
│ │ │ │ │ ├── profile-button.tsx
│ │ │ │ │ ├── profile-tabs.tsx
│ │ │ │ │ └── favorite-company-search.tsx
│ │ │ │ └── footer.tsx
│ │ │ ├── (pages)
│ │ │ │ ├── (dashboard)
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── redirection
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── (roles)
│ │ │ │ │ │ └── role
│ │ │ │ │ │ └── page.tsx
│ │ │ │ └── (protected)
│ │ │ │ │ └── layout.tsx
│ │ │ ├── styles
│ │ │ │ └── font.ts
│ │ │ ├── not-found.tsx
│ │ │ ├── api
│ │ │ │ └── trpc
│ │ │ │ │ └── [trpc]
│ │ │ │ │ └── route.ts
│ │ │ ├── robots.txt
│ │ │ └── layout.tsx
│ │ ├── middleware.ts
│ │ ├── utils
│ │ │ ├── reviewCountByStars.ts
│ │ │ ├── dateHelpers.ts
│ │ │ └── reviewsAggregationHelpers.ts
│ │ ├── trpc
│ │ │ ├── query-client.ts
│ │ │ └── server.ts
│ │ └── env.ts
│ ├── eslint.config.js
│ ├── tsconfig.json
│ ├── next.config.js
│ ├── README.md
│ └── package.json
└── auth-proxy
│ ├── tsconfig.json
│ ├── .output
│ ├── server
│ │ ├── index.mjs.map
│ │ ├── index.mjs
│ │ ├── package.json
│ │ └── chunks
│ │ │ └── routes
│ │ │ └── r
│ │ │ ├── _...auth_.mjs.map
│ │ │ └── _...auth_.mjs
│ └── nitro.json
│ ├── .env.example
│ ├── .nitro
│ └── types
│ │ ├── nitro.d.ts
│ │ ├── nitro-config.d.ts
│ │ ├── nitro-routes.d.ts
│ │ └── tsconfig.json
│ ├── eslint.config.js
│ ├── turbo.json
│ ├── routes
│ └── r
│ │ └── [...auth].ts
│ ├── package.json
│ └── README.md
├── .npmrc
├── tooling
├── github
│ ├── package.json
│ └── setup
│ │ └── action.yml
├── typescript
│ ├── package.json
│ ├── internal-package.json
│ └── base.json
├── tailwind
│ ├── eslint.config.js
│ ├── native.ts
│ ├── tsconfig.json
│ ├── package.json
│ └── base.ts
├── eslint
│ ├── tsconfig.json
│ ├── nextjs.js
│ ├── react.js
│ ├── package.json
│ └── types.d.ts
└── prettier
│ ├── tsconfig.json
│ ├── package.json
│ └── index.js
├── packages
├── db
│ ├── drizzle
│ │ ├── 0007_simple_korg.sql
│ │ ├── 0001_petite_white_tiger.sql
│ │ ├── 0004_cooing_the_leader.sql
│ │ ├── 0006_nappy_star_brand.sql
│ │ ├── 0005_productive_hulk.sql
│ │ ├── meta
│ │ │ └── _journal.json
│ │ └── 0003_silky_mister_fear.sql
│ ├── src
│ │ ├── index.ts
│ │ ├── client.ts
│ │ ├── utils
│ │ │ └── enums.ts
│ │ └── schema
│ │ │ ├── sessions.ts
│ │ │ ├── users.ts
│ │ │ ├── locations.ts
│ │ │ ├── profilesToRoles.ts
│ │ │ ├── profliesToReviews.ts
│ │ │ ├── profilesToCompanies.ts
│ │ │ ├── companiesToLocations.ts
│ │ │ ├── accounts.ts
│ │ │ ├── roleRequest.ts
│ │ │ ├── companies.ts
│ │ │ ├── companyRequest.ts
│ │ │ ├── roles.ts
│ │ │ ├── misc.ts
│ │ │ └── profiles.ts
│ ├── eslint.config.js
│ ├── tsconfig.json
│ ├── drizzle.config.ts
│ └── package.json
├── ui
│ ├── src
│ │ ├── icons.tsx
│ │ ├── index.ts
│ │ ├── chip.tsx
│ │ ├── label.tsx
│ │ ├── toaster.tsx
│ │ ├── textarea.tsx
│ │ ├── input.tsx
│ │ ├── hooks
│ │ │ └── use-custom-toast.ts
│ │ ├── checkbox.tsx
│ │ ├── popover.tsx
│ │ ├── logo.tsx
│ │ ├── radio-group.tsx
│ │ ├── error-toast.tsx
│ │ └── success-toast.tsx
│ ├── eslint.config.js
│ ├── tsconfig.json
│ ├── tailwind.config.ts
│ ├── components.json
│ └── package.json
├── api
│ ├── eslint.config.js
│ ├── tests
│ │ ├── mocks
│ │ │ ├── role.ts
│ │ │ ├── review.ts
│ │ │ └── company.ts
│ │ └── role.test.ts
│ ├── tsconfig.json
│ ├── src
│ │ ├── utils
│ │ │ ├── fuzzyHelper.ts
│ │ │ └── slugHelpers.ts
│ │ ├── router
│ │ │ ├── index.ts
│ │ │ ├── auth.ts
│ │ │ ├── companytoLocation.ts
│ │ │ └── location.ts
│ │ ├── root.ts
│ │ └── index.ts
│ └── package.json
├── auth
│ ├── tsconfig.json
│ ├── eslint.config.js
│ ├── src
│ │ ├── index.ts
│ │ └── index.rsc.ts
│ ├── env.ts
│ └── package.json
├── validators
│ ├── eslint.config.js
│ ├── tsconfig.json
│ ├── src
│ │ └── index.ts
│ └── package.json
└── scraper
│ ├── tsconfig.json
│ ├── scraped
│ └── seed data
│ │ ├── add_id.py
│ │ └── companies_to_csv.py
│ └── package.json
├── vitest.workspace.ts
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── turbo
└── generators
│ └── templates
│ ├── eslint.config.js.hbs
│ ├── tsconfig.json.hbs
│ └── package.json.hbs
├── pnpm-workspace.yaml
├── compose.yml
├── .github
├── workflows
│ ├── preview-migration.yml
│ ├── migration.yml
│ ├── assign-reviewers.yml
│ └── ci.yml
└── pull_request_template.md
├── .gitignore
├── .env.example
├── package.json
└── turbo.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.12
--------------------------------------------------------------------------------
/apps/docs/static/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | node-linker=hoisted
2 |
--------------------------------------------------------------------------------
/tooling/github/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/github"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/db/drizzle/0007_simple_korg.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "role" ALTER COLUMN "jobType" SET NOT NULL;
--------------------------------------------------------------------------------
/apps/web/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/apps/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/favicon.ico
--------------------------------------------------------------------------------
/packages/db/drizzle/0001_petite_white_tiger.sql:
--------------------------------------------------------------------------------
1 | -- ALTER TABLE "role" ADD COLUMN "createdBy" varchar NOT NULL;
--------------------------------------------------------------------------------
/packages/db/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "drizzle-orm/sql";
2 | export { alias } from "drizzle-orm/pg-core";
3 |
--------------------------------------------------------------------------------
/packages/ui/src/icons.tsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon } from "@radix-ui/react-icons";
2 |
3 | export { CheckIcon };
4 |
--------------------------------------------------------------------------------
/apps/auth-proxy/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/base.json",
3 | "include": ["routes"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/packages/db/drizzle/0004_cooing_the_leader.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "role"
2 | DROP CONSTRAINT IF EXISTS "role_slug_unique";
--------------------------------------------------------------------------------
/apps/docs/static/img/docusaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/docs/static/img/docusaurus.png
--------------------------------------------------------------------------------
/packages/db/drizzle/0006_nappy_star_brand.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "review" RENAME COLUMN "freeTransport" TO "travelBenefits";
2 |
--------------------------------------------------------------------------------
/apps/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
3 | };
4 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.output/server/index.mjs.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.mjs","sources":[],"sourcesContent":null,"names":[],"mappings":";;;;;"}
--------------------------------------------------------------------------------
/apps/web/test/app.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 |
3 | test("should pass", () => {
4 | expect(true).toBe(true);
5 | });
6 |
--------------------------------------------------------------------------------
/vitest.workspace.ts:
--------------------------------------------------------------------------------
1 | import { defineWorkspace } from "vitest/config";
2 |
3 | export default defineWorkspace(["packages/*", "apps/*", "tooling/*"]);
4 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.env.example:
--------------------------------------------------------------------------------
1 | AUTH_SECRET=""
2 | AUTH_GOOGLE_ID=""
3 | AUTH_GOOGLE_SECRET=""
4 | AUTH_REDIRECT_PROXY_URL=""
5 |
6 | NITRO_PRESET="vercel_edge"
--------------------------------------------------------------------------------
/tooling/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/tsconfig",
3 | "private": true,
4 | "version": "0.1.0",
5 | "files": [
6 | "*.json"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Black.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Bold.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Light.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Thin.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Italic.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Medium.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-Regular.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-SemiBold.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-BoldItalic.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraBold.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraLight.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ThinItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ThinItalic.ttf
--------------------------------------------------------------------------------
/apps/auth-proxy/.nitro/types/nitro.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-BlackItalic.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-LightItalic.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-MediumItalic.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraLightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandboxnu/cooper/main/apps/web/public/fonts/hanken-grotesk/HankenGrotesk-ExtraLightItalic.ttf
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "esbenp.prettier-vscode",
5 | "yoavbls.pretty-ts-errors",
6 | "bradlc.vscode-tailwindcss"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/turbo/generators/templates/eslint.config.js.hbs:
--------------------------------------------------------------------------------
1 | import baseConfig from "@cooper/eslint-config/base"; /** @type
2 | {import('typescript-eslint').Config} */ export default [ { ignores: [], },
3 | ...baseConfig, ];
--------------------------------------------------------------------------------
/apps/docs/docs/04-web/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Web",
3 | "position": 2,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Documentation for the Cooper web app."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@docusaurus/tsconfig",
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/db/src/client.ts:
--------------------------------------------------------------------------------
1 | import { sql } from "@vercel/postgres";
2 | import { drizzle } from "drizzle-orm/vercel-postgres";
3 |
4 | import * as schema from "./schema";
5 |
6 | export const db = drizzle(sql, { schema });
7 |
--------------------------------------------------------------------------------
/apps/docs/docs/03-backend/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Backend",
3 | "position": 2,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Documentation for the backend of the Cooper app."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tooling/tailwind/eslint.config.js:
--------------------------------------------------------------------------------
1 | // FIXME: This kinda stinks...
2 | ///
3 |
4 | import baseConfig from "@cooper/eslint-config/base";
5 |
6 | export default [...baseConfig];
7 |
--------------------------------------------------------------------------------
/tooling/tailwind/native.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | import base from "./base";
4 |
5 | export default {
6 | content: base.content,
7 | presets: [base],
8 | theme: {},
9 | } satisfies Config;
10 |
--------------------------------------------------------------------------------
/turbo/generators/templates/tsconfig.json.hbs:
--------------------------------------------------------------------------------
1 | { "extends": "@cooper/tsconfig/base.json", "compilerOptions": {
2 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, "include": ["*.ts",
3 | "src"], "exclude": ["node_modules"] }
--------------------------------------------------------------------------------
/tooling/eslint/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/api/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@cooper/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: ["dist/**"],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/api/tests/mocks/role.ts:
--------------------------------------------------------------------------------
1 | export const data = [
2 | { title: "Software Engineer", description: "you write code" },
3 | { title: "Journalist", description: "you write words" },
4 | { title: "Artist", description: "you draw" },
5 | ];
6 |
--------------------------------------------------------------------------------
/packages/api/tests/role.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 |
3 | describe("Company Router", () => {
4 | test("github action bandaage", () => {
5 | expect(true).toBe(true);
6 | });
7 | // Add your tests here
8 | });
9 |
--------------------------------------------------------------------------------
/tooling/prettier/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/tooling/tailwind/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/auth/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["src", "*.ts"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/validators/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@cooper/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: ["dist/**"],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/api/tests/mocks/review.ts:
--------------------------------------------------------------------------------
1 | export const data = [
2 | { id: "1", workTerm: "SPRING", workEnvironment: "REMOTE" },
3 | { id: "2", workTerm: "FALL", workEnvironment: "INPERSON" },
4 | { id: "3", workTerm: "SUMMER", workEnvironment: "HYBRID" },
5 | ];
6 |
--------------------------------------------------------------------------------
/apps/auth-proxy/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@cooper/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [".nitro/**", ".output/**"],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/apps/web/public/svg/checkmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/packages/scraper/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "CommonJS",
5 | "lib": ["esnext", "dom"],
6 | "moduleResolution": "node",
7 | "strict": true,
8 | "esModuleInterop": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/web/public/svg/toastX.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/packages/auth/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig, { restrictEnvAccess } from "@cooper/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ...restrictEnvAccess,
10 | ];
11 |
--------------------------------------------------------------------------------
/packages/db/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig, { restrictEnvAccess } from "@cooper/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: ["dist/**"],
7 | },
8 | ...baseConfig,
9 | ...restrictEnvAccess,
10 | ];
11 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.nitro/types/nitro-config.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by nitro
2 |
3 | // App Config
4 | import type { Defu } from "defu";
5 |
6 | type UserAppConfig = Defu<{}, []>;
7 |
8 | declare module "nitropack" {
9 | interface AppConfig extends UserAppConfig {}
10 | }
11 |
12 | export {};
13 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/internal-package.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
6 | },
7 | "include": ["src", "tests"],
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/form/form-card.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * FormCard component provides a structured container to organize form elements.
3 | */
4 | export function FormCard({ children }: { children: React.ReactNode }) {
5 | return
{children}
;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/validators/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/internal-package.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
6 | },
7 | "include": ["*.ts", "src"],
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/ui/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@cooper/eslint-config/base";
2 | import reactConfig from "@cooper/eslint-config/react";
3 |
4 | /** @type {import('typescript-eslint').Config} */
5 | export default [
6 | {
7 | ignores: [],
8 | },
9 | ...baseConfig,
10 | ...reactConfig,
11 | ];
12 |
--------------------------------------------------------------------------------
/packages/db/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/internal-package.json",
3 | "compilerOptions": {
4 | "strictNullChecks": true,
5 | "outDir": "dist",
6 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
7 | },
8 | "include": ["src"],
9 | "exclude": ["node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/validators/src/index.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const unused = z.string().describe(
4 | `This lib is currently not used as we use drizzle-zod for simple schemas
5 | But as the application grows and we need other validators to share
6 | with back and frontend, we can put them in here
7 | `,
8 | );
9 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.output/nitro.json:
--------------------------------------------------------------------------------
1 | {
2 | "date": "2025-11-14T23:53:58.107Z",
3 | "preset": "node-server",
4 | "framework": {
5 | "name": "nitro",
6 | "version": "2.9.7"
7 | },
8 | "versions": {
9 | "nitro": "2.9.7"
10 | },
11 | "commands": {
12 | "preview": "node ./server/index.mjs"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/auth-proxy/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "extends": ["//"],
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": [".nitro/**", ".output/**", ".vercel/**"]
8 | },
9 | "dev": {
10 | "persistent": true
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/auth/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { signIn, signOut } from "@cooper/auth";
4 |
5 | export async function handleGoogleSignIn() {
6 | await signIn("google", { redirectTo: "/" });
7 | }
8 |
9 | export async function handleSignOut() {
10 | await signOut({ redirectTo: "/" });
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/public/svg/perkCheck.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Next.js",
6 | "type": "node-terminal",
7 | "request": "launch",
8 | "command": "pnpm dev",
9 | "cwd": "${workspaceFolder}/apps/web/",
10 | "skipFiles": ["/**"]
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/form/form-section.tsx:
--------------------------------------------------------------------------------
1 | import { FormCard } from "~/app/_components/form/form-card";
2 |
3 | /**
4 | * FormSection component creates a section within a FormCard with a specified title.
5 | */
6 | export function FormSection({ children }: { children: React.ReactNode }) {
7 | return {children} ;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/internal-package.json",
3 | "compilerOptions": {
4 | "lib": ["dom", "dom.iterable", "ES2022"],
5 | "jsx": "preserve",
6 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
7 | },
8 | "include": ["*.ts", "src"],
9 | "exclude": ["node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/web/public/svg/toastCheck.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.output/server/index.mjs:
--------------------------------------------------------------------------------
1 | import process from "node:process";
2 | globalThis._importMeta_ = { url: import.meta.url, env: process.env };
3 | import "node:http";
4 | import "node:https";
5 | export { n as default } from "./chunks/runtime.mjs";
6 | import "node:fs";
7 | import "node:path";
8 | import "node:url";
9 | //# sourceMappingURL=index.mjs.map
10 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - apps/*
3 | - packages/*
4 | - tooling/*
5 |
6 | catalog:
7 | eslint: ^9.6.0
8 | prettier: ^3.3.2
9 | typescript: ^5.5.3
10 | zod: ^3.23.8
11 | vitest: ^2.1.1
12 |
13 | catalogs:
14 | react18:
15 | react: 18.3.1
16 | react-dom: 18.3.1
17 | "@types/react": ^18.3.3
18 | "@types/react-dom": ^18.3.0
19 |
--------------------------------------------------------------------------------
/tooling/typescript/internal-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | /** Emit types for internal packages to speed up editor performance. */
6 | "declaration": true,
7 | "declarationMap": true,
8 | "noEmit": false,
9 | "emitDeclarationOnly": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/public/svg/perkCross.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/themed/onboarding/input.tsx:
--------------------------------------------------------------------------------
1 | import { Input as InputPrimitive } from "@cooper/ui/input";
2 |
3 | export function Input(props: React.ComponentProps) {
4 | return (
5 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/packages/db/src/utils/enums.ts:
--------------------------------------------------------------------------------
1 | // TODO: Does not work as expected -- sets the varchar field length to the number of variants (not the intended behavior)
2 | export function enumToPgEnum(
3 | enumeration: Record,
4 | ): [string, ...string[]] {
5 | return Object.values(enumeration).map((value: string) => `${value}`) as [
6 | string,
7 | ...string[],
8 | ];
9 | }
10 |
--------------------------------------------------------------------------------
/packages/api/tests/mocks/company.ts:
--------------------------------------------------------------------------------
1 | export const data = [
2 | {
3 | name: "Draft Kings",
4 | description: "sports gambling company",
5 | industry: "Sports",
6 | },
7 | { name: "Klaviyo", description: "fire company", industry: "Tech" },
8 | {
9 | name: "Hubspot",
10 | description: "a company where a lot of our alum work",
11 | industry: "Business",
12 | },
13 | ];
14 |
--------------------------------------------------------------------------------
/packages/auth/src/index.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 |
3 | import { authConfig } from "./config";
4 |
5 | export type { Session } from "next-auth";
6 |
7 | const { handlers, auth, signIn, signOut } = NextAuth(authConfig);
8 |
9 | export { handlers, auth, signIn, signOut };
10 |
11 | export {
12 | invalidateSessionToken,
13 | validateToken,
14 | isSecureContext,
15 | } from "./config";
16 |
--------------------------------------------------------------------------------
/packages/db/drizzle/0005_productive_hulk.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "profile" ALTER COLUMN "updatedAt" SET DEFAULT now();--> statement-breakpoint
2 | ALTER TABLE "profile" ALTER COLUMN "updatedAt" SET NOT NULL;--> statement-breakpoint
3 | ALTER TABLE "review" ADD COLUMN IF NOT EXISTS "snackBar" boolean DEFAULT false NOT NULL;--> statement-breakpoint
4 | ALTER TABLE "role" ADD COLUMN IF NOT EXISTS "jobType" varchar DEFAULT 'CO-OP';
--------------------------------------------------------------------------------
/packages/ui/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is not used for any compilation purpose, it is only used
3 | * for Tailwind Intellisense & Autocompletion in the source files
4 | */
5 | import type { Config } from "tailwindcss";
6 |
7 | import baseConfig from "@cooper/tailwind-config/web";
8 |
9 | export default {
10 | content: ["./src/**/*.tsx"],
11 | presets: [baseConfig],
12 | } satisfies Config;
13 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/cooper-logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | interface CooperLogoProps {
4 | width?: number;
5 | }
6 |
7 | export default function CooperLogo({ width }: CooperLogoProps) {
8 | return (
9 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/ui/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "./tailwind.config.ts",
8 | "css": "unused.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "utils": "@cooper/ui",
14 | "components": "src/",
15 | "ui": "src/"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tooling/github/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: "Setup and install"
2 | description: "Common setup steps for Actions"
3 |
4 | runs:
5 | using: composite
6 | steps:
7 | - uses: pnpm/action-setup@v4
8 | - uses: actions/setup-node@v4
9 | with:
10 | node-version: 20
11 | cache: "pnpm"
12 |
13 | - shell: bash
14 | run: pnpm add -g turbo
15 |
16 | - shell: bash
17 | run: pnpm install
18 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/body-logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | interface CooperLogoProps {
4 | width?: number;
5 | }
6 |
7 | export default function BodyLogo({ width }: CooperLogoProps) {
8 | return (
9 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | # FIXME: This currently does not work with the Drizzle migrations
5 | postgres:
6 | image: postgres
7 | restart: always
8 | environment:
9 | - POSTGRES_USER=admin
10 | - POSTGRES_PASSWORD=admin
11 | - POSTGRES_DB=cooper
12 | volumes:
13 | - postgres:/var/lib/postgresql/data
14 | ports:
15 | - "5432:5432"
16 | volumes:
17 | postgres:
18 |
--------------------------------------------------------------------------------
/apps/web/public/svg/bookmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.output/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/auth-proxy-prod",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "private": true,
6 | "dependencies": {
7 | "@auth/core": "0.32.0",
8 | "@panva/hkdf": "1.2.1",
9 | "cookie": "0.6.0",
10 | "jose": "5.6.3",
11 | "oauth4webapi": "2.11.1",
12 | "preact": "10.11.3",
13 | "preact-render-to-string": "5.2.3",
14 | "std-env": "3.7.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig, { restrictEnvAccess } from "@cooper/eslint-config/base";
2 | import nextjsConfig from "@cooper/eslint-config/nextjs";
3 | import reactConfig from "@cooper/eslint-config/react";
4 |
5 | /** @type {import('typescript-eslint').Config} */
6 | export default [
7 | {
8 | ignores: [".next/**"],
9 | },
10 | ...baseConfig,
11 | ...reactConfig,
12 | ...nextjsConfig,
13 | ...restrictEnvAccess,
14 | ];
15 |
--------------------------------------------------------------------------------
/apps/web/src/middleware.ts:
--------------------------------------------------------------------------------
1 | export { auth as middleware } from "@cooper/auth";
2 |
3 | // Or like this if you need to do something here.
4 | // export default auth((req) => {
5 | // console.log(req.auth) // { session: { user: { ... } } }
6 | // })
7 |
8 | // Read more: https://web.org/docs/app/building-your-application/routing/middleware#matcher
9 | export const config = {
10 | matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
11 | };
12 |
--------------------------------------------------------------------------------
/packages/api/src/utils/fuzzyHelper.ts:
--------------------------------------------------------------------------------
1 | import Fuse from "fuse.js";
2 |
3 | export function performFuseSearch(
4 | elem: T[],
5 | options: string[],
6 | searchQuery: string | undefined,
7 | ): T[] {
8 | if (!searchQuery) {
9 | return elem;
10 | }
11 |
12 | const fuseOptions = {
13 | keys: options,
14 | };
15 |
16 | const fuse = new Fuse(elem, fuseOptions);
17 | return fuse.search(searchQuery).map((result) => result.item);
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/public/svg/filledBookmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/web/public/svg/hoverBookmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/packages/ui/src/index.ts:
--------------------------------------------------------------------------------
1 | import { cx } from "class-variance-authority";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | const cn = (...inputs: Parameters) => twMerge(cx(inputs));
5 |
6 | export { cn };
7 |
8 | export { CustomToaster } from "./custom-toaster";
9 | export { useCustomToast } from "./hooks/use-custom-toast";
10 | export { SuccessToast } from "./success-toast";
11 | export { ErrorToast } from "./error-toast";
12 | export { Pagination } from "./pagination";
13 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.nitro/types/nitro-routes.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by nitro
2 | import type { Serialize, Simplify } from "nitropack";
3 | declare module "nitropack" {
4 | type Awaited = T extends PromiseLike ? Awaited : T;
5 | interface InternalApi {
6 | "/r/**:auth": {
7 | default: Simplify<
8 | Serialize<
9 | Awaited>
10 | >
11 | >;
12 | };
13 | }
14 | }
15 | export {};
16 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cooper/tsconfig/base.json",
3 | "compilerOptions": {
4 | "lib": ["es2022", "dom", "dom.iterable"],
5 | "jsx": "preserve",
6 | "baseUrl": ".",
7 | "paths": {
8 | "~/*": ["./src/*"]
9 | },
10 | "plugins": [{ "name": "next" }],
11 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
12 | "module": "esnext"
13 | },
14 | "include": [".", ".next/types/**/*.ts"],
15 | "exclude": ["node_modules"]
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/public/svg/burgerMenu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/packages/db/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "drizzle-kit";
2 |
3 | if (!process.env.POSTGRES_URL) {
4 | throw new Error("Missing POSTGRES_URL");
5 | }
6 |
7 | const nonPoolingUrl = process.env.POSTGRES_URL.replace(":6543", ":5432");
8 |
9 | export default {
10 | schema: "./src/schema",
11 | out: "./drizzle",
12 | dialect: "postgresql",
13 | dbCredentials: {
14 | url: nonPoolingUrl,
15 | ssl: {
16 | rejectUnauthorized: false,
17 | },
18 | },
19 | } satisfies Config;
20 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/back-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 |
5 | import { Button } from "@cooper/ui/button";
6 |
7 | export default function BackButton() {
8 | const router = useRouter();
9 | return (
10 | router.back()}
15 | className="my-1 border-black text-cooper-blue-600"
16 | >
17 | Go Back
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/src/utils/reviewCountByStars.ts:
--------------------------------------------------------------------------------
1 | import type { ReviewType } from "@cooper/db/schema";
2 |
3 | export function calculateRatings(reviews: ReviewType[] = []) {
4 | const totalReviews = reviews.length;
5 |
6 | return [5, 4, 3, 2, 1].map((star) => {
7 | const count = reviews.filter(
8 | (r) => r.overallRating.toFixed(0) === star.toString(),
9 | ).length;
10 | const percentage =
11 | totalReviews > 0 ? Math.round((count / totalReviews) * 100) : 0;
12 | return { stars: star, percentage };
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/tooling/eslint/nextjs.js:
--------------------------------------------------------------------------------
1 | import nextPlugin from "@next/eslint-plugin-next";
2 |
3 | /** @type {Awaited} */
4 | export default [
5 | {
6 | files: ["**/*.ts", "**/*.tsx"],
7 | plugins: {
8 | "@next/next": nextPlugin,
9 | },
10 | rules: {
11 | ...nextPlugin.configs.recommended.rules,
12 | ...nextPlugin.configs["core-web-vitals"].rules,
13 | // TypeError: context.getAncestors is not a function
14 | "@next/next/no-duplicate-head": "off",
15 | },
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/apps/web/src/app/(pages)/(dashboard)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { CustomToaster } from "@cooper/ui";
2 |
3 | import HeaderLayout from "~/app/_components/header/header-layout";
4 | import OnboardingWrapper from "~/app/_components/onboarding/onboarding-wrapper";
5 |
6 | export default function RootLayout({
7 | children,
8 | }: {
9 | children: React.ReactNode;
10 | }) {
11 | return (
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/auth/login-button-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 |
5 | import { handleGoogleSignIn } from "./actions";
6 |
7 | export default function LoginButtonClient() {
8 | return (
9 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/preview-migration.yml:
--------------------------------------------------------------------------------
1 | name: Migration
2 |
3 | on:
4 | pull_request:
5 | branches: ["main"]
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
10 |
11 | jobs:
12 | migrate:
13 | runs-on: ubuntu-latest
14 | env:
15 | POSTGRES_URL: ${{ secrets.POSTGRES_URL_STAGING }}
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Setup
20 | uses: ./tooling/github/setup
21 |
22 | - name: Migrate
23 | run: pnpm db:migrate
24 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/onboarding/post-onboarding/coop-prompt.tsx:
--------------------------------------------------------------------------------
1 | import { WelcomeDialog } from "~/app/_components/onboarding/post-onboarding/welcome-dialog";
2 |
3 | interface CoopPromptProps {
4 | firstName: string;
5 | onClick: () => void;
6 | }
7 |
8 | export function CoopPrompt({ firstName, onClick }: CoopPromptProps) {
9 | return (
10 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.output/server/chunks/routes/r/_...auth_.mjs.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"_...auth_.mjs","sources":["../../../../../routes/r/[...auth].ts"],"sourcesContent":null,"names":[],"mappings":";;;;;;;;;AAIA,kBAAe,YAAA;AAAA,EAAa,OAAO,KAAA,KACjC,IAAK,CAAA,YAAA,CAAa,KAAK,CAAG,EAAA;AAAA,IACxB,QAAU,EAAA,IAAA;AAAA,IACV,MAAA,EAAQ,QAAQ,GAAI,CAAA,WAAA;AAAA,IACpB,SAAW,EAAA,CAAC,CAAC,OAAA,CAAQ,GAAI,CAAA,MAAA;AAAA,IACzB,gBAAA,EAAkB,QAAQ,GAAI,CAAA,uBAAA;AAAA,IAC9B,SAAW,EAAA;AAAA,MACT,MAAO,CAAA;AAAA,QACL,QAAA,EAAU,QAAQ,GAAI,CAAA,cAAA;AAAA,QACtB,YAAA,EAAc,QAAQ,GAAI,CAAA,kBAAA;AAAA,OAC3B,CAAA;AAAA,KACH;AAAA,GACD,CAAA;AACH,CAAA;;;;"}
--------------------------------------------------------------------------------
/apps/docs/docs/05-next-steps.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Next Steps
6 |
7 | Now that you have some familiarity with the Cooper codebase, try to make some changes to the codebase / add new features!
8 |
9 | One thing to always remember is that while this documentation is a great resource for you to familiarize yourself with the project, it _will_ become outdated as the project evolves rapidly.
10 |
11 | It is **your** responsiblity to circle back to the documentation and update it as you make changes to the codebase :unclesampointing:
12 |
13 | Good luck and happy coding! :tada:
14 |
--------------------------------------------------------------------------------
/turbo/generators/templates/package.json.hbs:
--------------------------------------------------------------------------------
1 | { "name": "@cooper/{{name}}", "private": true, "version": "0.1.0", "type":
2 | "module", "exports": { ".": "./src/index.ts" }, "license": "MIT", "scripts": {
3 | "clean": "rm -rf .turbo node_modules", "format": "prettier --check .
4 | --ignore-path ../../.gitignore", "lint": "eslint", "typecheck": "tsc --noEmit"
5 | }, "devDependencies": { "@cooper/eslint-config": "workspace:*",
6 | "@cooper/prettier-config": "workspace:*", "@cooper/tsconfig": "workspace:*",
7 | "eslint": "catalog:", "prettier": "catalog:", "typescript": "catalog:" },
8 | "prettier": "@cooper/prettier-config" }
--------------------------------------------------------------------------------
/apps/docs/docs/04-web/ui.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # UI
6 |
7 | We make extensive use of [shadcn/ui](https://ui.shadcn.com/) on Cooper to build components.
8 |
9 | All of the components are built using the `ui` package. We either directly use the components from the package or consume them to make new components, specific to the Cooper webapp.
10 |
11 | ## Adding a New Component
12 |
13 | To add a new component, you can make use of the `ui-add` command. This command will create the necessary files for a new component in the `ui` package.
14 |
15 | ```bash
16 | pnpm ui-add
17 | ```
18 |
--------------------------------------------------------------------------------
/apps/web/public/svg/compareAdd.svg:
--------------------------------------------------------------------------------
1 |
8 |
16 |
22 |
--------------------------------------------------------------------------------
/apps/web/src/app/(pages)/(dashboard)/redirection/page.tsx:
--------------------------------------------------------------------------------
1 | export default function ErrorPage() {
2 | return (
3 |
4 |
5 |
6 |
7 | Authentication Error
8 |
9 |
You must log in with husky.neu.edu
10 |
Click the sign in button to try again
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/tooling/eslint/react.js:
--------------------------------------------------------------------------------
1 | import reactPlugin from "eslint-plugin-react";
2 | import hooksPlugin from "eslint-plugin-react-hooks";
3 |
4 | /** @type {Awaited} */
5 | export default [
6 | {
7 | files: ["**/*.ts", "**/*.tsx"],
8 | plugins: {
9 | react: reactPlugin,
10 | "react-hooks": hooksPlugin,
11 | },
12 | rules: {
13 | ...reactPlugin.configs["jsx-runtime"].rules,
14 | ...hooksPlugin.configs.recommended.rules,
15 | },
16 | languageOptions: {
17 | globals: {
18 | React: "writable",
19 | },
20 | },
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/apps/auth-proxy/routes/r/[...auth].ts:
--------------------------------------------------------------------------------
1 | import { Auth } from "@auth/core";
2 | import Google from "@auth/core/providers/google";
3 | import { eventHandler, toWebRequest } from "h3";
4 |
5 | export default eventHandler(async (event) =>
6 | Auth(toWebRequest(event), {
7 | basePath: "/r",
8 | secret: process.env.AUTH_SECRET,
9 | trustHost: !!process.env.VERCEL,
10 | redirectProxyUrl: process.env.AUTH_REDIRECT_PROXY_URL,
11 | providers: [
12 | Google({
13 | clientId: process.env.AUTH_GOOGLE_ID,
14 | clientSecret: process.env.AUTH_GOOGLE_SECRET,
15 | }),
16 | ],
17 | }),
18 | );
19 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/form/sections/index.ts:
--------------------------------------------------------------------------------
1 | import { BasicInfoSection } from "~/app/_components/form/sections/basic-info-section";
2 | import { ReviewSection } from "~/app/_components/form/sections//review-section";
3 | import { CompanyDetailsSection } from "~/app/_components/form/sections/company-details-section";
4 | import { InterviewSection } from "~/app/_components/form/sections/interview-section";
5 | import { PaySection } from "~/app/_components/form/sections/pay-section";
6 |
7 | export {
8 | BasicInfoSection,
9 | ReviewSection,
10 | CompanyDetailsSection,
11 | InterviewSection,
12 | PaySection,
13 | };
14 |
--------------------------------------------------------------------------------
/packages/api/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { authRouter } from "./auth";
2 | import { companyRouter } from "./company";
3 | import { companyToLocationRouter } from "./companytoLocation";
4 | import { locationRouter } from "./location";
5 | import { profileRouter } from "./profile";
6 | import { reviewRouter } from "./review";
7 | import { roleRouter } from "./role";
8 | import { roleAndCompanyRouter } from "./roleAndCompany";
9 |
10 | export {
11 | authRouter,
12 | companyRouter,
13 | profileRouter,
14 | reviewRouter,
15 | roleRouter,
16 | locationRouter,
17 | companyToLocationRouter,
18 | roleAndCompanyRouter,
19 | };
20 |
--------------------------------------------------------------------------------
/apps/web/src/app/(pages)/(protected)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | import { auth } from "@cooper/auth";
4 | import { CustomToaster } from "@cooper/ui";
5 | import HeaderLayout from "~/app/_components/header/header-layout";
6 |
7 | export default async function ProtectedLayour({
8 | children,
9 | }: {
10 | children: React.ReactNode;
11 | }) {
12 | // Ensure user is authenticated
13 | const session = await auth();
14 |
15 | if (!session) {
16 | redirect("/");
17 | }
18 |
19 | return (
20 |
21 | {children}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/.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 | # docs
9 | .docusaurus
10 | .cache-loader
11 |
12 |
13 | # testing
14 | coverage
15 |
16 | # next.js
17 | .next/
18 | out/
19 | next-env.d.ts
20 |
21 | # production
22 | build
23 |
24 | # misc
25 | .DS_Store
26 | *.pem
27 |
28 | # debug
29 | npm-debug.log*
30 | yarn-debug.log*
31 | yarn-error.log*
32 | .pnpm-debug.log*
33 |
34 | # local env files
35 | .env
36 | .env*.local
37 |
38 | # vercel
39 | .vercel
40 |
41 | # typescript
42 | *.tsbuildinfo
43 | dist/
44 |
45 | # turbo
46 | .turbo
47 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/auth/logout-button.tsx:
--------------------------------------------------------------------------------
1 | import { signOut } from "@cooper/auth";
2 | import { Button } from "@cooper/ui/button";
3 |
4 | export default function LogoutButton() {
5 | return (
6 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/loading-results.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@cooper/ui";
2 |
3 | import BodyLogo from "./body-logo";
4 |
5 | export default function LoadingResults({ className }: { className?: string }) {
6 | return (
7 |
13 |
14 |
15 |
Loading ...
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/companies/company-about.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { CompanyType } from "@cooper/db/schema";
4 |
5 | interface CompanyAboutProps {
6 | className?: string;
7 | companyObj: CompanyType | undefined;
8 | }
9 |
10 | export function CompanyAbout({ companyObj }: CompanyAboutProps) {
11 | return (
12 |
13 |
14 | About {companyObj?.name}
15 |
16 |
17 |
{companyObj?.description}
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/onboarding/post-onboarding/browse-around-prompt.tsx:
--------------------------------------------------------------------------------
1 | import { WelcomeDialog } from "~/app/_components/onboarding/post-onboarding/welcome-dialog";
2 |
3 | interface BrowseAroundPromptProps {
4 | firstName: string;
5 | onClick: () => void;
6 | }
7 |
8 | export function BrowseAroundPrompt({
9 | firstName,
10 | onClick,
11 | }: BrowseAroundPromptProps) {
12 | return (
13 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/auth/src/index.rsc.ts:
--------------------------------------------------------------------------------
1 | import { cache } from "react";
2 | import NextAuth from "next-auth";
3 |
4 | import { authConfig } from "./config";
5 |
6 | export type { Session } from "next-auth";
7 |
8 | const { handlers, auth: defaultAuth, signIn, signOut } = NextAuth(authConfig);
9 |
10 | /**
11 | * This is the main way to get session data for your RSCs.
12 | * This will de-duplicate all calls to next-auth's default `auth()` function and only call it once per request
13 | */
14 | const auth = cache(defaultAuth);
15 |
16 | export { handlers, auth, signIn, signOut };
17 |
18 | export {
19 | invalidateSessionToken,
20 | validateToken,
21 | isSecureContext,
22 | } from "./config";
23 |
--------------------------------------------------------------------------------
/packages/api/src/root.ts:
--------------------------------------------------------------------------------
1 | import {
2 | authRouter,
3 | companyRouter,
4 | companyToLocationRouter,
5 | locationRouter,
6 | profileRouter,
7 | reviewRouter,
8 | roleAndCompanyRouter,
9 | roleRouter,
10 | } from "./router";
11 | import { createTRPCRouter } from "./trpc";
12 |
13 | export const appRouter = createTRPCRouter({
14 | auth: authRouter,
15 | company: companyRouter,
16 | role: roleRouter,
17 | profile: profileRouter,
18 | review: reviewRouter,
19 | location: locationRouter,
20 | companyToLocation: companyToLocationRouter,
21 | roleAndCompany: roleAndCompanyRouter,
22 | });
23 |
24 | // export type definition of API
25 | export type AppRouter = typeof appRouter;
26 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/reviews/info-card.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import React from "react";
3 |
4 | interface InfoCardProps {
5 | title: string;
6 | children: ReactNode;
7 | }
8 |
9 | const InfoCard: React.FC = ({ title, children }) => {
10 | return (
11 |
12 |
13 | {title}
14 |
15 |
{children}
16 |
17 | );
18 | };
19 |
20 | export default InfoCard;
21 |
--------------------------------------------------------------------------------
/packages/db/src/schema/sessions.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
3 |
4 | import { User } from "./users";
5 |
6 | export const Session = pgTable("session", {
7 | sessionToken: varchar("sessionToken", { length: 255 }).notNull().primaryKey(),
8 | userId: uuid("userId")
9 | .notNull()
10 | .references(() => User.id, { onDelete: "cascade" }),
11 | expires: timestamp("expires", {
12 | mode: "date",
13 | withTimezone: true,
14 | }).notNull(),
15 | });
16 |
17 | export const SessionRelations = relations(Session, ({ one }) => ({
18 | user: one(User, { fields: [Session.userId], references: [User.id] }),
19 | }));
20 |
--------------------------------------------------------------------------------
/tooling/prettier/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/prettier-config",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "exports": {
7 | ".": "./index.js"
8 | },
9 | "scripts": {
10 | "clean": "rm -rf .turbo node_modules",
11 | "format": "prettier --check . --ignore-path ../../.gitignore",
12 | "typecheck": "tsc --noEmit"
13 | },
14 | "dependencies": {
15 | "@ianvs/prettier-plugin-sort-imports": "^4.3.0",
16 | "prettier": "catalog:",
17 | "prettier-plugin-tailwindcss": "^0.6.5"
18 | },
19 | "devDependencies": {
20 | "@cooper/tsconfig": "workspace:*",
21 | "typescript": "catalog:"
22 | },
23 | "prettier": "@cooper/prettier-config"
24 | }
25 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.nitro/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "forceConsistentCasingInFileNames": true,
4 | "strict": false,
5 | "noEmit": true,
6 | "target": "ESNext",
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "allowJs": true,
10 | "resolveJsonModule": true,
11 | "jsx": "preserve",
12 | "allowSyntheticDefaultImports": true,
13 | "jsxFactory": "h",
14 | "jsxFragmentFactory": "Fragment",
15 | "paths": {
16 | "#imports": ["./nitro-imports"],
17 | "~/*": ["../../*"],
18 | "@/*": ["../../*"],
19 | "~~/*": ["../../*"],
20 | "@@/*": ["../../*"]
21 | }
22 | },
23 | "include": ["./nitro.d.ts", "../../**/*"]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/scraper/scraped/seed data/add_id.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import uuid
3 |
4 | # Input and output file paths
5 | input_file = 'location.csv'
6 | output_file = 'output_with_uuid.csv'
7 |
8 | # Open the input file for reading and output file for writing
9 | with open(input_file, mode='r', newline='', encoding='utf-8') as infile, \
10 | open(output_file, mode='w', newline='', encoding='utf-8') as outfile:
11 |
12 | reader = csv.reader(infile)
13 | writer = csv.writer(outfile)
14 |
15 | # Append UUID to each row
16 | for row in reader:
17 | row_with_uuid = [str(uuid.uuid4())] + row
18 | writer.writerow(row_with_uuid)
19 |
20 | print(f"UUIDs added to each row. Output written to {output_file}")
--------------------------------------------------------------------------------
/packages/api/src/router/auth.ts:
--------------------------------------------------------------------------------
1 | import type { TRPCRouterRecord } from "@trpc/server";
2 |
3 | import { invalidateSessionToken } from "@cooper/auth";
4 |
5 | import { protectedProcedure, publicProcedure } from "../trpc";
6 |
7 | export const authRouter = {
8 | getSession: publicProcedure.query(({ ctx }) => {
9 | return ctx.session;
10 | }),
11 | getSecretMessage: protectedProcedure.query(() => {
12 | return "you can see this secret message!";
13 | }),
14 | signOut: protectedProcedure.mutation(async (opts) => {
15 | if (!opts.ctx.token) {
16 | return { success: false };
17 | }
18 | await invalidateSessionToken(opts.ctx.token);
19 | return { success: true };
20 | }),
21 | } satisfies TRPCRouterRecord;
22 |
--------------------------------------------------------------------------------
/packages/auth/env.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-properties */
2 | import { createEnv } from "@t3-oss/env-nextjs";
3 | import { z } from "zod";
4 |
5 | export const env = createEnv({
6 | server: {
7 | AUTH_GOOGLE_ID: z.string().min(1),
8 | AUTH_GOOGLE_SECRET: z.string().min(1),
9 | AUTH_SECRET:
10 | process.env.NODE_ENV === "production"
11 | ? z.string().min(1)
12 | : z.string().min(1).optional(),
13 | NODE_ENV: z.enum(["development", "production"]).optional(),
14 | VERCEL_ENV: z.enum(["development", "preview", "production"]).optional(),
15 | },
16 | client: {},
17 | experimental__runtimeEnv: {},
18 | skipValidation:
19 | !!process.env.CI || process.env.npm_lifecycle_event === "lint",
20 | });
21 |
--------------------------------------------------------------------------------
/packages/scraper/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/scraper",
3 | "version": "0.1.0",
4 | "private": true,
5 | "main": "index.js",
6 | "keywords": [],
7 | "author": "",
8 | "license": "MIT",
9 | "scripts": {
10 | "company_data": "npx ts-node src/levels_company_data.ts",
11 | "company_name": "npx ts-node src/levels_company_names.ts"
12 | },
13 | "dependencies": {
14 | "puppeteer": "^24.3.1"
15 | },
16 | "devDependencies": {
17 | "@cooper/eslint-config": "workspace:*",
18 | "@cooper/prettier-config": "workspace:*",
19 | "@cooper/tsconfig": "workspace:*",
20 | "eslint": "catalog:",
21 | "prettier": "catalog:",
22 | "typescript": "catalog:"
23 | },
24 | "prettier": "@cooper/prettier-config"
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/themed/onboarding/form.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FormLabel as FormLabelPrimitive,
3 | FormMessage as FormMessagePrimitive,
4 | } from "@cooper/ui/form";
5 |
6 | export function FormLabel({
7 | children,
8 | required,
9 | ...props
10 | }: React.ComponentProps & { required?: boolean }) {
11 | return (
12 |
13 | {children}
14 | {required && * }
15 |
16 | );
17 | }
18 |
19 | export function FormMessage(
20 | props: React.ComponentProps,
21 | ) {
22 | return ;
23 | }
24 |
--------------------------------------------------------------------------------
/apps/web/public/svg/compareRole.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/apps/web/public/svg/reviewReport.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/onboarding/onboarding-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | import { auth } from "@cooper/auth";
4 |
5 | import { OnboardingDialog } from "~/app/_components/onboarding/dialog";
6 |
7 | interface OnboardingWrapperProps {
8 | children: ReactNode;
9 | }
10 |
11 | /**
12 | * OnboardingWrapper component that wraps the app and initiates the onboarding dialog.
13 | * @param children - The children components
14 | * @returns The OnboardingWrapper component
15 | */
16 | export default async function OnboardingWrapper({
17 | children,
18 | }: OnboardingWrapperProps) {
19 | const session = await auth();
20 |
21 | return (
22 | <>
23 | {children}
24 |
25 | >
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/apps/web/public/svg/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/web/src/app/styles/font.ts:
--------------------------------------------------------------------------------
1 | import localFont from "next/font/local";
2 |
3 | export const hankenGroteskFont = localFont({
4 | src: [
5 | {
6 | path: "./../../../public/fonts/hanken-grotesk/HankenGrotesk-Light.ttf",
7 | weight: "200",
8 | style: "normal",
9 | },
10 | {
11 | path: "./../../../public/fonts/hanken-grotesk/HankenGrotesk-Regular.ttf",
12 | weight: "400",
13 | style: "normal",
14 | },
15 | {
16 | path: "./../../../public/fonts/hanken-grotesk/HankenGrotesk-Medium.ttf",
17 | weight: "600",
18 | style: "normal",
19 | },
20 | {
21 | path: "./../../../public/fonts/hanken-grotesk/HankenGrotesk-Bold.ttf",
22 | weight: "800",
23 | style: "normal",
24 | },
25 | ],
26 | variable: "--font-sans",
27 | });
28 |
--------------------------------------------------------------------------------
/apps/web/src/trpc/query-client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultShouldDehydrateQuery,
3 | QueryClient,
4 | } from "@tanstack/react-query";
5 | import SuperJSON from "superjson";
6 |
7 | export const createQueryClient = () =>
8 | new QueryClient({
9 | defaultOptions: {
10 | queries: {
11 | // With SSR, we usually want to set some default staleTime
12 | // above 0 to avoid refetching immediately on the client
13 | staleTime: 30 * 1000,
14 | },
15 | dehydrate: {
16 | serializeData: SuperJSON.serialize,
17 | shouldDehydrateQuery: (query) =>
18 | defaultShouldDehydrateQuery(query) ||
19 | query.state.status === "pending",
20 | },
21 | hydrate: {
22 | deserializeData: SuperJSON.deserialize,
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/packages/db/src/schema/users.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
3 |
4 | import { Account } from "./accounts";
5 | import { Profile } from "./profiles";
6 |
7 | export const User = pgTable("user", {
8 | id: uuid("id").notNull().primaryKey().defaultRandom(),
9 | name: varchar("name", { length: 255 }),
10 | email: varchar("email", { length: 255 }).notNull(),
11 | emailVerified: timestamp("emailVerified", {
12 | mode: "date",
13 | withTimezone: true,
14 | }),
15 | image: varchar("image", { length: 255 }),
16 | });
17 |
18 | export const UserRelations = relations(User, ({ one, many }) => ({
19 | accounts: many(Account),
20 | profile: one(Profile, {
21 | fields: [User.id],
22 | references: [Profile.userId],
23 | }),
24 | }));
25 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/reviews/review-search-bar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "../themed/onboarding/input";
4 |
5 | interface ReviewSearchBarProps {
6 | searchTerm: string;
7 | onSearchChange: (value: string) => void;
8 | className?: string;
9 | }
10 |
11 | export default function ReviewSearchBar({
12 | searchTerm,
13 | onSearchChange,
14 | className,
15 | }: ReviewSearchBarProps) {
16 | return (
17 |
18 | {
21 | onSearchChange(e.target.value);
22 | }}
23 | className="!h-10 w-full !border-[0.75px] !border-cooper-gray-400 bg-cooper-gray-100 !text-sm focus:ring-1 focus:ring-cooper-gray-400 focus:ring-offset-0"
24 | placeholder="Search reviews..."
25 | />
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/apps/web/src/utils/dateHelpers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Format the date according to designs in Figma.
3 | * @param date Date object to format
4 | * @returns Date in format of 'dd mth yyyy'
5 | */
6 | export function formatDate(date?: Date) {
7 | if (!date) {
8 | return "";
9 | }
10 |
11 | const months = [
12 | "Jan",
13 | "Feb",
14 | "Mar",
15 | "Apr",
16 | "May",
17 | "Jun",
18 | "Jul",
19 | "Aug",
20 | "Sep",
21 | "Oct",
22 | "Nov",
23 | "Dec",
24 | ];
25 |
26 | // Extract the year, month, and day components
27 | const year = date.getFullYear();
28 | const monthIndex = date.getMonth();
29 | const month = months[monthIndex];
30 | const day = String(date.getDate()).padStart(2, "0"); // Ensure two digits for the day
31 |
32 | // Return the formatted date string
33 | return `${day} ${month} ${year}`;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/ui/src/chip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@cooper/ui";
4 | import { Button } from "@cooper/ui/button";
5 |
6 | interface ChipProps {
7 | label: string;
8 | onClick?: () => void;
9 | selected?: boolean;
10 | }
11 |
12 | const Chip = React.forwardRef(
13 | ({ label, onClick, selected = false }: ChipProps, ref) => {
14 | return (
15 |
25 | {label}
26 |
27 | );
28 | },
29 | );
30 |
31 | export { Chip };
32 |
--------------------------------------------------------------------------------
/tooling/typescript/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | /** Base Options */
5 | "esModuleInterop": true,
6 | "skipLibCheck": true,
7 | "target": "ES2022",
8 | "lib": ["ES2022"],
9 | "allowJs": true,
10 | "resolveJsonModule": true,
11 | "moduleDetection": "force",
12 | "isolatedModules": true,
13 |
14 | /** Keep TSC performant in monorepos */
15 | "incremental": true,
16 | "disableSourceOfProjectReferenceRedirect": true,
17 |
18 | /** Strictness */
19 | "strict": true,
20 | "noUncheckedIndexedAccess": true,
21 | "checkJs": true,
22 |
23 | /** Transpile using Bundler (not tsc) */
24 | "module": "Preserve",
25 | "moduleResolution": "Bundler",
26 | "noEmit": true
27 | },
28 | "exclude": ["node_modules", "build", "dist", ".next"]
29 | }
30 |
--------------------------------------------------------------------------------
/apps/auth-proxy/.output/server/chunks/routes/r/_...auth_.mjs:
--------------------------------------------------------------------------------
1 | import { Auth } from "@auth/core";
2 | import Google from "@auth/core/providers/google";
3 | import { e as eventHandler, t as toWebRequest } from "../../runtime.mjs";
4 | import "node:http";
5 | import "node:https";
6 | import "node:fs";
7 | import "node:path";
8 | import "node:url";
9 |
10 | const ____auth_ = eventHandler(async (event) =>
11 | Auth(toWebRequest(event), {
12 | basePath: "/r",
13 | secret: process.env.AUTH_SECRET,
14 | trustHost: !!process.env.VERCEL,
15 | redirectProxyUrl: process.env.AUTH_REDIRECT_PROXY_URL,
16 | providers: [
17 | Google({
18 | clientId: process.env.AUTH_GOOGLE_ID,
19 | clientSecret: process.env.AUTH_GOOGLE_SECRET,
20 | }),
21 | ],
22 | }),
23 | );
24 |
25 | export { ____auth_ as default };
26 | //# sourceMappingURL=_...auth_.mjs.map
27 |
--------------------------------------------------------------------------------
/.github/workflows/migration.yml:
--------------------------------------------------------------------------------
1 | name: Migration
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
10 |
11 | jobs:
12 | migrate:
13 | runs-on: ubuntu-latest
14 | env:
15 | POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Setup
20 | uses: ./tooling/github/setup
21 |
22 | - name: Migrate
23 | run: pnpm db:migrate
24 |
25 | migrate-staging:
26 | runs-on: ubuntu-latest
27 | env:
28 | POSTGRES_URL: ${{ secrets.POSTGRES_URL_STAGING }}
29 | steps:
30 | - uses: actions/checkout@v4
31 |
32 | - name: Setup
33 | uses: ./tooling/github/setup
34 |
35 | - name: Migrate Staging
36 | run: pnpm db:migrate
37 |
--------------------------------------------------------------------------------
/packages/ui/src/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { VariantProps } from "class-variance-authority";
4 | import * as React from "react";
5 | import * as LabelPrimitive from "@radix-ui/react-label";
6 | import { cva } from "class-variance-authority";
7 |
8 | import { cn } from "@cooper/ui";
9 |
10 | const labelVariants = cva(
11 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
12 | );
13 |
14 | const Label = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef &
17 | VariantProps
18 | >(({ className, ...props }, ref) => (
19 |
24 | ));
25 | Label.displayName = LabelPrimitive.Root.displayName;
26 |
27 | export { Label };
28 |
--------------------------------------------------------------------------------
/apps/auth-proxy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/auth-proxy",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "nitro build",
7 | "clean": "git clean -xdf .cache .nitro .output .turbo .vercel node_modules",
8 | "lint": "eslint",
9 | "format": "prettier --check . --ignore-path ../../.gitignore",
10 | "typecheck": "tsc --noEmit"
11 | },
12 | "dependencies": {
13 | "@auth/core": "0.32.0"
14 | },
15 | "devDependencies": {
16 | "@cooper/eslint-config": "workspace:*",
17 | "@cooper/prettier-config": "workspace:*",
18 | "@cooper/tailwind-config": "workspace:*",
19 | "@cooper/tsconfig": "workspace:*",
20 | "@types/node": "^20.14.15",
21 | "eslint": "catalog:",
22 | "h3": "^1.12.0",
23 | "nitropack": "^2.9.7",
24 | "prettier": "catalog:",
25 | "typescript": "catalog:"
26 | },
27 | "prettier": "@cooper/prettier-config"
28 | }
29 |
--------------------------------------------------------------------------------
/apps/docs/sidebars.ts:
--------------------------------------------------------------------------------
1 | import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
2 |
3 | /**
4 | * Creating a sidebar enables you to:
5 | - create an ordered group of docs
6 | - render a sidebar for each doc of that group
7 | - provide next/previous navigation
8 |
9 | The sidebars can be generated from the filesystem, or explicitly defined here.
10 |
11 | Create as many sidebars as you want.
12 | */
13 | const sidebars: SidebarsConfig = {
14 | // By default, Docusaurus generates a sidebar from the docs folder structure
15 | tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
16 |
17 | // But you can create a sidebar manually
18 | /*
19 | tutorialSidebar: [
20 | 'intro',
21 | 'hello',
22 | {
23 | type: 'category',
24 | label: 'Tutorial',
25 | items: ['tutorial-basics/create-a-document'],
26 | },
27 | ],
28 | */
29 | };
30 |
31 | export default sidebars;
32 |
--------------------------------------------------------------------------------
/apps/web/public/svg/apartment.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tooling/tailwind/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/tailwind-config",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "exports": {
7 | "./native": "./native.ts",
8 | "./web": "./web.ts"
9 | },
10 | "license": "MIT",
11 | "scripts": {
12 | "clean": "rm -rf .turbo node_modules",
13 | "format": "prettier --check . --ignore-path ../../.gitignore",
14 | "lint": "eslint",
15 | "typecheck": "tsc --noEmit"
16 | },
17 | "dependencies": {
18 | "postcss": "^8.4.39",
19 | "tailwindcss": "^3.4.4",
20 | "tailwindcss-animate": "^1.0.7"
21 | },
22 | "devDependencies": {
23 | "@cooper/eslint-config": "workspace:*",
24 | "@cooper/prettier-config": "workspace:*",
25 | "@cooper/tsconfig": "workspace:*",
26 | "eslint": "catalog:",
27 | "prettier": "catalog:",
28 | "typescript": "catalog:"
29 | },
30 | "prettier": "@cooper/prettier-config"
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/onboarding/post-onboarding/welcome-dialog.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import { Button } from "@cooper/ui/button";
4 |
5 | interface WelcomeDialogProps {
6 | heading: string;
7 | subheading: string;
8 | buttonText: string;
9 | onClick: () => void;
10 | }
11 |
12 | export function WelcomeDialog({
13 | heading,
14 | subheading,
15 | buttonText,
16 | onClick,
17 | }: WelcomeDialogProps) {
18 | return (
19 |
20 |
21 |
22 | {heading}
23 |
24 |
25 | {subheading}
26 |
27 |
{buttonText}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/src/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useToast } from "./hooks/use-toast";
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "./toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/packages/validators/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/validators",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "exports": {
7 | ".": {
8 | "types": "./dist/index.d.ts",
9 | "default": "./src/index.ts"
10 | }
11 | },
12 | "license": "MIT",
13 | "scripts": {
14 | "build": "tsc",
15 | "dev": "tsc --watch",
16 | "clean": "rm -rf .turbo dist node_modules",
17 | "format": "prettier --check . --ignore-path ../../.gitignore",
18 | "lint": "eslint",
19 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
20 | },
21 | "dependencies": {
22 | "zod": "catalog:"
23 | },
24 | "devDependencies": {
25 | "@cooper/eslint-config": "workspace:*",
26 | "@cooper/prettier-config": "workspace:*",
27 | "@cooper/tsconfig": "workspace:*",
28 | "eslint": "catalog:",
29 | "prettier": "catalog:",
30 | "typescript": "catalog:"
31 | },
32 | "prettier": "@cooper/prettier-config"
33 | }
34 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "editor.formatOnSave": true,
4 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
5 | "eslint.useFlatConfig": true,
6 | "eslint.workingDirectories": [
7 | { "pattern": "apps/*/" },
8 | { "pattern": "packages/*/" },
9 | { "pattern": "tooling/*/" }
10 | ],
11 | "prettier.ignorePath": ".gitignore",
12 | "tailwindCSS.experimental.classRegex": [
13 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
14 | ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
15 | ],
16 | "tailwindCSS.experimental.configFile": "./tooling/tailwind/web.ts",
17 | "typescript.enablePromptUseWorkspaceTsdk": true,
18 | "typescript.preferences.autoImportFileExcludePatterns": [
19 | "next/router.d.ts",
20 | "next/dist/client/router.d.ts"
21 | ],
22 | "typescript.tsdk": "node_modules/typescript/lib",
23 | "files.associations": {
24 | "*.json": "jsonc"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
2 | # Keep this file up-to-date when you add new variables to \`.env\`.
3 |
4 | # This file will be committed to version control, so make sure not to have any secrets in it.
5 | # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
6 |
7 | # The database URL is used to connect to your Supabase database.
8 | POSTGRES_URL="postgresql://admin:admin@localhost:5432/cooper"
9 |
10 | # You can generate the secret via 'openssl rand -base64 32' on Unix
11 | # @see https://next-auth.js.org/configuration/options#secret
12 | AUTH_SECRET='supersecret'
13 |
14 | # Preconfigured Google OAuth provider, works out-of-the-box
15 | # @see https://next-auth.js.org/providers/google
16 | AUTH_GOOGLE_ID=''
17 | AUTH_GOOGLE_SECRET=''
18 |
19 | # In case you're using the Auth Proxy (apps/auth-proxy)
20 | # AUTH_REDIRECT_PROXY_URL='https://auth.your-server.com/r'
--------------------------------------------------------------------------------
/packages/db/src/schema/locations.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, uuid, varchar } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { CompaniesToLocations } from "./companiesToLocations";
7 | import { Review } from "./reviews";
8 |
9 | export const Location = pgTable("location", {
10 | id: uuid("id").notNull().primaryKey().defaultRandom(),
11 | city: varchar("city").notNull(),
12 | state: varchar("state"),
13 | country: varchar("country").notNull(),
14 | });
15 |
16 | export type LocationType = typeof Location.$inferSelect;
17 |
18 | export const LocationRelations = relations(Location, ({ many }) => ({
19 | companies_to_locations: many(CompaniesToLocations),
20 | reviews: many(Review),
21 | }));
22 |
23 | export const CreateLocationSchema = createInsertSchema(Location, {
24 | city: z.string(),
25 | state: z.string(),
26 | country: z.string(),
27 | }).omit({
28 | id: true,
29 | });
30 |
--------------------------------------------------------------------------------
/apps/web/public/svg/magnifyingGlass.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/web/public/svg/work.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/packages/ui/src/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@cooper/ui";
4 |
5 | export type TextareaProps =
6 | React.TextareaHTMLAttributes & {
7 | variant?: "default" | "dialogue";
8 | };
9 |
10 | const Textarea = React.forwardRef(
11 | ({ className, variant, ...props }, ref) => {
12 | const style =
13 | variant === "dialogue"
14 | ? "flex h-fit w-[100%] rounded-lg outline outline-[1px] outline-[#474747] px-3 py-2"
15 | : "flex min-h-[200px] w-full rounded-md border-[3px] border-cooper-blue-600 bg-white px-3 py-2 text-xl font-normal ring-offset-background placeholder:text-cooper-gray-600 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50";
16 |
17 | return ;
18 | },
19 | );
20 | Textarea.displayName = "Textarea";
21 |
22 | export { Textarea };
23 |
--------------------------------------------------------------------------------
/apps/web/src/trpc/server.ts:
--------------------------------------------------------------------------------
1 | import { cache } from "react";
2 | import { headers } from "next/headers";
3 | import { createHydrationHelpers } from "@trpc/react-query/rsc";
4 |
5 | import type { AppRouter } from "@cooper/api";
6 | import { createCaller, createTRPCContext } from "@cooper/api";
7 | import { auth } from "@cooper/auth";
8 |
9 | import { createQueryClient } from "./query-client";
10 |
11 | /**
12 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
13 | * handling a tRPC call from a React Server Component.
14 | */
15 | const createContext = cache(async () => {
16 | const heads = new Headers(headers());
17 | heads.set("x-trpc-source", "rsc");
18 |
19 | return createTRPCContext({
20 | session: await auth(),
21 | headers: heads,
22 | });
23 | });
24 |
25 | const getQueryClient = cache(createQueryClient);
26 | const caller = createCaller(createContext);
27 |
28 | export const { trpc: api, HydrateClient } = createHydrationHelpers(
29 | caller,
30 | getQueryClient,
31 | );
32 |
--------------------------------------------------------------------------------
/tooling/eslint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/eslint-config",
3 | "private": true,
4 | "version": "0.3.0",
5 | "type": "module",
6 | "exports": {
7 | "./base": "./base.js",
8 | "./nextjs": "./nextjs.js",
9 | "./react": "./react.js"
10 | },
11 | "scripts": {
12 | "clean": "rm -rf .turbo node_modules",
13 | "format": "prettier --check . --ignore-path ../../.gitignore",
14 | "typecheck": "tsc --noEmit"
15 | },
16 | "dependencies": {
17 | "@next/eslint-plugin-next": "^14.2.4",
18 | "eslint-plugin-import": "^2.29.1",
19 | "eslint-plugin-jsx-a11y": "^6.9.0",
20 | "eslint-plugin-react": "^7.34.3",
21 | "eslint-plugin-react-hooks": "5.2.0",
22 | "eslint-plugin-turbo": "^2.0.6",
23 | "typescript-eslint": "8.0.0"
24 | },
25 | "devDependencies": {
26 | "@cooper/prettier-config": "workspace:*",
27 | "@cooper/tsconfig": "workspace:*",
28 | "eslint": "catalog:",
29 | "prettier": "catalog:",
30 | "typescript": "catalog:"
31 | },
32 | "prettier": "@cooper/prettier-config"
33 | }
34 |
--------------------------------------------------------------------------------
/packages/api/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
2 |
3 | import type { AppRouter } from "./root";
4 | import { appRouter } from "./root";
5 | import { createCallerFactory, createTRPCContext } from "./trpc";
6 |
7 | /**
8 | * Create a server-side caller for the tRPC API
9 | * @example
10 | * const trpc = createCaller(createContext);
11 | * const res = await trpc.post.all();
12 | * ^? Post[]
13 | */
14 | const createCaller = createCallerFactory(appRouter);
15 |
16 | /**
17 | * Inference helpers for input types
18 | * @example
19 | * type PostByIdInput = RouterInputs['post']['byId']
20 | * ^? { id: number }
21 | **/
22 | type RouterInputs = inferRouterInputs;
23 |
24 | /**
25 | * Inference helpers for output types
26 | * @example
27 | * type AllPostsOutput = RouterOutputs['post']['all']
28 | * ^? Post[]
29 | **/
30 | type RouterOutputs = inferRouterOutputs;
31 |
32 | export { createTRPCContext, appRouter, createCaller };
33 | export type { AppRouter, RouterInputs, RouterOutputs };
34 |
--------------------------------------------------------------------------------
/packages/ui/src/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@cooper/ui";
4 |
5 | export type InputProps = React.InputHTMLAttributes & {
6 | variant?: "default" | "dialogue";
7 | };
8 |
9 | const Input = React.forwardRef(
10 | ({ className, type, variant, ...props }, ref) => {
11 | const style =
12 | variant === "dialogue"
13 | ? "flex h-fit w-[100%] rounded-lg outline outline-[1px] outline-[#474747] px-3 py-2"
14 | : "flex h-16 w-full rounded-md border-[3px] border-cooper-blue-600 bg-white px-3 py-2 text-xl font-normal file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:border-cooper-gray-300 placeholder:text-cooper-gray-600 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50";
15 |
16 | return (
17 |
23 | );
24 | },
25 | );
26 | Input.displayName = "Input";
27 |
28 | export { Input };
29 |
--------------------------------------------------------------------------------
/packages/api/src/router/companytoLocation.ts:
--------------------------------------------------------------------------------
1 | import type { TRPCRouterRecord } from "@trpc/server";
2 | import { z } from "zod";
3 |
4 | import { eq } from "@cooper/db";
5 | import {
6 | CompaniesToLocations,
7 | CreateCompanyToLocationSchema,
8 | Location,
9 | } from "@cooper/db/schema";
10 |
11 | import { protectedProcedure, publicProcedure } from "../trpc";
12 |
13 | export const companyToLocationRouter = {
14 | create: protectedProcedure
15 | .input(CreateCompanyToLocationSchema)
16 | .mutation(({ ctx, input }) => {
17 | return ctx.db.insert(CompaniesToLocations).values(input);
18 | }),
19 |
20 | getLocationsByCompanyId: publicProcedure
21 | .input(z.object({ companyId: z.string() }))
22 | .query(async ({ ctx, input }) => {
23 | const locations = await ctx.db
24 | .select()
25 | .from(CompaniesToLocations)
26 | .leftJoin(Location, eq(CompaniesToLocations.locationId, Location.id))
27 | .where(eq(CompaniesToLocations.companyId, input.companyId));
28 | return locations;
29 | }),
30 | } satisfies TRPCRouterRecord;
31 |
--------------------------------------------------------------------------------
/packages/api/src/utils/slugHelpers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a URL-friendly slug from text
3 | * Removes special characters, converts to lowercase, replaces spaces with hyphens
4 | */
5 | export const createSlug = (text: string): string => {
6 | return text
7 | .toLowerCase()
8 | .replace(/[^a-z0-9\s-]/g, "") // Remove all non-alphanumeric characters except spaces and hyphens
9 | .replace(/\s+/g, "-") // Replace spaces with hyphens
10 | .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
11 | .trim();
12 | };
13 |
14 | /**
15 | * Generate a unique slug by appending a number if needed
16 | * @param baseSlug The base slug to make unique
17 | * @param existingSlugs Array of existing slugs to check against
18 | * @returns A unique slug
19 | */
20 | export const generateUniqueSlug = (
21 | baseSlug: string,
22 | existingSlugs: string[],
23 | ): string => {
24 | let slug = baseSlug;
25 | let counter = 2;
26 |
27 | while (existingSlugs.includes(slug)) {
28 | slug = `${baseSlug}-${counter}`;
29 | counter++;
30 | }
31 |
32 | return slug;
33 | };
34 |
--------------------------------------------------------------------------------
/apps/web/public/svg/xSymbol.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
12 |
--------------------------------------------------------------------------------
/packages/ui/src/hooks/use-custom-toast.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useToast as useBaseToast } from "./use-toast";
4 |
5 | export function useCustomToast() {
6 | const { toast: baseToast, ...rest } = useBaseToast();
7 |
8 | const toast = {
9 | success: (description: string) =>
10 | baseToast({
11 | description,
12 | // Use a custom property that our custom toaster will recognize
13 | className: "toast-success",
14 | variant: "default",
15 | }),
16 |
17 | error: (description: string) =>
18 | baseToast({
19 | description,
20 | className: "toast-error",
21 | variant: "destructive",
22 | }),
23 |
24 | warning: (description: string) =>
25 | baseToast({
26 | description,
27 | className: "toast-warning",
28 | variant: "default",
29 | }),
30 |
31 | info: (description: string) =>
32 | baseToast({
33 | description,
34 | className: "toast-info",
35 | variant: "default",
36 | }),
37 |
38 | custom: baseToast,
39 | };
40 |
41 | return {
42 | ...rest,
43 | toast,
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/apps/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #2e8555;
10 | --ifm-color-primary-dark: #29784c;
11 | --ifm-color-primary-darker: #277148;
12 | --ifm-color-primary-darkest: #205d3b;
13 | --ifm-color-primary-light: #33925d;
14 | --ifm-color-primary-lighter: #359962;
15 | --ifm-color-primary-lightest: #3cad6e;
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 | }
19 |
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme="dark"] {
22 | --ifm-color-primary: #25c2a0;
23 | --ifm-color-primary-dark: #21af90;
24 | --ifm-color-primary-darker: #1fa588;
25 | --ifm-color-primary-darkest: #1a8870;
26 | --ifm-color-primary-light: #29d5b0;
27 | --ifm-color-primary-lighter: #32d8b4;
28 | --ifm-color-primary-lightest: #4fddbf;
29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
30 | }
31 |
--------------------------------------------------------------------------------
/packages/auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/auth",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "exports": {
7 | ".": {
8 | "react-server": "./src/index.rsc.ts",
9 | "default": "./src/index.ts"
10 | },
11 | "./env": "./env.ts"
12 | },
13 | "license": "MIT",
14 | "scripts": {
15 | "clean": "rm -rf .turbo node_modules",
16 | "format": "prettier --check . --ignore-path ../../.gitignore",
17 | "lint": "eslint",
18 | "typecheck": "tsc --noEmit"
19 | },
20 | "dependencies": {
21 | "@auth/core": "0.32.0",
22 | "@auth/drizzle-adapter": "^1.4.1",
23 | "@cooper/db": "workspace:*",
24 | "@t3-oss/env-nextjs": "^0.10.1",
25 | "next": "^14.2.4",
26 | "next-auth": "5.0.0-beta.19",
27 | "react": "catalog:react18",
28 | "react-dom": "catalog:react18",
29 | "zod": "catalog:"
30 | },
31 | "devDependencies": {
32 | "@cooper/eslint-config": "workspace:*",
33 | "@cooper/prettier-config": "workspace:*",
34 | "@cooper/tsconfig": "workspace:*",
35 | "eslint": "catalog:",
36 | "prettier": "catalog:",
37 | "typescript": "catalog:"
38 | },
39 | "prettier": "@cooper/prettier-config"
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/src/app/(pages)/(dashboard)/(roles)/role/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSearchParams } from "next/navigation";
4 |
5 | import type { RoleType } from "@cooper/db/schema";
6 |
7 | import LoadingResults from "~/app/_components/loading-results";
8 | import NoResults from "~/app/_components/no-results";
9 | import { RoleInfo } from "~/app/_components/reviews/role-info";
10 | import { api } from "~/trpc/react";
11 |
12 | export default function Role() {
13 | const searchParams = useSearchParams();
14 | const roleId = searchParams.get("id");
15 |
16 | const role = api.role.getById.useQuery({ id: roleId ?? "" });
17 |
18 | return (
19 | <>
20 | {role.isSuccess && (
21 |
22 |
26 |
27 | )}
28 | {role.isPending && (
29 |
30 |
31 |
32 | )}
33 | {!role.isPending && !role.isSuccess && (
34 |
35 |
36 |
37 | )}
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/db/src/schema/profilesToRoles.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, primaryKey, uuid } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { Profile } from "./profiles";
7 | import { Role } from "./roles";
8 |
9 | export const ProfilesToRoles = pgTable(
10 | "profiles_to_roles",
11 | {
12 | profileId: uuid("profileId")
13 | .notNull()
14 | .references(() => Profile.id),
15 | roleId: uuid("roleId")
16 | .notNull()
17 | .references(() => Role.id),
18 | },
19 | (t) => ({
20 | primaryKey: primaryKey({ columns: [t.profileId, t.roleId] }),
21 | }),
22 | );
23 |
24 | export const ProfilesToRolesRelations = relations(
25 | ProfilesToRoles,
26 | ({ one }) => ({
27 | profile: one(Profile, {
28 | fields: [ProfilesToRoles.profileId],
29 | references: [Profile.id],
30 | }),
31 | role: one(Role, {
32 | fields: [ProfilesToRoles.roleId],
33 | references: [Role.id],
34 | }),
35 | }),
36 | );
37 |
38 | export const CreateProfileToRoleSchema = createInsertSchema(ProfilesToRoles, {
39 | profileId: z.string(),
40 | roleId: z.string(),
41 | });
42 |
--------------------------------------------------------------------------------
/apps/web/public/svg/verified.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/header/header-layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | import { auth } from "@cooper/auth";
4 |
5 | import Header from "~/app/_components/header/header";
6 | import LoginButton from "../auth/login-button";
7 | import ProfileButton from "../profile/profile-button";
8 |
9 | /**
10 | * This should be used when placing content under the header, standardizes how children are placed under a header.
11 | * @param param0 Children to pass into the layout
12 | * @returns A layout component that standardizes the distance from the header
13 | */
14 | export default async function HeaderLayout({
15 | children,
16 | }: {
17 | children: ReactNode;
18 | }) {
19 | const session = await auth();
20 | const button = session ? (
21 |
22 | ) : (
23 |
24 | );
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 | {children}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 | import createJiti from "jiti";
3 |
4 | // Import env files to validate at build time. Use jiti so we can load .ts files in here.
5 | createJiti(fileURLToPath(import.meta.url))("./src/env");
6 |
7 | /** @type {import("next").NextConfig} */
8 | const config = {
9 | reactStrictMode: true,
10 |
11 | /** Enables hot reloading for local packages without a build step */
12 | transpilePackages: [
13 | "@cooper/api",
14 | "@cooper/auth",
15 | "@cooper/db",
16 | "@cooper/ui",
17 | "@cooper/validators",
18 | ],
19 |
20 | /** We already do linting and typechecking as separate tasks in CI */
21 | eslint: { ignoreDuringBuilds: true },
22 | typescript: { ignoreBuildErrors: true },
23 |
24 | /** Remote server host for company logos */
25 | images: {
26 | remotePatterns: [
27 | {
28 | protocol: "https",
29 | hostname: "img.logo.dev",
30 | port: "",
31 | pathname: "/**",
32 | },
33 | {
34 | protocol: "https",
35 | hostname: "lh3.googleusercontent.com",
36 | port: "",
37 | pathname: "/**",
38 | },
39 | ],
40 | },
41 | };
42 |
43 | export default config;
44 |
--------------------------------------------------------------------------------
/apps/web/public/svg/defaultProfile.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/api",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "exports": {
7 | ".": {
8 | "types": "./dist/index.d.ts",
9 | "default": "./src/index.ts"
10 | }
11 | },
12 | "license": "MIT",
13 | "scripts": {
14 | "build": "tsc",
15 | "dev": "tsc --watch",
16 | "test": "vitest",
17 | "clean": "rm -rf .turbo dist node_modules",
18 | "format": "prettier --check . --ignore-path ../../.gitignore",
19 | "lint": "eslint",
20 | "typecheck": "tsc --noEmit --emitDeclarationOnly false"
21 | },
22 | "dependencies": {
23 | "@cooper/auth": "workspace:*",
24 | "@cooper/db": "workspace:*",
25 | "@cooper/validators": "workspace:*",
26 | "@trpc/server": "11.0.0-rc.441",
27 | "bad-words": "^4.0.0",
28 | "fuse.js": "^7.0.0",
29 | "superjson": "2.2.1",
30 | "zod": "catalog:"
31 | },
32 | "devDependencies": {
33 | "@cooper/eslint-config": "workspace:*",
34 | "@cooper/prettier-config": "workspace:*",
35 | "@cooper/tsconfig": "workspace:*",
36 | "eslint": "catalog:",
37 | "prettier": "catalog:",
38 | "typescript": "catalog:"
39 | },
40 | "prettier": "@cooper/prettier-config"
41 | }
42 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Home
3 | ---
4 |
5 | # Cooper
6 |
7 | ## Overview
8 |
9 | Cooper is a **tool for Northeastern students to both submit reviews of their co-ops and filter through reviews of co-ops** left by other students.
10 |
11 | It is intended to be similar to other job review platforms, such as [Glassdoor](https://www.glassdoor.com/) and the no-longer-maintained [Co-op’d app](https://www.reddit.com/r/NEU/comments/xo3yr1/helpful_coop_reviews_app/). You can think of it as similar to Trace or RateMyProfessor but for Northeastern co-ops.
12 |
13 | ## Why does Cooper matter?
14 |
15 | **Choosing a co-op is an important career move** - it helps you build experience, jump-starts your career, and can lead to job offers upon graduation. Despite this, **Northeastern students have little information to base their co-op decision on**. Companies often provide only surface-level information about co-ops and other review tools like Glassdoor are not specific to Northeastern co-ops.
16 |
17 | In addition, students who have completed a co-op may want to pass on their advice, and creating a co-op review platform is a great way to aggregate that information into one place.
18 |
19 | To get started, read the [documentation](/docs/setup)! 🤓
20 |
--------------------------------------------------------------------------------
/packages/ui/src/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import Image from "next/image";
5 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
6 |
7 | import { cn } from "@cooper/ui";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/packages/api/src/router/location.ts:
--------------------------------------------------------------------------------
1 | import type { TRPCRouterRecord } from "@trpc/server";
2 | import { z } from "zod";
3 |
4 | import { asc, eq } from "@cooper/db";
5 | import { CreateLocationSchema, Location } from "@cooper/db/schema";
6 |
7 | import { protectedProcedure, publicProcedure } from "../trpc";
8 |
9 | export const locationRouter = {
10 | list: publicProcedure.query(({ ctx }) => {
11 | return ctx.db.query.Location.findMany({
12 | orderBy: asc(Location.city),
13 | });
14 | }),
15 |
16 | getById: publicProcedure
17 | .input(z.object({ id: z.string() }))
18 | .query(({ ctx, input }) => {
19 | return ctx.db.query.Location.findFirst({
20 | where: eq(Location.id, input.id),
21 | });
22 | }),
23 |
24 | getByPrefix: publicProcedure
25 | .input(z.object({ prefix: z.string() }))
26 | .query(({ ctx, input }) => {
27 | return ctx.db.query.Location.findMany({
28 | where: (loc, { ilike }) => ilike(loc.city, `${input.prefix}%`),
29 | orderBy: asc(Location.city),
30 | });
31 | }),
32 |
33 | create: protectedProcedure
34 | .input(CreateLocationSchema)
35 | .mutation(({ ctx, input }) => {
36 | return ctx.db.insert(Location).values(input);
37 | }),
38 | } satisfies TRPCRouterRecord;
39 |
--------------------------------------------------------------------------------
/packages/db/src/schema/profliesToReviews.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, primaryKey, uuid } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { Profile } from "./profiles";
7 | import { Review } from "./reviews";
8 |
9 | export const ProfilesToReviews = pgTable(
10 | "profiles_to_reviews",
11 | {
12 | profileId: uuid("profileId")
13 | .notNull()
14 | .references(() => Profile.id),
15 | reviewId: uuid("reviewId")
16 | .notNull()
17 | .references(() => Review.id),
18 | },
19 | (t) => ({
20 | primaryKey: primaryKey({ columns: [t.profileId, t.reviewId] }),
21 | }),
22 | );
23 |
24 | export const ProfilesToReviewsRelations = relations(
25 | ProfilesToReviews,
26 | ({ one }) => ({
27 | profile: one(Profile, {
28 | fields: [ProfilesToReviews.profileId],
29 | references: [Profile.id],
30 | }),
31 | review: one(Review, {
32 | fields: [ProfilesToReviews.reviewId],
33 | references: [Review.id],
34 | }),
35 | }),
36 | );
37 |
38 | export const CreateProfileToReviewSchema = createInsertSchema(
39 | ProfilesToReviews,
40 | {
41 | profileId: z.string(),
42 | reviewId: z.string(),
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/screen-size-indicator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from "react";
4 |
5 | export function ScreenSizeIndicator() {
6 | const [screenSize, setScreenSize] = useState("");
7 |
8 | useEffect(() => {
9 | const updateScreenSize = () => {
10 | if (window.matchMedia("(min-width: 1536px)").matches) {
11 | setScreenSize("2xl");
12 | } else if (window.matchMedia("(min-width: 1280px)").matches) {
13 | setScreenSize("xl");
14 | } else if (window.matchMedia("(min-width: 1024px)").matches) {
15 | setScreenSize("lg");
16 | } else if (window.matchMedia("(min-width: 768px)").matches) {
17 | setScreenSize("md");
18 | } else if (window.matchMedia("(min-width: 640px)").matches) {
19 | setScreenSize("sm");
20 | } else {
21 | setScreenSize("xs");
22 | }
23 | };
24 |
25 | updateScreenSize();
26 | window.addEventListener("resize", updateScreenSize);
27 |
28 | return () => {
29 | window.removeEventListener("resize", updateScreenSize);
30 | };
31 | }, []);
32 |
33 | return (
34 |
35 | Screen: {screenSize}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/shared/favorite-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Image from "next/image";
5 |
6 | import { useFavoriteToggle } from "./useFavoriteToggle";
7 |
8 | interface FavoriteButtonProps {
9 | objId: string;
10 | objType: "role" | "company";
11 | }
12 |
13 | interface FavoriteButtonProps {
14 | objId: string;
15 | objType: "role" | "company";
16 | }
17 |
18 | export function FavoriteButton({ objId, objType }: FavoriteButtonProps) {
19 | const { isFavorited, toggle, isLoading, profileId } = useFavoriteToggle(
20 | objId,
21 | objType,
22 | );
23 |
24 | const [hover, setHover] = useState(false);
25 |
26 | if (!profileId) return null;
27 |
28 | const src =
29 | hover && !isFavorited
30 | ? "/svg/hoverBookmark.svg"
31 | : isFavorited
32 | ? "/svg/filledBookmark.svg"
33 | : "/svg/bookmark.svg";
34 |
35 | return (
36 | setHover(true)}
45 | onMouseLeave={() => setHover(false)}
46 | />
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/packages/db/src/schema/profilesToCompanies.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, primaryKey, uuid } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { Company } from "./companies";
7 | import { Profile } from "./profiles";
8 |
9 | export const ProfilesToCompanies = pgTable(
10 | "profiles_to_companies",
11 | {
12 | profileId: uuid("profileId")
13 | .notNull()
14 | .references(() => Profile.id),
15 | companyId: uuid("companyId")
16 | .notNull()
17 | .references(() => Company.id),
18 | },
19 | (t) => ({
20 | primaryKey: primaryKey({ columns: [t.profileId, t.companyId] }),
21 | }),
22 | );
23 |
24 | export const ProfilesToCompaniesRelations = relations(
25 | ProfilesToCompanies,
26 | ({ one }) => ({
27 | profile: one(Profile, {
28 | fields: [ProfilesToCompanies.profileId],
29 | references: [Profile.id],
30 | }),
31 | company: one(Company, {
32 | fields: [ProfilesToCompanies.companyId],
33 | references: [Company.id],
34 | }),
35 | }),
36 | );
37 |
38 | export const CreateProfileToCompanySchema = createInsertSchema(
39 | ProfilesToCompanies,
40 | {
41 | profileId: z.string(),
42 | companyId: z.string(),
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/apps/web/src/env.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-properties */
2 | import { createEnv } from "@t3-oss/env-nextjs";
3 | import { vercel } from "@t3-oss/env-nextjs/presets";
4 | import { z } from "zod";
5 |
6 | import { env as authEnv } from "@cooper/auth/env";
7 |
8 | export const env = createEnv({
9 | extends: [authEnv, vercel()],
10 | shared: {
11 | NODE_ENV: z
12 | .enum(["development", "production", "test"])
13 | .default("development"),
14 | },
15 | /**
16 | * Specify your server-side environment variables schema here.
17 | * This way you can ensure the app isn't built with invalid env vars.
18 | */
19 | server: {
20 | POSTGRES_URL: z.string().url(),
21 | },
22 |
23 | /**
24 | * Specify your client-side environment variables schema here.
25 | * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
26 | */
27 | client: {
28 | // NEXT_PUBLIC_CLIENTVAR: z.string(),
29 | },
30 | /**
31 | * Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
32 | */
33 | experimental__runtimeEnv: {
34 | NODE_ENV: process.env.NODE_ENV,
35 |
36 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
37 | },
38 | skipValidation:
39 | !!process.env.CI || process.env.npm_lifecycle_event === "lint",
40 | });
41 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/search/simple-search-bar.tsx:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "react-hook-form";
2 |
3 | import { FormControl, FormField, FormItem } from "@cooper/ui/form";
4 | import { Input } from "@cooper/ui/input";
5 |
6 | /**
7 | * This Search Bar employs filtering and fuzzy searching.
8 | *
9 | * NOTE: Cycle and Term only make sense for Roles
10 | *
11 | * @param param0 Cycle and Term to be set as default values for their respective dropdowns
12 | * @returns A search bar which is connected to a parent 'useForm'
13 | */
14 | export function SimpleSearchBar() {
15 | const form = useFormContext();
16 |
17 | return (
18 |
19 | (
23 |
24 |
25 |
30 |
31 |
32 | )}
33 | />
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/no-results.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname, useRouter } from "next/navigation";
4 |
5 | import { cn } from "@cooper/ui";
6 | import { Button } from "@cooper/ui/button";
7 |
8 | import CooperLogo from "./cooper-logo";
9 |
10 | export default function NoResults({
11 | className,
12 | clearFunction,
13 | }: {
14 | className?: string;
15 | clearFunction?: boolean;
16 | }) {
17 | const router = useRouter();
18 | const pathname = usePathname();
19 |
20 | function clearFilters() {
21 | router.push(pathname);
22 | }
23 |
24 | return (
25 |
31 |
32 |
33 |
No Results Found
34 | {clearFunction && (
35 |
41 | Clear Filters
42 |
43 | )}
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/web/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import HeaderLayout from "~/app/_components/header/header-layout";
4 | import BackButton from "./_components/back-button";
5 |
6 | export default function NotFound() {
7 | return (
8 |
9 |
10 |
11 | 404
12 |
13 |
14 |
15 | 4
16 |
17 |
23 |
24 | 4
25 |
26 |
27 |
28 | Page Not Found
29 |
30 |
31 | We can’t seem to find the page you are looking for
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/db/src/schema/companiesToLocations.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, primaryKey, uuid } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { Company } from "./companies";
7 | import { Location } from "./locations";
8 |
9 | export const CompaniesToLocations = pgTable(
10 | "companies_to_locations",
11 | {
12 | companyId: uuid("companyId")
13 | .notNull()
14 | .references(() => Company.id),
15 | locationId: uuid("locationId")
16 | .notNull()
17 | .references(() => Location.id),
18 | },
19 | (t) => ({
20 | primaryKey: primaryKey({ columns: [t.companyId, t.locationId] }),
21 | }),
22 | );
23 |
24 | export const CompaniesToLocationsRelations = relations(
25 | CompaniesToLocations,
26 | ({ one }) => ({
27 | company: one(Company, {
28 | fields: [CompaniesToLocations.companyId],
29 | references: [Company.id],
30 | }),
31 | location: one(Location, {
32 | fields: [CompaniesToLocations.locationId],
33 | references: [Location.id],
34 | }),
35 | }),
36 | );
37 |
38 | export const CreateCompanyToLocationSchema = createInsertSchema(
39 | CompaniesToLocations,
40 | {
41 | companyId: z.string(),
42 | locationId: z.string(),
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/packages/db/src/schema/accounts.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import {
3 | integer,
4 | pgTable,
5 | primaryKey,
6 | text,
7 | uuid,
8 | varchar,
9 | } from "drizzle-orm/pg-core";
10 |
11 | import { User } from "./users";
12 |
13 | export const Account = pgTable(
14 | "account",
15 | {
16 | userId: uuid("userId")
17 | .notNull()
18 | .references(() => User.id, { onDelete: "cascade" }),
19 | type: varchar("type", { length: 255 })
20 | .$type<"email" | "oauth" | "oidc" | "webauthn">()
21 | .notNull(),
22 | provider: varchar("provider", { length: 255 }).notNull(),
23 | providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(),
24 | refresh_token: varchar("refresh_token", { length: 255 }),
25 | access_token: text("access_token"),
26 | expires_at: integer("expires_at"),
27 | token_type: varchar("token_type", { length: 255 }),
28 | scope: varchar("scope", { length: 255 }),
29 | id_token: text("id_token"),
30 | session_state: varchar("session_state", { length: 255 }),
31 | },
32 | (account) => ({
33 | compoundKey: primaryKey({
34 | columns: [account.provider, account.providerAccountId],
35 | }),
36 | }),
37 | );
38 |
39 | export const AccountRelations = relations(Account, ({ one }) => ({
40 | user: one(User, { fields: [Account.userId], references: [User.id] }),
41 | }));
42 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/reviews/new-role-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import { Card, CardContent, CardHeader, CardTitle } from "@cooper/ui/card";
6 |
7 | import { api } from "~/trpc/react";
8 | import NewRoleDialog from "./new-role-dialogue";
9 |
10 | interface NewRoleCardProps {
11 | companyId: string;
12 | }
13 |
14 | export default function NewRoleCard({ companyId }: NewRoleCardProps) {
15 | const [authorized, setAuthorized] = useState(false);
16 | const { data: session } = api.auth.getSession.useQuery();
17 |
18 | useEffect(() => {
19 | setAuthorized(!!session);
20 | }, [setAuthorized, session]);
21 |
22 | return (
23 |
28 |
29 |
30 | {authorized ? "Don't see your role?" : "Sign in to create a new role"}
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/src/app/api/trpc/[trpc]/route.ts:
--------------------------------------------------------------------------------
1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
2 |
3 | import { appRouter, createTRPCContext } from "@cooper/api";
4 | import { auth } from "@cooper/auth";
5 |
6 | export const runtime = "edge";
7 |
8 | /**
9 | * Configure basic CORS headers
10 | * You should extend this to match your needs
11 | */
12 | const setCorsHeaders = (res: Response) => {
13 | res.headers.set("Access-Control-Allow-Origin", "*");
14 | res.headers.set("Access-Control-Request-Method", "*");
15 | res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST");
16 | res.headers.set("Access-Control-Allow-Headers", "*");
17 | };
18 |
19 | export const OPTIONS = () => {
20 | const response = new Response(null, {
21 | status: 204,
22 | });
23 | setCorsHeaders(response);
24 | return response;
25 | };
26 |
27 | const handler = auth(async (req) => {
28 | const response = await fetchRequestHandler({
29 | endpoint: "/api/trpc",
30 | router: appRouter,
31 | req,
32 | createContext: () =>
33 | createTRPCContext({
34 | session: req.auth,
35 | headers: req.headers,
36 | }),
37 | onError({ error, path }) {
38 | console.error(`>>> tRPC Error on '${path}'`, error);
39 | },
40 | });
41 |
42 | setCorsHeaders(response);
43 | return response;
44 | });
45 |
46 | export { handler as GET, handler as POST };
47 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/auth/login-button.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { signIn } from "@cooper/auth";
3 | import { Button } from "@cooper/ui/button";
4 |
5 | export default function LoginButton() {
6 | return (
7 | <>
8 | {/* Image for small screens */}
9 |
27 |
28 | {/* Button for larger screens */}
29 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/apps/web/src/app/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: AI2Bot
2 | User-agent: Ai2Bot-Dolma
3 | User-agent: Amazonbot
4 | User-agent: anthropic-ai
5 | User-agent: Applebot
6 | User-agent: Applebot-Extended
7 | User-agent: Brightbot 1.0
8 | User-agent: Bytespider
9 | User-agent: CCBot
10 | User-agent: ChatGPT-User
11 | User-agent: Claude-Web
12 | User-agent: ClaudeBot
13 | User-agent: cohere-ai
14 | User-agent: cohere-training-data-crawler
15 | User-agent: Crawlspace
16 | User-agent: Diffbot
17 | User-agent: DuckAssistBot
18 | User-agent: FacebookBot
19 | User-agent: FriendlyCrawler
20 | User-agent: Google-Extended
21 | User-agent: GoogleOther
22 | User-agent: GoogleOther-Image
23 | User-agent: GoogleOther-Video
24 | User-agent: GPTBot
25 | User-agent: iaskspider/2.0
26 | User-agent: ICC-Crawler
27 | User-agent: ImagesiftBot
28 | User-agent: img2dataset
29 | User-agent: ISSCyberRiskCrawler
30 | User-agent: Kangaroo Bot
31 | User-agent: Meta-ExternalAgent
32 | User-agent: Meta-ExternalFetcher
33 | User-agent: OAI-SearchBot
34 | User-agent: omgili
35 | User-agent: omgilibot
36 | User-agent: PanguBot
37 | User-agent: PerplexityBot
38 | User-agent: Perplexity‑User
39 | User-agent: PetalBot
40 | User-agent: Scrapy
41 | User-agent: SemrushBot-OCOB
42 | User-agent: SemrushBot-SWA
43 | User-agent: Sidetrade indexer bot
44 | User-agent: Timpibot
45 | User-agent: VelenPublicWebCrawler
46 | User-agent: Webzio-Extended
47 | User-agent: YouBot
48 | Disallow: /
49 |
--------------------------------------------------------------------------------
/.github/workflows/assign-reviewers.yml:
--------------------------------------------------------------------------------
1 | name: Assign Reviewers
2 | on:
3 | pull_request:
4 | types: [opened, ready_for_review]
5 |
6 | jobs:
7 | assign-reviewers:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Assign reviewers
11 | uses: actions/github-script@v7
12 | with:
13 | github-token: ${{ secrets.GITHUB_TOKEN }}
14 | script: |
15 | const groupA = ["tracyyh", "gpalmer27"];
16 | const groupB = ["suxls", "mattrwang", "songmichael11"];
17 | const prAuthor = context.payload.pull_request.user.login;
18 |
19 | function pickRandom(arr) {
20 | return arr[Math.floor(Math.random() * arr.length)];
21 | }
22 |
23 | // Filter out PR author from each group
24 | const filteredA = groupA.filter(user => user !== prAuthor);
25 | const filteredB = groupB.filter(user => user !== prAuthor);
26 |
27 | // Pick reviewers only from filtered lists
28 | const reviewerA = pickRandom(filteredA);
29 | const reviewerB = pickRandom(filteredB);
30 |
31 | await github.rest.pulls.requestReviewers({
32 | owner: context.repo.owner,
33 | repo: context.repo.repo,
34 | pull_number: context.payload.pull_request.number,
35 | reviewers: [reviewerA, reviewerB].filter(Boolean)
36 | });
37 |
--------------------------------------------------------------------------------
/apps/web/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | import { cn } from "@cooper/ui";
4 |
5 | import { hankenGroteskFont } from "~/app/styles/font";
6 | import { TRPCReactProvider } from "~/trpc/react";
7 | import { CompareProvider } from "~/app/_components/compare/compare-context";
8 |
9 | import "~/app/styles/globals.css";
10 |
11 | import { env } from "~/env";
12 |
13 | export const metadata: Metadata = {
14 | metadataBase: new URL(
15 | env.VERCEL_ENV === "production"
16 | ? "https://cooper-sandboxneu.vercel.app"
17 | : "http://localhost:3000",
18 | ),
19 | title: "Cooper",
20 | description: "A Co-op Review Platform",
21 | openGraph: {
22 | title: "Cooper",
23 | description: "A Co-op Review Platform",
24 | url: "https://cooper-sandboxneu.vercel.app",
25 | siteName: "Cooper",
26 | },
27 | };
28 |
29 | export default function RootLayout(props: { children: React.ReactNode }) {
30 | return (
31 |
36 |
41 |
42 | {props.children}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/web/src/utils/reviewsAggregationHelpers.ts:
--------------------------------------------------------------------------------
1 | import type { ReviewType, WorkEnvironmentType } from "@cooper/db/schema";
2 |
3 | export function mostCommonWorkEnviornment(
4 | reviews: ReviewType[],
5 | ): "In Person" | "Hybrid" | "Remote" {
6 | const counts = {
7 | INPERSON: 0,
8 | HYBRID: 0,
9 | REMOTE: 0,
10 | };
11 |
12 | reviews.forEach((review) => {
13 | counts[review.workEnvironment as WorkEnvironmentType]++;
14 | });
15 |
16 | if (counts.INPERSON >= Math.max(counts.HYBRID, counts.REMOTE))
17 | return "In Person";
18 | if (counts.HYBRID >= Math.max(counts.INPERSON, counts.REMOTE))
19 | return "Hybrid";
20 | return "Remote";
21 | }
22 |
23 | export function averageStarRating(reviews: ReviewType[]): number {
24 | const totalStars = reviews.reduce((accum, curr) => {
25 | return accum + curr.overallRating;
26 | }, 0);
27 | return totalStars / reviews.length;
28 | }
29 |
30 | export function listBenefits(reviewObj: ReviewType): string[] {
31 | const benefits: string[] = [];
32 | if (reviewObj.pto) benefits.push("Paid Time Off");
33 | if (reviewObj.federalHolidays) benefits.push("Federal Holidays Off");
34 | if (reviewObj.freeLunch) benefits.push("Free Lunch");
35 | if (reviewObj.travelBenefits) benefits.push("Free Transporation");
36 | if (reviewObj.freeMerch) benefits.push("Free Merch");
37 | if (reviewObj.snackBar) benefits.push("Snack Bar");
38 | return benefits;
39 | }
40 |
--------------------------------------------------------------------------------
/tooling/prettier/index.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 |
3 | /** @typedef {import("prettier").Config} PrettierConfig */
4 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */
5 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
6 |
7 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */
8 | const config = {
9 | plugins: [
10 | "@ianvs/prettier-plugin-sort-imports",
11 | "prettier-plugin-tailwindcss",
12 | ],
13 | tailwindConfig: fileURLToPath(
14 | new URL("../../tooling/tailwind/web.ts", import.meta.url),
15 | ),
16 | tailwindFunctions: ["cn", "cva"],
17 | importOrder: [
18 | "",
19 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)",
20 | "^(next/(.*)$)|^(next$)",
21 | "",
22 | "",
23 | "^@cooper",
24 | "^@cooper/(.*)$",
25 | "",
26 | "^[.|..|~]",
27 | "^~/",
28 | "^[../]",
29 | "^[./]",
30 | ],
31 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
32 | importOrderTypeScriptVersion: "4.4.0",
33 | overrides: [
34 | {
35 | files: "*.json.hbs",
36 | options: {
37 | parser: "json",
38 | },
39 | },
40 | {
41 | files: "*.js.hbs",
42 | options: {
43 | parser: "babel",
44 | },
45 | },
46 | ],
47 | };
48 |
49 | export default config;
50 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | # Create T3 App
2 |
3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
4 |
5 | ## What's next? How do I make an app with this?
6 |
7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
8 |
9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [GOOGLE](https://t3.gg/GOOGLE) and ask for help.
10 |
11 | - [Next.js](https://web.org)
12 | - [NextAuth.js](https://next-auth.js.org)
13 | - [Drizzle](https://orm.drizzle.team)
14 | - [Tailwind CSS](https://tailwindcss.com)
15 | - [tRPC](https://trpc.io)
16 |
17 | ## Learn More
18 |
19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
20 |
21 | - [Documentation](https://create.t3.gg/)
22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
23 |
24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
25 |
26 | ## How do I deploy this?
27 |
28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
29 |
--------------------------------------------------------------------------------
/packages/ui/src/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "@cooper/ui";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor;
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ));
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
34 |
--------------------------------------------------------------------------------
/packages/db/drizzle/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7",
3 | "dialect": "postgresql",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "7",
8 | "when": 1742239526673,
9 | "tag": "0000_sloppy_smiling_tiger",
10 | "breakpoints": true
11 | },
12 | {
13 | "idx": 1,
14 | "version": "7",
15 | "when": 1743552656900,
16 | "tag": "0001_petite_white_tiger",
17 | "breakpoints": true
18 | },
19 | {
20 | "idx": 2,
21 | "version": "7",
22 | "when": 1743719701762,
23 | "tag": "0002_closed_black_crow",
24 | "breakpoints": true
25 | },
26 | {
27 | "idx": 3,
28 | "version": "7",
29 | "when": 1763148408980,
30 | "tag": "0003_silky_mister_fear",
31 | "breakpoints": true
32 | },
33 | {
34 | "idx": 4,
35 | "version": "7",
36 | "when": 1763152319700,
37 | "tag": "0004_cooing_the_leader",
38 | "breakpoints": true
39 | },
40 | {
41 | "idx": 5,
42 | "version": "7",
43 | "when": 1764907430477,
44 | "tag": "0005_productive_hulk",
45 | "breakpoints": true
46 | },
47 | {
48 | "idx": 6,
49 | "version": "7",
50 | "when": 1764908076765,
51 | "tag": "0006_nappy_star_brand",
52 | "breakpoints": true
53 | },
54 | {
55 | "idx": 7,
56 | "version": "7",
57 | "when": 1764942026193,
58 | "tag": "0007_simple_korg",
59 | "breakpoints": true
60 | }
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/packages/ui/src/logo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Image from "next/image";
5 |
6 | import type { CompanyType } from "../../db/src/schema/companies";
7 | import { cn } from ".";
8 |
9 | interface ILogoProps {
10 | className?: string;
11 | company: Omit & { slug?: string };
12 | size?: "small" | "default" | "medium";
13 | }
14 |
15 | const Logo: React.FC = ({ company, size, className }) => {
16 | const rawWebsite = company.website;
17 | const website =
18 | rawWebsite && rawWebsite !== ""
19 | ? rawWebsite.replace(/^(https?:\/\/)/, "")
20 | : `${company.name.replace(/\s/g, "")}.com`;
21 | const [imageError, setImageError] = useState(false);
22 | return imageError ? (
23 |
29 | {company.name.charAt(0)}
30 |
31 | ) : (
32 | setImageError(true)}
42 | />
43 | );
44 | };
45 |
46 | export default Logo;
47 |
--------------------------------------------------------------------------------
/apps/docs/docs/04-web/webapp.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Web App
6 |
7 | The web app for Cooper is classic [Next.js](https://nextjs.org/) app.
8 |
9 | All of the web app code is in the `web` package. This package is responsible for handling all the requests that come into the web app. The web app is split into multiple pages.
10 |
11 | ## Pages
12 |
13 | ### Dashboard
14 |
15 | The dashboard is the main page of the web app. It allows the user to search for companies and co-op reviews.
16 |
17 | The dashboard has two subpages:
18 |
19 | - `jobs` - This page shows a list of reviews that the user can search through.
20 | - `companies` - This page shows a list of companies that the user can search through.
21 |
22 | ### Review
23 |
24 | The review page allows the user to write a review for a company. The user can rate the company on various parameters and write a review. The review is then saved to the database.
25 |
26 | All of the heavy lifting for the review page is done by the `ReviewForm` component. On the backend, we ensure that:
27 |
28 | 1. The user is authenticated
29 | 2. The passed in `id` query parameter for the **role** is valid
30 | 3. The user has a profile associated with them
31 |
32 | :::info[Profile]
33 |
34 | Currently, we do **not** have a user onboarding flow in place. As a result, a user profile is never created. In order to access the review page, you will need to manually create a profile for the user. See [Drizzle Studio](/docs/backend/database#studio) for more information.
35 | :::
36 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/header/mobile-header-button.tsx:
--------------------------------------------------------------------------------
1 | // components/MobileHeaderButton.tsx
2 | import Image from "next/image";
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 |
6 | import { Button } from "@cooper/ui/button";
7 |
8 | interface MobileHeaderButtonProps {
9 | href?: string;
10 | iconSrc?: string;
11 | label?: string;
12 | onClick?: () => void;
13 | children?: React.ReactNode;
14 | }
15 |
16 | export default function MobileHeaderButton({
17 | href,
18 | iconSrc,
19 | label = "",
20 | onClick,
21 | children,
22 | }: MobileHeaderButtonProps) {
23 | const path = usePathname();
24 | const button = (
25 |
30 | {iconSrc && }
31 | {children}
32 | {label}
33 |
34 | );
35 |
36 | return (
37 |
38 | {path === href && (
39 |
46 | )}
47 | {href ? (
48 |
49 | {button}
50 |
51 | ) : (
52 | button
53 | )}
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/packages/db/drizzle/0003_silky_mister_fear.sql:
--------------------------------------------------------------------------------
1 | -- Add slug columns as nullable first to allow backfilling
2 | ALTER TABLE "company" ADD COLUMN "slug" varchar;--> statement-breakpoint
3 | ALTER TABLE "role" ADD COLUMN "slug" varchar;--> statement-breakpoint
4 |
5 | -- Backfill company slugs from names
6 | UPDATE "company"
7 | SET "slug" = LOWER(
8 | REGEXP_REPLACE(
9 | REGEXP_REPLACE(name, '[^a-zA-Z0-9\s-]', '', 'g'),
10 | '\s+', '-', 'g'
11 | )
12 | )
13 | WHERE "slug" IS NULL;--> statement-breakpoint
14 |
15 | -- Backfill role slugs from titles (no uniqueness needed - multiple companies can have same role)
16 | UPDATE "role"
17 | SET "slug" = LOWER(
18 | REGEXP_REPLACE(
19 | REGEXP_REPLACE(title, '[^a-zA-Z0-9\s-]', '', 'g'),
20 | '\s+', '-', 'g'
21 | )
22 | )
23 | WHERE "slug" IS NULL;--> statement-breakpoint
24 |
25 | -- Handle duplicate company slugs by appending numbers
26 | WITH numbered_companies AS (
27 | SELECT
28 | id,
29 | slug,
30 | ROW_NUMBER() OVER (PARTITION BY slug ORDER BY "createdAt") as rn
31 | FROM "company"
32 | )
33 | UPDATE "company" c
34 | SET "slug" = nc.slug || '-' || nc.rn
35 | FROM numbered_companies nc
36 | WHERE c.id = nc.id AND nc.rn > 1;--> statement-breakpoint
37 |
38 | -- Now make the columns NOT NULL and add unique constraint only for company
39 | ALTER TABLE "company" ALTER COLUMN "slug" SET NOT NULL;--> statement-breakpoint
40 | ALTER TABLE "role" ALTER COLUMN "slug" SET NOT NULL;--> statement-breakpoint
41 | ALTER TABLE "company" ADD CONSTRAINT "company_slug_unique" UNIQUE("slug");
--------------------------------------------------------------------------------
/apps/auth-proxy/README.md:
--------------------------------------------------------------------------------
1 | # Auth Proxy
2 |
3 | This is a simple proxy server that enables OAuth authentication for preview environments and Expo apps.
4 |
5 | ## Setup
6 |
7 | Deploy it somewhere (Vercel is a one-click, zero-config option) and set the following environment variables:
8 |
9 | - `AUTH_GOOGLE_ID` - The Google OAuth client ID
10 | - `AUTH_GOOGLE_SECRET` - The Google OAuth client secret
11 | - `AUTH_REDIRECT_PROXY_URL` - The URL of this proxy server (e.g. )
12 | - `AUTH_SECRET` - Your secret
13 |
14 | Make sure the `AUTH_SECRET` and `AUTH_REDIRECT_PROXY_URL` match the values set for the main application's deployment for preview environments, and that you're using the same OAuth credentials for the proxy and the application's preview environment.
15 | `AUTH_REDIRECT_PROXY_URL` should only be set for the main application's preview environment. Do not set it for the production environment.
16 | The lines below shows what values should match eachother in both deployments.
17 |
18 | > [!NOTE]
19 | >
20 | > For using the proxy for local development set the `AUTH_REDIRECT_PROXY_URL` in the `.env` file as well.
21 |
22 | 
23 |
24 | For providers that require an origin and a redirect URL, set them to `{AUTH_REDIRECT_PROXY_URL}` and `{AUTH_REDIRECT_PROXY_URL}/r/callback/{provider}` accordingly.
25 |
26 | 
27 |
--------------------------------------------------------------------------------
/tooling/tailwind/base.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
5 | content: ["src/**/*.{ts,tsx}"],
6 | theme: {
7 | extend: {
8 | colors: {
9 | border: "hsl(var(--border))",
10 | input: "hsl(var(--input))",
11 | ring: "hsl(var(--ring))",
12 | background: "hsl(var(--background))",
13 | foreground: "hsl(var(--foreground))",
14 | primary: {
15 | DEFAULT: "hsl(var(--primary))",
16 | foreground: "hsl(var(--primary-foreground))",
17 | },
18 | secondary: {
19 | DEFAULT: "hsl(var(--secondary))",
20 | foreground: "hsl(var(--secondary-foreground))",
21 | },
22 | destructive: {
23 | DEFAULT: "hsl(var(--destructive))",
24 | foreground: "hsl(var(--destructive-foreground))",
25 | },
26 | muted: {
27 | DEFAULT: "hsl(var(--muted))",
28 | foreground: "hsl(var(--muted-foreground))",
29 | },
30 | accent: {
31 | DEFAULT: "hsl(var(--accent))",
32 | foreground: "hsl(var(--accent-foreground))",
33 | },
34 | popover: {
35 | DEFAULT: "hsl(var(--popover))",
36 | foreground: "hsl(var(--popover-foreground))",
37 | },
38 | card: {
39 | DEFAULT: "hsl(var(--card))",
40 | foreground: "hsl(var(--card-foreground))",
41 | },
42 | },
43 | borderColor: {
44 | DEFAULT: "hsl(var(--border))",
45 | },
46 | },
47 | },
48 | } satisfies Config;
49 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/profile/profile-button-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | import type { Session } from "@cooper/auth";
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuLabel,
11 | DropdownMenuSeparator,
12 | DropdownMenuTrigger,
13 | } from "@cooper/ui/dropdown-menu";
14 |
15 | import { handleSignOut } from "../auth/actions";
16 |
17 | interface ProfileButtonClientProps {
18 | session: Session;
19 | }
20 |
21 | export default function ProfileButtonClient({
22 | session,
23 | }: ProfileButtonClientProps) {
24 | const linkElement = (
25 |
32 | );
33 |
34 | return (
35 |
36 |
37 | {linkElement}
38 |
39 |
40 | Profile
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/packages/db/src/schema/roleRequest.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { Company } from "./companies";
7 | import { RequestStatus } from "./misc";
8 | import { Profile } from "./profiles";
9 |
10 | export const RoleRequest = pgTable("role_request", {
11 | id: uuid("id").notNull().primaryKey().defaultRandom(),
12 | roleTitle: varchar("title").notNull(),
13 | roleDescription: text("description"),
14 | companyId: varchar("companyId").notNull(),
15 | createdAt: timestamp("createdAt").defaultNow().notNull(),
16 | status: varchar("status", {
17 | enum: ["PENDING", "APPROVED", "REJECTED"], // Explicitly list the enum values
18 | })
19 | .notNull()
20 | .default("PENDING"),
21 | });
22 |
23 | export type CompanyRequestType = typeof RoleRequest.$inferSelect;
24 |
25 | export const RoleRequestRelations = relations(RoleRequest, ({ one }) => ({
26 | company: one(Company, {
27 | fields: [RoleRequest.companyId],
28 | references: [Company.id],
29 | }),
30 | profile: one(Profile, {
31 | fields: [RoleRequest.id],
32 | references: [Profile.userId],
33 | }),
34 | }));
35 |
36 | // Zod validation schema for creating a role request
37 | export const CreateCompanyRequestSchema = createInsertSchema(RoleRequest, {
38 | roleTitle: z.string(),
39 | roleDescription: z.string(),
40 | status: z.nativeEnum(RequestStatus),
41 | }).omit({
42 | id: true,
43 | createdAt: true,
44 | });
45 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/docs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "clean": "git clean -xdf .docusaurus .turbo node_modules",
7 | "docusaurus": "docusaurus",
8 | "start": "docusaurus start",
9 | "build": "docusaurus build",
10 | "swizzle": "docusaurus swizzle",
11 | "deploy": "docusaurus deploy",
12 | "clear": "docusaurus clear",
13 | "serve": "docusaurus serve",
14 | "write-translations": "docusaurus write-translations",
15 | "write-heading-ids": "docusaurus write-heading-ids",
16 | "typecheck": "tsc --noEmit",
17 | "format": "prettier --check . --ignore-path ../../.gitignore",
18 | "lint": "echo 'No linting configured'"
19 | },
20 | "dependencies": {
21 | "@docusaurus/core": "3.5.2",
22 | "@docusaurus/preset-classic": "3.5.2",
23 | "@mdx-js/react": "^3.0.0",
24 | "clsx": "^2.0.0",
25 | "prism-react-renderer": "^2.3.0",
26 | "react": "catalog:react18",
27 | "react-dom": "catalog:react18"
28 | },
29 | "devDependencies": {
30 | "@docusaurus/module-type-aliases": "3.5.2",
31 | "@docusaurus/tsconfig": "3.5.2",
32 | "@docusaurus/types": "3.5.2",
33 | "prettier": "catalog:",
34 | "typescript": "catalog:"
35 | },
36 | "prettier": "@cooper/prettier-config",
37 | "browserslist": {
38 | "production": [
39 | ">0.5%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 3 chrome version",
45 | "last 3 firefox version",
46 | "last 5 safari version"
47 | ]
48 | },
49 | "engines": {
50 | "node": ">=18.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/db/src/schema/companies.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { CompaniesToLocations } from "./companiesToLocations";
7 | import { Industry } from "./misc";
8 | import { ProfilesToCompanies } from "./profilesToCompanies";
9 | import { Review } from "./reviews";
10 | import { Role } from "./roles";
11 |
12 | export const Company = pgTable("company", {
13 | id: uuid("id").notNull().primaryKey().defaultRandom(),
14 | name: varchar("name").notNull(),
15 | slug: varchar("slug").notNull().unique(),
16 | description: text("description"),
17 | industry: varchar("industry").notNull(),
18 | website: varchar("website"),
19 | createdAt: timestamp("createdAt").defaultNow().notNull(),
20 | updatedAt: timestamp("updatedAt", {
21 | mode: "date",
22 | withTimezone: true,
23 | }).$onUpdate(() => new Date()),
24 | });
25 |
26 | export type CompanyType = typeof Company.$inferSelect;
27 |
28 | export const CompanyRelations = relations(Company, ({ many }) => ({
29 | roles: many(Role),
30 | reviews: many(Review),
31 | companies_to_locations: many(CompaniesToLocations),
32 | profiles_to_companies: many(ProfilesToCompanies),
33 | }));
34 |
35 | export const CreateCompanySchema = createInsertSchema(Company, {
36 | name: z.string(),
37 | description: z.string().optional(),
38 | industry: z.nativeEnum(Industry),
39 | website: z.string().optional(),
40 | }).omit({
41 | id: true,
42 | slug: true,
43 | createdAt: true,
44 | updatedAt: true,
45 | });
46 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches: ["*"]
6 | push:
7 | branches: ["main"]
8 | merge_group:
9 |
10 | concurrency:
11 | group: ${{ github.workflow }}-${{ github.ref }}
12 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
13 |
14 | # You can leverage Vercel Remote Caching with Turbo to speed up your builds
15 | # @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
16 | env:
17 | FORCE_COLOR: 3
18 | TURBO_TEAM: ${{ vars.TURBO_TEAM }}
19 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
20 |
21 | jobs:
22 | lint:
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v4
26 |
27 | - name: Setup
28 | uses: ./tooling/github/setup
29 |
30 | - name: Copy env
31 | shell: bash
32 | run: cp .env.example .env
33 |
34 | - name: Lint
35 | run: pnpm lint && pnpm lint:ws
36 |
37 | format:
38 | runs-on: ubuntu-latest
39 | steps:
40 | - uses: actions/checkout@v4
41 |
42 | - name: Setup
43 | uses: ./tooling/github/setup
44 |
45 | - name: Format
46 | run: pnpm format
47 |
48 | typecheck:
49 | runs-on: ubuntu-latest
50 | steps:
51 | - uses: actions/checkout@v4
52 |
53 | - name: Setup
54 | uses: ./tooling/github/setup
55 |
56 | - name: Typecheck
57 | run: pnpm typecheck
58 |
59 | test:
60 | runs-on: ubuntu-latest
61 | steps:
62 | - uses: actions/checkout@v4
63 |
64 | - name: Setup
65 | uses: ./tooling/github/setup
66 |
67 | - name: Test
68 | run: pnpm test
69 |
--------------------------------------------------------------------------------
/packages/db/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/db",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "exports": {
7 | ".": {
8 | "types": "./dist/index.d.ts",
9 | "default": "./src/index.ts"
10 | },
11 | "./client": {
12 | "types": "./dist/client.d.ts",
13 | "default": "./src/client.ts"
14 | },
15 | "./schema": {
16 | "types": "./dist/schema.d.ts",
17 | "default": "./src/schema.ts"
18 | }
19 | },
20 | "license": "MIT",
21 | "scripts": {
22 | "build": "tsc",
23 | "dev": "tsc --watch",
24 | "clean": "rm -rf .turbo dist node_modules",
25 | "format": "prettier --check . --ignore-path ../../.gitignore",
26 | "lint": "eslint",
27 | "generate": "pnpm with-env drizzle-kit generate",
28 | "migrate": "pnpm with-env drizzle-kit migrate",
29 | "push": "pnpm with-env drizzle-kit push",
30 | "studio": "pnpm with-env drizzle-kit studio",
31 | "typecheck": "tsc --noEmit --emitDeclarationOnly false",
32 | "with-env": "dotenv -e ../../.env --"
33 | },
34 | "dependencies": {
35 | "@t3-oss/env-core": "^0.10.1",
36 | "@vercel/postgres": "^0.9.0",
37 | "dayjs": "^1.11.13",
38 | "drizzle-orm": "^0.31.2",
39 | "drizzle-zod": "^0.5.1",
40 | "zod": "catalog:"
41 | },
42 | "devDependencies": {
43 | "@cooper/eslint-config": "workspace:*",
44 | "@cooper/prettier-config": "workspace:*",
45 | "@cooper/tsconfig": "workspace:*",
46 | "dotenv-cli": "^7.4.2",
47 | "drizzle-kit": "^0.22.8",
48 | "eslint": "catalog:",
49 | "prettier": "catalog:",
50 | "typescript": "catalog:"
51 | },
52 | "prettier": "@cooper/prettier-config"
53 | }
54 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 |
7 | ## Motivation and Context
8 |
9 |
10 |
11 | Closes #[ticket]
12 |
13 | ## How has this been tested?
14 |
15 |
16 |
17 |
18 |
19 | ## Screenshots (if appropriate):
20 |
21 | ## Types of changes
22 |
23 |
24 |
25 | - [ ] Bug fix (non-breaking change which fixes an issue)
26 | - [ ] New feature (non-breaking change which adds functionality)
27 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
28 | - [ ] Database migration
29 | - [ ] Ran `pnpm db:generate` and verified generated SQL migration files in `packages/db/drizzle`
30 |
31 | ## Checklist:
32 |
33 |
34 |
35 |
36 | - [ ] My code follows the code style of this project.
37 | - [ ] I have moved the ticket to "In Review"
38 | - [ ] My change requires a change to the documentation.
39 | - [ ] I have updated the documentation accordingly.
40 | - [ ] I have added tests to cover my changes.
41 | - [ ] All new and existing tests passed.
42 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import { MessageSquare } from "lucide-react";
4 |
5 | export default function Footer() {
6 | return (
7 |
8 |
13 |
20 |
21 | Checkout cooper on GitHub
22 |
23 |
24 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/profile/profile-button.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | import type { Session } from "@cooper/auth";
5 | import { signOut } from "@cooper/auth";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuLabel,
10 | DropdownMenuSeparator,
11 | DropdownMenuTrigger,
12 | } from "@cooper/ui/dropdown-menu";
13 |
14 | interface ProfileButtonProps {
15 | session: Session;
16 | }
17 |
18 | export default function ProfileButton({ session }: ProfileButtonProps) {
19 | const linkElement = (
20 |
27 | );
28 |
29 | return (
30 |
31 |
32 | {linkElement}
33 |
34 |
35 | Profile
36 |
37 |
38 |
39 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/reviews/bar-graph.tsx:
--------------------------------------------------------------------------------
1 | interface BarGraphProps {
2 | title: string;
3 | maxValue: number;
4 | value: number;
5 | industryAvg?: number;
6 | }
7 |
8 | export default function BarGraph({
9 | title,
10 | maxValue,
11 | value,
12 | industryAvg,
13 | }: BarGraphProps) {
14 | const fillPercentage = Math.min(value / maxValue, 1) * 100;
15 | const industryAvgPos = industryAvg ? (industryAvg / maxValue) * 100 : null;
16 | return (
17 |
18 |
19 | {title}
20 |
21 | ?
22 |
23 |
24 |
25 |
26 |
30 |
31 | {industryAvgPos && (
32 |
36 | )}
37 |
38 |
{value.toPrecision(2)}
39 |
40 | {industryAvg && (
41 |
Industry average: {industryAvg}
42 | )}
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/reviews/collapsable-info.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import React, { useState } from "react";
3 |
4 | interface CollapsableInfoCardProps {
5 | title: string;
6 | children: ReactNode;
7 | }
8 |
9 | const CollapsableInfoCard: React.FC = ({
10 | title,
11 | children,
12 | }) => {
13 | const [isExpanded, setIsExpanded] = useState(true);
14 |
15 | return (
16 |
17 |
setIsExpanded(!isExpanded)}
19 | className={`flex w-full items-center gap-2 border-b-[0.75px] bg-gray-50 p-4 font-medium transition-colors hover:bg-gray-100 ${
20 | isExpanded ? "rounded-t-lg border-cooper-gray-400" : "rounded-lg"
21 | }`}
22 | >
23 |
29 |
35 |
36 | {title}
37 |
38 |
45 |
46 | );
47 | };
48 |
49 | export default CollapsableInfoCard;
50 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/profile/profile-tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
4 |
5 | import { cn } from "@cooper/ui";
6 |
7 | export default function ProfileTabs({ numReviews }: { numReviews: number }) {
8 | const tabs = [
9 | { name: "Saved roles", value: "saved-roles" },
10 | { name: "Saved companies", value: "saved-companies" },
11 | { name: "My reviews", value: "my-reviews" },
12 | ];
13 |
14 | const searchParams = useSearchParams();
15 | const router = useRouter();
16 | const pathname = usePathname();
17 |
18 | const currentTab = searchParams.get("tab") ?? "saved-roles";
19 |
20 | const createQueryString = (name: string, value: string) => {
21 | const params = new URLSearchParams(searchParams);
22 | params.set(name, value);
23 | return params.toString();
24 | };
25 |
26 | return (
27 |
31 | {tabs.map((tab) => (
32 | {
35 | router.push(`${pathname}?${createQueryString("tab", tab.value)}`);
36 | }}
37 | className={cn(
38 | tab.value === currentTab
39 | ? "border-primary-500 border-[#151515] text-[#151515]"
40 | : "border-transparent text-[#5A5A5A] hover:border-gray-300 hover:text-gray-700",
41 | "flex items-center whitespace-nowrap border-b-2 px-1 py-2 text-sm font-medium",
42 | )}
43 | >
44 | {tab.name} {tab.name === "My reviews" ? "(" + numReviews + ")" : ""}
45 |
46 | ))}
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/reviews/round-bar-graph.tsx:
--------------------------------------------------------------------------------
1 | interface RoundBarGraphProps {
2 | minValue?: number; // leftmost side of bar
3 | maxValue: number; // rightmost side of bar
4 | lowValue?: number; // smallest value
5 | highValue: number; // largest value
6 | lowIndustryAvg?: number;
7 | highIndustryAvg?: number;
8 | }
9 |
10 | export default function RoundBarGraph({
11 | minValue = 0,
12 | maxValue,
13 | lowValue = 0,
14 | highValue,
15 | lowIndustryAvg,
16 | highIndustryAvg,
17 | }: RoundBarGraphProps) {
18 | const fillPercentage =
19 | Math.min((highValue - lowValue) / (maxValue - minValue), 1) * 100;
20 | const leftPos = ((lowValue - minValue) / (maxValue - minValue)) * 100;
21 |
22 | return (
23 |
24 |
28 | {lowIndustryAvg && (
29 |
35 | )}
36 | {highIndustryAvg && (
37 |
43 | )}
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/header/header-layout-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { ReactNode } from "react";
4 |
5 | import Header from "~/app/_components/header/header";
6 | import { api } from "~/trpc/react";
7 | import LoginButtonClient from "../auth/login-button-client";
8 | import ProfileButtonClient from "../profile/profile-button-client";
9 | /**
10 | * Client-side version of HeaderLayout for use in client components.
11 | * This should be used when placing content under the header in client components.
12 | * @param param0 Children to pass into the layout
13 | * @returns A layout component that standardizes the distance from the header
14 | */
15 | export default function HeaderLayoutClient({
16 | children,
17 | }: {
18 | children: ReactNode;
19 | }) {
20 | const { data: session, isLoading } = api.auth.getSession.useQuery();
21 |
22 | if (isLoading) {
23 | return (
24 |
25 |
26 | } />
27 |
28 |
29 | {children}
30 |
31 |
32 | );
33 | }
34 |
35 | const button = session ? (
36 |
37 | ) : (
38 |
39 | );
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 | {children}
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/packages/ui/src/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5 | import { Circle } from "lucide-react";
6 |
7 | import { cn } from "@cooper/ui";
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | );
20 | });
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
39 |
40 |
41 |
42 |
43 | );
44 | });
45 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
46 |
47 | export { RadioGroup, RadioGroupItem };
48 |
--------------------------------------------------------------------------------
/packages/db/src/schema/companyRequest.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import { pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { Industry, RequestStatus } from "./misc";
7 | import { Profile } from "./profiles";
8 |
9 | export const CompanyRequest = pgTable("company_request", {
10 | id: uuid("id").notNull().primaryKey().defaultRandom(),
11 | companyName: varchar("name").notNull(),
12 | companyDescription: text("description"),
13 | industry: varchar("industry").notNull(),
14 | website: varchar("website"),
15 | locationId: varchar("locationId"),
16 | roleTitle: varchar("title").notNull(),
17 | roleDescription: text("description"),
18 | createdAt: timestamp("createdAt").defaultNow().notNull(),
19 | status: varchar("status", {
20 | enum: ["PENDING", "APPROVED", "REJECTED"], // Explicitly list the enum values
21 | })
22 | .notNull()
23 | .default("PENDING"),
24 | });
25 |
26 | export type CompanyRequestType = typeof CompanyRequest.$inferSelect;
27 |
28 | export const RequestRelations = relations(CompanyRequest, ({ one }) => ({
29 | profile: one(Profile, {
30 | fields: [CompanyRequest.id],
31 | references: [Profile.userId],
32 | }),
33 | }));
34 |
35 | // Zod validation schema for creating a company request
36 | export const CreateCompanyRequestSchema = createInsertSchema(CompanyRequest, {
37 | companyName: z.string(),
38 | companyDescription: z.string().optional(),
39 | industry: z.nativeEnum(Industry),
40 | website: z.string().optional(),
41 | locationId: z.string(),
42 | roleTitle: z.string(),
43 | roleDescription: z.string(),
44 | status: z.nativeEnum(RequestStatus),
45 | }).omit({
46 | id: true,
47 | createdAt: true,
48 | });
49 |
--------------------------------------------------------------------------------
/packages/scraper/scraped/seed data/companies_to_csv.py:
--------------------------------------------------------------------------------
1 | import json
2 | import csv
3 | import uuid
4 | from datetime import datetime
5 |
6 | # Input and output file paths
7 | input_file = "companies.json"
8 | output_file = "companies.csv"
9 |
10 | # Load the JSON data
11 | with open(input_file, "r") as f:
12 | companies = json.load(f)
13 |
14 | # Open CSV file for writing
15 | with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
16 | fieldnames = ["id", "name", "description", "industry", "website", "createdAt", "updatedAt"]
17 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
18 |
19 | # Write header row
20 | writer.writeheader()
21 |
22 | # Current timestamp for createdAt column
23 | current_timestamp = datetime.now().isoformat()
24 |
25 | # Write each company as a row with specified modifications
26 | for name, details in companies.items():
27 | # If industry is null or empty, set to "UNKNOWN"
28 | industry = details.get("industry")
29 | if not industry:
30 | industry = "UNKNOWN"
31 |
32 | desc = details.get('description')
33 | if not desc:
34 | desc = "Learn more at " + website
35 |
36 | # If website is null or empty, set to company name + ".com"
37 | website = details.get("website")
38 | if not website:
39 | website = name.lower() + ".com"
40 |
41 | writer.writerow({
42 | "id": str(uuid.uuid4()),
43 | "name": name,
44 | "description": desc,
45 | "industry": industry,
46 | "website": website,
47 | "createdAt": current_timestamp,
48 | "updatedAt": current_timestamp
49 | })
50 |
51 | print(f"CSV file created at: {output_file}")
52 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/profile/favorite-company-search.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import type { CompanyType } from "@cooper/db/schema";
6 |
7 | import { CompanyCardPreview } from "../companies/company-card-preview";
8 | import { Input } from "../themed/onboarding/input";
9 |
10 | export default function FavoriteCompanySearch({
11 | favoriteCompanies,
12 | }: {
13 | favoriteCompanies: (CompanyType | undefined)[];
14 | }) {
15 | const [companyLabel, setCompanyLabel] = useState("");
16 |
17 | const prefixedCompanies = companyLabel
18 | ? favoriteCompanies.filter(
19 | (company) =>
20 | company?.name.substring(0, companyLabel.length).toLowerCase() ===
21 | companyLabel.toLowerCase(),
22 | )
23 | : favoriteCompanies;
24 | return (
25 |
26 |
27 | {
30 | setCompanyLabel(e.target.value);
31 | }}
32 | className="w-full !border-none !shadow-none !outline-[#EAEAEA]"
33 | placeholder="Search for a saved company..."
34 | />
35 |
36 |
37 | {prefixedCompanies.length > 0 ? (
38 | prefixedCompanies
39 | .filter(
40 | (company): company is NonNullable
=>
41 | company !== undefined,
42 | )
43 | .map((company) => )
44 | ) : (
45 |
46 | No saved companies found.
47 |
48 | )}
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/packages/db/src/schema/roles.ts:
--------------------------------------------------------------------------------
1 | import { relations, sql } from "drizzle-orm";
2 | import { pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
3 | import { createInsertSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { Company } from "./companies";
7 | import { ProfilesToRoles } from "./profilesToRoles";
8 | import { Review } from "./reviews";
9 | import { User } from "./users";
10 |
11 | export const Role = pgTable("role", {
12 | id: uuid("id").notNull().primaryKey().defaultRandom(),
13 | title: varchar("title").notNull(),
14 | slug: varchar("slug").notNull(),
15 | description: text("description"),
16 | jobType: varchar("jobType", {
17 | enum: ["CO-OP", "INTERNSHIP"],
18 | })
19 | .notNull()
20 | .default("CO-OP"),
21 | companyId: varchar("companyId").notNull(),
22 | createdAt: timestamp("created_at").defaultNow().notNull(),
23 | updatedAt: timestamp("updatedAt", {
24 | mode: "date",
25 | withTimezone: true,
26 | }).$onUpdateFn(() => sql`now()`),
27 | createdBy: varchar("createdBy").notNull(),
28 | });
29 |
30 | export type RoleType = typeof Role.$inferSelect;
31 |
32 | export const RoleRelations = relations(Role, ({ one, many }) => ({
33 | company: one(Company, {
34 | fields: [Role.companyId],
35 | references: [Company.id],
36 | }),
37 | reviews: many(Review),
38 | profiles_to_roles: many(ProfilesToRoles),
39 | createdBy: one(User, {
40 | fields: [Role.createdBy],
41 | references: [User.id],
42 | }),
43 | }));
44 |
45 | export const CreateRoleSchema = createInsertSchema(Role, {
46 | title: z.string(),
47 | description: z.string(),
48 | jobType: z.enum(["CO-OP", "INTERNSHIP"]),
49 | companyId: z.string(),
50 | createdBy: z.string(),
51 | }).omit({
52 | id: true,
53 | slug: true,
54 | createdAt: true,
55 | updatedAt: true,
56 | });
57 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/onboarding/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import type { Session } from "@cooper/auth";
6 | import { Dialog, DialogContent } from "@cooper/ui/dialog";
7 |
8 | import { OnboardingForm } from "~/app/_components/onboarding/onboarding-form";
9 | import { api } from "~/trpc/react";
10 |
11 | interface OnboardingDialogProps {
12 | isOpen?: boolean;
13 | session: Session | null;
14 | }
15 |
16 | /**
17 | * OnboardingDialog component that handles user onboarding.
18 | * Implementation note: Use OnboardingWrapper to wrap the component and initiate the dialog.
19 | * @param isOpen - Whether the dialog is open
20 | * @param session - The current user session
21 | * @returns The OnboardingDialog component or null
22 | */
23 | export function OnboardingDialog({
24 | isOpen = true,
25 | session,
26 | }: OnboardingDialogProps) {
27 | const [open, setOpen] = useState(isOpen);
28 |
29 | const profile = api.profile.getCurrentUser.useQuery(undefined, {
30 | refetchOnWindowFocus: false,
31 | });
32 |
33 | const shouldShowOnboarding = session && !profile.data;
34 |
35 | const closeDialog = () => {
36 | setOpen(false);
37 | };
38 |
39 | if (profile.isPending || !shouldShowOnboarding) {
40 | return null;
41 | }
42 |
43 | return (
44 |
45 |
49 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/packages/ui/src/error-toast.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import Image from "next/image";
5 | import * as ToastPrimitives from "@radix-ui/react-toast";
6 |
7 | import { cn } from "@cooper/ui";
8 |
9 | const ErrorToast = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef & {
12 | title?: React.ReactNode;
13 | description?: React.ReactNode;
14 | action?: React.ReactNode;
15 | }
16 | >(({ className, description, action, ...props }, ref) => {
17 | return (
18 |
26 |
27 |
28 |
29 | {description && (
30 |
31 | {description}
32 |
33 | )}
34 | {action &&
{action}
}
35 |
36 |
37 | );
38 | });
39 | ErrorToast.displayName = "ErrorToast";
40 |
41 | export { ErrorToast };
42 |
--------------------------------------------------------------------------------
/apps/web/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "build": "pnpm with-env next build",
8 | "clean": "git clean -xdf .next .turbo node_modules",
9 | "dev": "pnpm with-env next dev",
10 | "format": "prettier --check . --ignore-path ../../.gitignore",
11 | "lint": "eslint",
12 | "start": "pnpm with-env next start",
13 | "typecheck": "tsc --noEmit",
14 | "with-env": "dotenv -e ../../.env --"
15 | },
16 | "dependencies": {
17 | "@cooper/api": "workspace:*",
18 | "@cooper/auth": "workspace:*",
19 | "@cooper/db": "workspace:*",
20 | "@cooper/ui": "workspace:*",
21 | "@cooper/validators": "workspace:*",
22 | "@t3-oss/env-nextjs": "^0.10.1",
23 | "@tanstack/react-query": "^5.49.2",
24 | "@trpc/client": "11.0.0-rc.441",
25 | "@trpc/react-query": "11.0.0-rc.441",
26 | "@trpc/server": "11.0.0-rc.441",
27 | "bad-words": "^4.0.0",
28 | "dayjs": "^1.11.13",
29 | "fuse.js": "^7.0.0",
30 | "geist": "^1.3.0",
31 | "lucide-react": "^0.436.0",
32 | "next": "^14.2.4",
33 | "react": "catalog:react18",
34 | "react-dom": "catalog:react18",
35 | "react-hook-form": "^7.52.1",
36 | "react-scroll": "^1.9.0",
37 | "superjson": "2.2.1",
38 | "zod": "catalog:"
39 | },
40 | "devDependencies": {
41 | "@cooper/eslint-config": "workspace:*",
42 | "@cooper/prettier-config": "workspace:*",
43 | "@cooper/tailwind-config": "workspace:*",
44 | "@cooper/tsconfig": "workspace:*",
45 | "@types/node": "^20.14.15",
46 | "@types/react": "catalog:react18",
47 | "@types/react-dom": "catalog:react18",
48 | "@types/react-scroll": "^1.8.10",
49 | "dotenv-cli": "^7.4.2",
50 | "eslint": "catalog:",
51 | "jiti": "^1.21.6",
52 | "prettier": "catalog:",
53 | "tailwindcss": "^3.4.4",
54 | "typescript": "catalog:"
55 | },
56 | "prettier": "@cooper/prettier-config"
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/public/svg/logoOutline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/docs/docs/03-backend/api.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # API
6 |
7 | The API for Cooper is built using [tRPC](https://trpc.io/).
8 |
9 | All of the API code is in the `api` package. This package is responsible for handling all the requests that come into the API. The API is split into multiple routers, each responsible for a specific set of endpoints.
10 |
11 | ## Routers
12 |
13 | ### Types of Requests
14 |
15 | - `list` - Fetch _all_ the objects for a particular resource in the database
16 | - `get` - Fetch a single object by its ID, name, or other unique identifier
17 | - `create` - Create a new object in the database
18 | - `delete` - Delete an object from the database
19 |
20 | ### tRPC Operations
21 |
22 | tRPC provides a set of operations that can be used to interact with the API. These operations are:
23 |
24 | - **Query** - Fetch data from the API
25 | - **Mutation** - Modify data in the API
26 |
27 | tRPC also has other operations like `Subscription`, but they are not used in Cooper at the moment.
28 |
29 | ### Authentication
30 |
31 | tRPC allows for authentication to be handled at the endpoint level. This means that each endpoint can specify whether it is a `public` endpoint (`publicProcedure`) or a `protected` endpoint (`protectedProcedure`). If an endpoint is `protected`, the user must be authenticated to access it.
32 |
33 | In general, most of the read operations are `public`, while the write operations are `protected`.
34 |
35 | ## New Router
36 |
37 | ### Adding a New Router
38 |
39 | To add a new router to the API, follow these steps:
40 |
41 | 1. Create a new router in the `router/` folder
42 | 2. Export it from the `index.ts` file
43 | 3. Add the route in the `root.ts` file
44 |
45 | ### Defining Endpoints
46 |
47 | An endpoint has two parts:
48 |
49 | - The `input` type, which defines the shape of the request. This makes uses of [Zod](https://zod.dev/) to validate the input.
50 | - The operation (query or mutation) that the endpoint performs
51 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cooper/ui",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "exports": {
7 | ".": "./src/index.ts",
8 | "./*": [
9 | "./src/*.tsx",
10 | "./src/*.ts"
11 | ]
12 | },
13 | "license": "MIT",
14 | "scripts": {
15 | "clean": "rm -rf .turbo node_modules",
16 | "format": "prettier --check . --ignore-path ../../.gitignore",
17 | "lint": "eslint",
18 | "typecheck": "tsc --noEmit --emitDeclarationOnly false",
19 | "ui-add": "pnpm dlx shadcn@latest add && prettier src --write --list-different"
20 | },
21 | "dependencies": {
22 | "@hookform/resolvers": "^3.9.0",
23 | "@radix-ui/react-checkbox": "^1.1.1",
24 | "@radix-ui/react-dialog": "^1.1.1",
25 | "@radix-ui/react-dropdown-menu": "^2.1.1",
26 | "@radix-ui/react-icons": "^1.3.0",
27 | "@radix-ui/react-label": "^2.1.0",
28 | "@radix-ui/react-popover": "^1.1.1",
29 | "@radix-ui/react-radio-group": "^1.2.0",
30 | "@radix-ui/react-select": "^2.1.2",
31 | "@radix-ui/react-slot": "^1.1.0",
32 | "@radix-ui/react-toast": "^1.2.2",
33 | "class-variance-authority": "^0.7.0",
34 | "cmdk": "1.0.0",
35 | "next-themes": "^0.3.0",
36 | "react-hook-form": "^7.52.1",
37 | "sonner": "^1.5.0",
38 | "tailwind-merge": "^2.3.0",
39 | "tailwindcss-animate": "^1.0.7"
40 | },
41 | "devDependencies": {
42 | "@cooper/eslint-config": "workspace:*",
43 | "@cooper/prettier-config": "workspace:*",
44 | "@cooper/tailwind-config": "workspace:*",
45 | "@cooper/tsconfig": "workspace:*",
46 | "@types/react": "catalog:react18",
47 | "eslint": "catalog:",
48 | "prettier": "catalog:",
49 | "react": "catalog:react18",
50 | "tailwindcss": "^3.4.4",
51 | "typescript": "catalog:",
52 | "zod": "catalog:"
53 | },
54 | "peerDependencies": {
55 | "react": "catalog:react18",
56 | "zod": "catalog:"
57 | },
58 | "prettier": "@cooper/prettier-config"
59 | }
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cooper",
3 | "private": true,
4 | "engines": {
5 | "node": ">=20.12.0"
6 | },
7 | "packageManager": "pnpm@9.9.0",
8 | "scripts": {
9 | "__BUILD_____________": "",
10 | "build": "turbo run build",
11 | "__CLEAN_____________": "",
12 | "clean": "git clean -xdf node_modules",
13 | "clean:workspaces": "turbo run clean",
14 | "__TEST______________": "",
15 | "test": "vitest",
16 | "test:ui": "vitest --ui",
17 | "test:run": "vitest run",
18 | "__DATABASE__________": "",
19 | "db:push": "turbo run -F @cooper/db push",
20 | "db:generate": "turbo run -F @cooper/db generate",
21 | "db:migrate": "turbo run -F @cooper/db migrate",
22 | "db:studio": "turbo run -F @cooper/db studio",
23 | "__DEVELOPMENT_______": "",
24 | "dev": "turbo watch dev",
25 | "dev:web": "turbo watch dev -F @cooper/web...",
26 | "dev:docs": "turbo run -F @cooper/docs start",
27 | "__FORMAT____________": "",
28 | "format": "turbo run format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
29 | "format:fix": "turbo run format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
30 | "__LINT______________": "",
31 | "lint": "turbo run lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache",
32 | "lint:fix": "turbo run lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache",
33 | "lint:ws": "pnpm dlx sherif@latest",
34 | "__MISC______________": "",
35 | "postinstall": "pnpm lint:ws",
36 | "typecheck": "turbo run typecheck",
37 | "ui-add": "turbo run ui-add"
38 | },
39 | "devDependencies": {
40 | "@cooper/prettier-config": "workspace:*",
41 | "@turbo/gen": "^2.1.1",
42 | "@vitest/ui": "2.1.2",
43 | "prettier": "catalog:",
44 | "turbo": "^2.1.1",
45 | "typescript": "catalog:",
46 | "vitest": "catalog:"
47 | },
48 | "prettier": "@cooper/prettier-config"
49 | }
50 |
--------------------------------------------------------------------------------
/packages/ui/src/success-toast.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import Image from "next/image";
5 | import * as ToastPrimitives from "@radix-ui/react-toast";
6 |
7 | import { cn } from "@cooper/ui";
8 |
9 | const SuccessToast = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef & {
12 | title?: React.ReactNode;
13 | description?: React.ReactNode;
14 | action?: React.ReactNode;
15 | }
16 | >(({ className, description, action, ...props }, ref) => {
17 | return (
18 |
26 |
27 |
33 |
34 | {description && (
35 |
36 | {description}
37 |
38 | )}
39 |
40 | {action &&
{action}
}
41 |
42 |
43 | );
44 | });
45 | SuccessToast.displayName = "SuccessToast";
46 |
47 | export { SuccessToast };
48 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "ui": "tui",
4 | "tasks": {
5 | "topo": {
6 | "dependsOn": ["^topo"]
7 | },
8 | "build": {
9 | "dependsOn": ["^build"],
10 | "outputs": [
11 | ".next/**",
12 | "!.next/cache/**",
13 | "next-env.d.ts",
14 | ".output/**",
15 | ".vercel/output/**"
16 | ]
17 | },
18 | "dev": {
19 | "persistent": true,
20 | "cache": false
21 | },
22 | "format": {
23 | "outputs": ["node_modules/.cache/.prettiercache"],
24 | "outputLogs": "new-only"
25 | },
26 | "lint": {
27 | "dependsOn": ["^topo", "^build"],
28 | "outputs": ["node_modules/.cache/.eslintcache"]
29 | },
30 | "typecheck": {
31 | "dependsOn": ["^topo", "^build"],
32 | "outputs": ["node_modules/.cache/tsbuildinfo.json"]
33 | },
34 | "clean": {
35 | "cache": false
36 | },
37 | "//#clean": {
38 | "cache": false
39 | },
40 | "ui-add": {
41 | "cache": false,
42 | "interactive": true
43 | },
44 | "push": {
45 | "cache": false,
46 | "interactive": true
47 | },
48 | "generate": {
49 | "cache": false,
50 | "interactive": true
51 | },
52 | "migrate": {
53 | "cache": false,
54 | "interactive": false
55 | },
56 | "studio": {
57 | "cache": false,
58 | "interactive": true
59 | },
60 | "start": {
61 | "cache": false,
62 | "interactive": true
63 | },
64 | "test": {
65 | "cache": true,
66 | "interactive": true
67 | }
68 | },
69 | "globalEnv": [
70 | "POSTGRES_URL",
71 | "AUTH_GOOGLE_ID",
72 | "AUTH_GOOGLE_SECRET",
73 | "AUTH_REDIRECT_PROXY_URL",
74 | "AUTH_SECRET",
75 | "PORT"
76 | ],
77 | "globalPassThroughEnv": [
78 | "NODE_ENV",
79 | "CI",
80 | "VERCEL",
81 | "VERCEL_ENV",
82 | "VERCEL_URL",
83 | "npm_lifecycle_event"
84 | ]
85 | }
86 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/companies/all-company-roles.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/navigation";
2 |
3 | import { cn } from "@cooper/ui";
4 |
5 | import { api } from "~/trpc/react";
6 | import LoadingResults from "../loading-results";
7 | import NewRoleCard from "../reviews/new-role-card";
8 | import { RoleCardPreview } from "../reviews/role-card-preview";
9 | import type { CompanyType } from "@cooper/db/schema";
10 |
11 | interface RenderAllRolesProps {
12 | company: CompanyType | null;
13 | }
14 |
15 | export default function RenderAllRoles({ company }: RenderAllRolesProps) {
16 | const roles = api.role.getByCompany.useQuery({
17 | companyId: company?.id ?? "",
18 | });
19 | const router = useRouter();
20 |
21 | return (
22 |
23 |
24 | Roles at {company?.name} ({roles.data?.length})
25 |
26 | {roles.isPending ? (
27 |
28 | ) : (
29 |
30 | {roles.isSuccess && roles.data.length > 0 && (
31 | <>
32 | {roles.data.map((role) => {
33 | return (
34 |
router.push(`/role?id=${role.id}`)}
38 | >
39 |
45 |
46 | );
47 | })}
48 | >
49 | )}
50 | {company && (
51 |
52 |
53 |
54 | )}
55 |
56 | )}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/packages/db/src/schema/misc.ts:
--------------------------------------------------------------------------------
1 | export const Industry = {
2 | TECHNOLOGY: "TECHNOLOGY",
3 | HEALTHCARE: "HEALTHCARE",
4 | FINANCE: "FINANCE",
5 | EDUCATION: "EDUCATION",
6 | MANUFACTURING: "MANUFACTURING",
7 | HOSPITALITY: "HOSPITALITY",
8 | RETAIL: "RETAIL",
9 | TRANSPORTATION: "TRANSPORTATION",
10 | ENERGY: "ENERGY",
11 | MEDIA: "MEDIA",
12 | AEROSPACE: "AEROSPACE",
13 | TELECOMMUNICATIONS: "TELECOMMUNICATIONS",
14 | BIOTECHNOLOGY: "BIOTECHNOLOGY",
15 | PHARMACEUTICAL: "PHARMACEUTICAL",
16 | CONSTRUCTION: "CONSTRUCTION",
17 | REALESTATE: "REALESTATE",
18 | FASHIONANDBEAUTY: "FASHIONANDBEAUTY",
19 | ENTERTAINMENT: "ENTERTAINMENT",
20 | GOVERNMENT: "GOVERNMENT",
21 | NONPROFIT: "NONPROFIT",
22 | FOODANDBEVERAGE: "FOODANDBEVERAGE",
23 | GAMING: "GAMING",
24 | SPORTS: "SPORTS",
25 | MARKETING: "MARKETING",
26 | CONSULTING: "CONSULTING",
27 | FITNESS: "FITNESS",
28 | ECOMMERCE: "ECOMMERCE",
29 | ENVIRONMENTAL: "ENVIRONMENTAL",
30 | ROBOTICS: "ROBOTICS",
31 | MUSIC: "MUSIC",
32 | INSURANCE: "INSURANCE",
33 | DESIGN: "DESIGN",
34 | PUBLISHING: "PUBLISHING",
35 | ARCHITECTURE: "ARCHITECTURE",
36 | VETERINARY: "VETERINARY",
37 | } as const;
38 |
39 | export type IndustryType = (typeof Industry)[keyof typeof Industry];
40 |
41 | export const WorkEnvironment = {
42 | INPERSON: "INPERSON",
43 | HYBRID: "HYBRID",
44 | REMOTE: "REMOTE",
45 | } as const;
46 |
47 | export const JobType = {
48 | COOP: "coop",
49 | PARTTIME: "parttime",
50 | INTERNSHIP: "internship",
51 | } as const;
52 |
53 | export type WorkEnvironmentType =
54 | (typeof WorkEnvironment)[keyof typeof WorkEnvironment];
55 |
56 | export const WorkTerm = {
57 | FALL: "FALL",
58 | SPRING: "SPRING",
59 | SUMMER: "SUMMER",
60 | } as const;
61 |
62 | export type WorkTermType = (typeof WorkTerm)[keyof typeof WorkTerm];
63 |
64 | export const RequestStatus = {
65 | PENDING: "PENDING",
66 | APPROVED: "APPROVED",
67 | REJECTED: "REJECTED",
68 | } as const;
69 |
70 | export type RequestStatusType =
71 | (typeof RequestStatus)[keyof typeof RequestStatus];
72 |
--------------------------------------------------------------------------------
/tooling/eslint/types.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Since the ecosystem hasn't fully migrated to ESLint's new FlatConfig system yet,
3 | * we "need" to type some of the plugins manually :(
4 | */
5 |
6 | declare module "@eslint/js" {
7 | // Why the hell doesn't eslint themselves export their types?
8 | import type { Linter } from "eslint";
9 |
10 | export const configs: {
11 | readonly recommended: { readonly rules: Readonly };
12 | readonly all: { readonly rules: Readonly };
13 | };
14 | }
15 |
16 | declare module "eslint-plugin-import" {
17 | import type { Linter, Rule } from "eslint";
18 |
19 | export const configs: {
20 | recommended: { rules: Linter.RulesRecord };
21 | };
22 | export const rules: Record;
23 | }
24 |
25 | declare module "eslint-plugin-react" {
26 | import type { Linter, Rule } from "eslint";
27 |
28 | export const configs: {
29 | recommended: { rules: Linter.RulesRecord };
30 | all: { rules: Linter.RulesRecord };
31 | "jsx-runtime": { rules: Linter.RulesRecord };
32 | };
33 | export const rules: Record;
34 | }
35 |
36 | declare module "eslint-plugin-react-hooks" {
37 | import type { Linter, Rule } from "eslint";
38 |
39 | export const configs: {
40 | recommended: {
41 | rules: {
42 | "rules-of-hooks": Linter.RuleEntry;
43 | "exhaustive-deps": Linter.RuleEntry;
44 | };
45 | };
46 | };
47 | export const rules: Record;
48 | }
49 |
50 | declare module "@next/eslint-plugin-next" {
51 | import type { Linter, Rule } from "eslint";
52 |
53 | export const configs: {
54 | recommended: { rules: Linter.RulesRecord };
55 | "core-web-vitals": { rules: Linter.RulesRecord };
56 | };
57 | export const rules: Record;
58 | }
59 |
60 | declare module "eslint-plugin-turbo" {
61 | import type { Linter, Rule } from "eslint";
62 |
63 | export const configs: {
64 | recommended: { rules: Linter.RulesRecord };
65 | };
66 | export const rules: Record;
67 | }
68 |
--------------------------------------------------------------------------------
/packages/db/src/schema/profiles.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import {
3 | integer,
4 | pgTable,
5 | timestamp,
6 | uuid,
7 | varchar,
8 | } from "drizzle-orm/pg-core";
9 | import { createInsertSchema } from "drizzle-zod";
10 | import { z } from "zod";
11 |
12 | import { ProfilesToCompanies } from "./profilesToCompanies";
13 | import { Review } from "./reviews";
14 | import { User } from "./users";
15 |
16 | export const Profile = pgTable("profile", {
17 | id: uuid("id").notNull().primaryKey().defaultRandom(),
18 | firstName: varchar("firstName").notNull(),
19 | lastName: varchar("lastName").notNull(),
20 | major: varchar("major").notNull(),
21 | minor: varchar("minor"),
22 | graduationYear: integer("graduationYear").notNull(),
23 | graduationMonth: integer("graduationMonth").notNull(),
24 | createdAt: timestamp("created_at").defaultNow().notNull(),
25 | updatedAt: timestamp("updatedAt", {
26 | mode: "date",
27 | withTimezone: true,
28 | })
29 | .defaultNow()
30 | .$onUpdate(() => new Date())
31 | .notNull(),
32 | userId: varchar("userId").notNull().unique(),
33 | });
34 |
35 | export const ProfileRelations = relations(Profile, ({ one, many }) => ({
36 | user: one(User, {
37 | fields: [Profile.userId],
38 | references: [User.id],
39 | }),
40 | reviews: many(Review),
41 | proflies_to_companies: many(ProfilesToCompanies),
42 | }));
43 |
44 | const MAX_GRADUATION_LENGTH = 6;
45 | const MONTH_LB = 1;
46 | const MONTH_UB = 12;
47 | const YEAR_LB = new Date().getFullYear();
48 | const YEAR_UB = YEAR_LB + MAX_GRADUATION_LENGTH;
49 |
50 | export const CreateProfileSchema = createInsertSchema(Profile, {
51 | firstName: z.string(),
52 | lastName: z.string(),
53 | major: z.string(),
54 | minor: z.string().optional(),
55 | graduationYear: z.number().min(YEAR_LB).max(YEAR_UB),
56 | graduationMonth: z.number().min(MONTH_LB).max(MONTH_UB),
57 | }).omit({
58 | id: true,
59 | createdAt: true,
60 | updatedAt: true,
61 | });
62 |
63 | export const UpdateProfileNameMajorSchema = z.object({
64 | id: z.string(),
65 | firstName: z.string(),
66 | lastName: z.string(),
67 | major: z.string(),
68 | });
69 |
--------------------------------------------------------------------------------
/apps/web/src/app/_components/companies/company-reviews.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { CompanyType } from "@cooper/db/schema";
4 |
5 | import { api } from "~/trpc/react";
6 | import { calculateRatings } from "~/utils/reviewCountByStars";
7 | import StarGraph from "../shared/star-graph";
8 | import CompanyStatistics from "./company-statistics";
9 | import {
10 | calculatePay,
11 | calculatePayRange,
12 | calculateWorkModels,
13 | } from "~/utils/companyStatistics";
14 |
15 | interface CompanyReviewProps {
16 | className?: string;
17 | companyObj: CompanyType | undefined;
18 | }
19 |
20 | export function CompanyReview({ companyObj }: CompanyReviewProps) {
21 | const reviews = api.review.getByCompany.useQuery({
22 | id: companyObj?.id ?? "",
23 | });
24 |
25 | const avg = api.company.getAverageById.useQuery({
26 | companyId: companyObj?.id ?? "",
27 | });
28 |
29 | const ratings = calculateRatings(reviews.data ?? []);
30 | const workModels = calculateWorkModels(reviews.data ?? []);
31 | const payStats = calculatePay(reviews.data ?? []);
32 | const payRange = calculatePayRange(reviews.data ?? []);
33 |
34 | const averages = api.review.list
35 | .useQuery({})
36 | .data?.filter((r) => r.overallRating != 0)
37 | .map((review) => review.overallRating);
38 | const cooperAvg: number =
39 | Math.round(
40 | ((averages ?? []).reduce((accumulator, currentValue) => {
41 | return accumulator + currentValue;
42 | }, 0) /
43 | (averages?.length ?? 1)) *
44 | 10,
45 | ) / 10;
46 |
47 | return (
48 |
49 |
50 |
56 | {(reviews.data?.length ?? 0) > 0 && (
57 |
63 | )}
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------