Merge branch 'main' into hongpo/sorting-pagination

pull/457/head
Jeff Sieu 3 years ago
commit b0b91ea0c8

@ -5,4 +5,7 @@ module.exports = {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
rules: {
'@typescript-eslint/ban-ts-comment': 0,
},
};

@ -9,7 +9,8 @@
"lint": "next lint",
"tsc": "tsc",
"postinstall": "prisma generate",
"seed": "ts-node prisma/seed.ts"
"seed": "ts-node prisma/seed.ts",
"seed-questions": "ts-node prisma/seed-questions.ts"
},
"dependencies": {
"@headlessui/react": "^1.7.3",

File diff suppressed because it is too large Load Diff

@ -0,0 +1,410 @@
{
"data": [
{ "country_id": "1", "sortname": "AF", "country_name": "Afghanistan" },
{ "country_id": "2", "sortname": "AL", "country_name": "Albania" },
{ "country_id": "3", "sortname": "DZ", "country_name": "Algeria" },
{ "country_id": "4", "sortname": "AS", "country_name": "American Samoa" },
{ "country_id": "5", "sortname": "AD", "country_name": "Andorra" },
{ "country_id": "6", "sortname": "AO", "country_name": "Angola" },
{ "country_id": "7", "sortname": "AI", "country_name": "Anguilla" },
{ "country_id": "8", "sortname": "AQ", "country_name": "Antarctica" },
{
"country_id": "9",
"sortname": "AG",
"country_name": "Antigua And Barbuda"
},
{ "country_id": "10", "sortname": "AR", "country_name": "Argentina" },
{ "country_id": "11", "sortname": "AM", "country_name": "Armenia" },
{ "country_id": "12", "sortname": "AW", "country_name": "Aruba" },
{ "country_id": "13", "sortname": "AU", "country_name": "Australia" },
{ "country_id": "14", "sortname": "AT", "country_name": "Austria" },
{ "country_id": "15", "sortname": "AZ", "country_name": "Azerbaijan" },
{ "country_id": "16", "sortname": "BS", "country_name": "The Bahamas" },
{ "country_id": "17", "sortname": "BH", "country_name": "Bahrain" },
{ "country_id": "18", "sortname": "BD", "country_name": "Bangladesh" },
{ "country_id": "19", "sortname": "BB", "country_name": "Barbados" },
{ "country_id": "20", "sortname": "BY", "country_name": "Belarus" },
{ "country_id": "21", "sortname": "BE", "country_name": "Belgium" },
{ "country_id": "22", "sortname": "BZ", "country_name": "Belize" },
{ "country_id": "23", "sortname": "BJ", "country_name": "Benin" },
{ "country_id": "24", "sortname": "BM", "country_name": "Bermuda" },
{ "country_id": "25", "sortname": "BT", "country_name": "Bhutan" },
{ "country_id": "26", "sortname": "BO", "country_name": "Bolivia" },
{
"country_id": "27",
"sortname": "BA",
"country_name": "Bosnia and Herzegovina"
},
{ "country_id": "28", "sortname": "BW", "country_name": "Botswana" },
{ "country_id": "29", "sortname": "BV", "country_name": "Bouvet Island" },
{ "country_id": "30", "sortname": "BR", "country_name": "Brazil" },
{
"country_id": "31",
"sortname": "IO",
"country_name": "British Indian Ocean Territory"
},
{ "country_id": "32", "sortname": "BN", "country_name": "Brunei" },
{ "country_id": "33", "sortname": "BG", "country_name": "Bulgaria" },
{ "country_id": "34", "sortname": "BF", "country_name": "Burkina Faso" },
{ "country_id": "35", "sortname": "BI", "country_name": "Burundi" },
{ "country_id": "36", "sortname": "KH", "country_name": "Cambodia" },
{ "country_id": "37", "sortname": "CM", "country_name": "Cameroon" },
{ "country_id": "38", "sortname": "CA", "country_name": "Canada" },
{ "country_id": "39", "sortname": "CV", "country_name": "Cape Verde" },
{ "country_id": "40", "sortname": "KY", "country_name": "Cayman Islands" },
{
"country_id": "41",
"sortname": "CF",
"country_name": "Central African Republic"
},
{ "country_id": "42", "sortname": "TD", "country_name": "Chad" },
{ "country_id": "43", "sortname": "CL", "country_name": "Chile" },
{ "country_id": "44", "sortname": "CN", "country_name": "China" },
{
"country_id": "45",
"sortname": "CX",
"country_name": "Christmas Island"
},
{
"country_id": "46",
"sortname": "CC",
"country_name": "Cocos (Keeling) Islands"
},
{ "country_id": "47", "sortname": "CO", "country_name": "Colombia" },
{ "country_id": "48", "sortname": "KM", "country_name": "Comoros" },
{ "country_id": "49", "sortname": "CG", "country_name": "Congo" },
{
"country_id": "50",
"sortname": "CD",
"country_name": "Democratic Republic of The Congo"
},
{ "country_id": "51", "sortname": "CK", "country_name": "Cook Islands" },
{ "country_id": "52", "sortname": "CR", "country_name": "Costa Rica" },
{
"country_id": "53",
"sortname": "CI",
"country_name": "Cote D'Ivoire (Ivory Coast)"
},
{
"country_id": "54",
"sortname": "HR",
"country_name": "Croatia (Hrvatska)"
},
{ "country_id": "55", "sortname": "CU", "country_name": "Cuba" },
{ "country_id": "56", "sortname": "CY", "country_name": "Cyprus" },
{ "country_id": "57", "sortname": "CZ", "country_name": "Czech Republic" },
{ "country_id": "58", "sortname": "DK", "country_name": "Denmark" },
{ "country_id": "59", "sortname": "DJ", "country_name": "Djibouti" },
{ "country_id": "60", "sortname": "DM", "country_name": "Dominica" },
{
"country_id": "61",
"sortname": "DO",
"country_name": "Dominican Republic"
},
{ "country_id": "62", "sortname": "TP", "country_name": "East Timor" },
{ "country_id": "63", "sortname": "EC", "country_name": "Ecuador" },
{ "country_id": "64", "sortname": "EG", "country_name": "Egypt" },
{ "country_id": "65", "sortname": "SV", "country_name": "El Salvador" },
{
"country_id": "66",
"sortname": "GQ",
"country_name": "Equatorial Guinea"
},
{ "country_id": "67", "sortname": "ER", "country_name": "Eritrea" },
{ "country_id": "68", "sortname": "EE", "country_name": "Estonia" },
{ "country_id": "69", "sortname": "ET", "country_name": "Ethiopia" },
{
"country_id": "70",
"sortname": "XA",
"country_name": "External Territories of Australia"
},
{
"country_id": "71",
"sortname": "FK",
"country_name": "Falkland Islands"
},
{ "country_id": "72", "sortname": "FO", "country_name": "Faroe Islands" },
{ "country_id": "73", "sortname": "FJ", "country_name": "Fiji Islands" },
{ "country_id": "74", "sortname": "FI", "country_name": "Finland" },
{ "country_id": "75", "sortname": "FR", "country_name": "France" },
{ "country_id": "76", "sortname": "GF", "country_name": "French Guiana" },
{
"country_id": "77",
"sortname": "PF",
"country_name": "French Polynesia"
},
{
"country_id": "78",
"sortname": "TF",
"country_name": "French Southern Territories"
},
{ "country_id": "79", "sortname": "GA", "country_name": "Gabon" },
{ "country_id": "80", "sortname": "GM", "country_name": "The Gambia" },
{ "country_id": "81", "sortname": "GE", "country_name": "Georgia" },
{ "country_id": "82", "sortname": "DE", "country_name": "Germany" },
{ "country_id": "83", "sortname": "GH", "country_name": "Ghana" },
{ "country_id": "84", "sortname": "GI", "country_name": "Gibraltar" },
{ "country_id": "85", "sortname": "GR", "country_name": "Greece" },
{ "country_id": "86", "sortname": "GL", "country_name": "Greenland" },
{ "country_id": "87", "sortname": "GD", "country_name": "Grenada" },
{ "country_id": "88", "sortname": "GP", "country_name": "Guadeloupe" },
{ "country_id": "89", "sortname": "GU", "country_name": "Guam" },
{ "country_id": "90", "sortname": "GT", "country_name": "Guatemala" },
{
"country_id": "91",
"sortname": "XU",
"country_name": "Guernsey and Alderney"
},
{ "country_id": "92", "sortname": "GN", "country_name": "Guinea" },
{ "country_id": "93", "sortname": "GW", "country_name": "Guinea-Bissau" },
{ "country_id": "94", "sortname": "GY", "country_name": "Guyana" },
{ "country_id": "95", "sortname": "HT", "country_name": "Haiti" },
{
"country_id": "96",
"sortname": "HM",
"country_name": "Heard and McDonald Islands"
},
{ "country_id": "97", "sortname": "HN", "country_name": "Honduras" },
{
"country_id": "98",
"sortname": "HK",
"country_name": "Hong Kong"
},
{ "country_id": "99", "sortname": "HU", "country_name": "Hungary" },
{ "country_id": "100", "sortname": "IS", "country_name": "Iceland" },
{ "country_id": "101", "sortname": "IN", "country_name": "India" },
{ "country_id": "102", "sortname": "ID", "country_name": "Indonesia" },
{ "country_id": "103", "sortname": "IR", "country_name": "Iran" },
{ "country_id": "104", "sortname": "IQ", "country_name": "Iraq" },
{ "country_id": "105", "sortname": "IE", "country_name": "Ireland" },
{ "country_id": "106", "sortname": "IL", "country_name": "Israel" },
{ "country_id": "107", "sortname": "IT", "country_name": "Italy" },
{ "country_id": "108", "sortname": "JM", "country_name": "Jamaica" },
{ "country_id": "109", "sortname": "JP", "country_name": "Japan" },
{ "country_id": "110", "sortname": "XJ", "country_name": "Jersey" },
{ "country_id": "111", "sortname": "JO", "country_name": "Jordan" },
{ "country_id": "112", "sortname": "KZ", "country_name": "Kazakhstan" },
{ "country_id": "113", "sortname": "KE", "country_name": "Kenya" },
{ "country_id": "114", "sortname": "KI", "country_name": "Kiribati" },
{ "country_id": "115", "sortname": "KP", "country_name": "North Korea" },
{ "country_id": "116", "sortname": "KR", "country_name": "South Korea" },
{ "country_id": "117", "sortname": "KW", "country_name": "Kuwait" },
{ "country_id": "118", "sortname": "KG", "country_name": "Kyrgyzstan" },
{ "country_id": "119", "sortname": "LA", "country_name": "Laos" },
{ "country_id": "120", "sortname": "LV", "country_name": "Latvia" },
{ "country_id": "121", "sortname": "LB", "country_name": "Lebanon" },
{ "country_id": "122", "sortname": "LS", "country_name": "Lesotho" },
{ "country_id": "123", "sortname": "LR", "country_name": "Liberia" },
{ "country_id": "124", "sortname": "LY", "country_name": "Libya" },
{ "country_id": "125", "sortname": "LI", "country_name": "Liechtenstein" },
{ "country_id": "126", "sortname": "LT", "country_name": "Lithuania" },
{ "country_id": "127", "sortname": "LU", "country_name": "Luxembourg" },
{ "country_id": "128", "sortname": "MO", "country_name": "Macau" },
{ "country_id": "129", "sortname": "MK", "country_name": "Macedonia" },
{ "country_id": "130", "sortname": "MG", "country_name": "Madagascar" },
{ "country_id": "131", "sortname": "MW", "country_name": "Malawi" },
{ "country_id": "132", "sortname": "MY", "country_name": "Malaysia" },
{ "country_id": "133", "sortname": "MV", "country_name": "Maldives" },
{ "country_id": "134", "sortname": "ML", "country_name": "Mali" },
{ "country_id": "135", "sortname": "MT", "country_name": "Malta" },
{ "country_id": "136", "sortname": "XM", "country_name": "Isle of Man" },
{
"country_id": "137",
"sortname": "MH",
"country_name": "Marshall Islands"
},
{ "country_id": "138", "sortname": "MQ", "country_name": "Martinique" },
{ "country_id": "139", "sortname": "MR", "country_name": "Mauritania" },
{ "country_id": "140", "sortname": "MU", "country_name": "Mauritius" },
{ "country_id": "141", "sortname": "YT", "country_name": "Mayotte" },
{ "country_id": "142", "sortname": "MX", "country_name": "Mexico" },
{ "country_id": "143", "sortname": "FM", "country_name": "Micronesia" },
{ "country_id": "144", "sortname": "MD", "country_name": "Moldova" },
{ "country_id": "145", "sortname": "MC", "country_name": "Monaco" },
{ "country_id": "146", "sortname": "MN", "country_name": "Mongolia" },
{ "country_id": "147", "sortname": "MS", "country_name": "Montserrat" },
{ "country_id": "148", "sortname": "MA", "country_name": "Morocco" },
{ "country_id": "149", "sortname": "MZ", "country_name": "Mozambique" },
{ "country_id": "150", "sortname": "MM", "country_name": "Myanmar" },
{ "country_id": "151", "sortname": "NA", "country_name": "Namibia" },
{ "country_id": "152", "sortname": "NR", "country_name": "Nauru" },
{ "country_id": "153", "sortname": "NP", "country_name": "Nepal" },
{
"country_id": "154",
"sortname": "AN",
"country_name": "Netherlands Antilles"
},
{
"country_id": "155",
"sortname": "NL",
"country_name": "Netherlands The"
},
{ "country_id": "156", "sortname": "NC", "country_name": "New Caledonia" },
{ "country_id": "157", "sortname": "NZ", "country_name": "New Zealand" },
{ "country_id": "158", "sortname": "NI", "country_name": "Nicaragua" },
{ "country_id": "159", "sortname": "NE", "country_name": "Niger" },
{ "country_id": "160", "sortname": "NG", "country_name": "Nigeria" },
{ "country_id": "161", "sortname": "NU", "country_name": "Niue" },
{ "country_id": "162", "sortname": "NF", "country_name": "Norfolk Island" },
{
"country_id": "163",
"sortname": "MP",
"country_name": "Northern Mariana Islands"
},
{ "country_id": "164", "sortname": "NO", "country_name": "Norway" },
{ "country_id": "165", "sortname": "OM", "country_name": "Oman" },
{ "country_id": "166", "sortname": "PK", "country_name": "Pakistan" },
{ "country_id": "167", "sortname": "PW", "country_name": "Palau" },
{
"country_id": "168",
"sortname": "PS",
"country_name": "Palestinian Territory Occupied"
},
{ "country_id": "169", "sortname": "PA", "country_name": "Panama" },
{
"country_id": "170",
"sortname": "PG",
"country_name": "Papua new Guinea"
},
{ "country_id": "171", "sortname": "PY", "country_name": "Paraguay" },
{ "country_id": "172", "sortname": "PE", "country_name": "Peru" },
{ "country_id": "173", "sortname": "PH", "country_name": "Philippines" },
{
"country_id": "174",
"sortname": "PN",
"country_name": "Pitcairn Island"
},
{ "country_id": "175", "sortname": "PL", "country_name": "Poland" },
{ "country_id": "176", "sortname": "PT", "country_name": "Portugal" },
{ "country_id": "177", "sortname": "PR", "country_name": "Puerto Rico" },
{ "country_id": "178", "sortname": "QA", "country_name": "Qatar" },
{ "country_id": "179", "sortname": "RE", "country_name": "Reunion" },
{ "country_id": "180", "sortname": "RO", "country_name": "Romania" },
{ "country_id": "181", "sortname": "RU", "country_name": "Russia" },
{ "country_id": "182", "sortname": "RW", "country_name": "Rwanda" },
{ "country_id": "183", "sortname": "SH", "country_name": "Saint Helena" },
{
"country_id": "184",
"sortname": "KN",
"country_name": "Saint Kitts And Nevis"
},
{ "country_id": "185", "sortname": "LC", "country_name": "Saint Lucia" },
{
"country_id": "186",
"sortname": "PM",
"country_name": "Saint Pierre and Miquelon"
},
{
"country_id": "187",
"sortname": "VC",
"country_name": "Saint Vincent and The Grenadines"
},
{ "country_id": "188", "sortname": "WS", "country_name": "Samoa" },
{ "country_id": "189", "sortname": "SM", "country_name": "San Marino" },
{
"country_id": "190",
"sortname": "ST",
"country_name": "Sao Tome and Principe"
},
{ "country_id": "191", "sortname": "SA", "country_name": "Saudi Arabia" },
{ "country_id": "192", "sortname": "SN", "country_name": "Senegal" },
{ "country_id": "193", "sortname": "RS", "country_name": "Serbia" },
{ "country_id": "194", "sortname": "SC", "country_name": "Seychelles" },
{ "country_id": "195", "sortname": "SL", "country_name": "Sierra Leone" },
{ "country_id": "196", "sortname": "SG", "country_name": "Singapore" },
{ "country_id": "197", "sortname": "SK", "country_name": "Slovakia" },
{ "country_id": "198", "sortname": "SI", "country_name": "Slovenia" },
{
"country_id": "199",
"sortname": "XG",
"country_name": "Smaller Territories of the UK"
},
{
"country_id": "200",
"sortname": "SB",
"country_name": "Solomon Islands"
},
{ "country_id": "201", "sortname": "SO", "country_name": "Somalia" },
{ "country_id": "202", "sortname": "ZA", "country_name": "South Africa" },
{ "country_id": "203", "sortname": "GS", "country_name": "South Georgia" },
{ "country_id": "204", "sortname": "SS", "country_name": "South Sudan" },
{ "country_id": "205", "sortname": "ES", "country_name": "Spain" },
{ "country_id": "206", "sortname": "LK", "country_name": "Sri Lanka" },
{ "country_id": "207", "sortname": "SD", "country_name": "Sudan" },
{ "country_id": "208", "sortname": "SR", "country_name": "Suriname" },
{
"country_id": "209",
"sortname": "SJ",
"country_name": "Svalbard And Jan Mayen Islands"
},
{ "country_id": "210", "sortname": "SZ", "country_name": "Swaziland" },
{ "country_id": "211", "sortname": "SE", "country_name": "Sweden" },
{ "country_id": "212", "sortname": "CH", "country_name": "Switzerland" },
{ "country_id": "213", "sortname": "SY", "country_name": "Syria" },
{ "country_id": "214", "sortname": "TW", "country_name": "Taiwan" },
{ "country_id": "215", "sortname": "TJ", "country_name": "Tajikistan" },
{ "country_id": "216", "sortname": "TZ", "country_name": "Tanzania" },
{ "country_id": "217", "sortname": "TH", "country_name": "Thailand" },
{ "country_id": "218", "sortname": "TG", "country_name": "Togo" },
{ "country_id": "219", "sortname": "TK", "country_name": "Tokelau" },
{ "country_id": "220", "sortname": "TO", "country_name": "Tonga" },
{
"country_id": "221",
"sortname": "TT",
"country_name": "Trinidad And Tobago"
},
{ "country_id": "222", "sortname": "TN", "country_name": "Tunisia" },
{ "country_id": "223", "sortname": "TR", "country_name": "Turkey" },
{ "country_id": "224", "sortname": "TM", "country_name": "Turkmenistan" },
{
"country_id": "225",
"sortname": "TC",
"country_name": "Turks And Caicos Islands"
},
{ "country_id": "226", "sortname": "TV", "country_name": "Tuvalu" },
{ "country_id": "227", "sortname": "UG", "country_name": "Uganda" },
{ "country_id": "228", "sortname": "UA", "country_name": "Ukraine" },
{
"country_id": "229",
"sortname": "AE",
"country_name": "United Arab Emirates"
},
{ "country_id": "230", "sortname": "GB", "country_name": "United Kingdom" },
{ "country_id": "231", "sortname": "US", "country_name": "United States" },
{
"country_id": "232",
"sortname": "UM",
"country_name": "United States Minor Outlying Islands"
},
{ "country_id": "233", "sortname": "UY", "country_name": "Uruguay" },
{ "country_id": "234", "sortname": "UZ", "country_name": "Uzbekistan" },
{ "country_id": "235", "sortname": "VU", "country_name": "Vanuatu" },
{
"country_id": "236",
"sortname": "VA",
"country_name": "Vatican City State (Holy See)"
},
{ "country_id": "237", "sortname": "VE", "country_name": "Venezuela" },
{ "country_id": "238", "sortname": "VN", "country_name": "Vietnam" },
{
"country_id": "239",
"sortname": "VG",
"country_name": "Virgin Islands (British)"
},
{
"country_id": "240",
"sortname": "VI",
"country_name": "Virgin Islands (US)"
},
{
"country_id": "241",
"sortname": "WF",
"country_name": "Wallis And Futuna Islands"
},
{ "country_id": "242", "sortname": "EH", "country_name": "Western Sahara" },
{ "country_id": "243", "sortname": "YE", "country_name": "Yemen" },
{ "country_id": "244", "sortname": "YU", "country_name": "Yugoslavia" },
{ "country_id": "245", "sortname": "ZM", "country_name": "Zambia" },
{ "country_id": "246", "sortname": "ZW", "country_name": "Zimbabwe" }
]
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,44 @@
-- CreateTable
CREATE TABLE "Country" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"code" TEXT NOT NULL,
CONSTRAINT "Country_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "State" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"countryId" TEXT NOT NULL,
CONSTRAINT "State_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "City" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
CONSTRAINT "City_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Country_name_key" ON "Country"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Country_code_key" ON "Country"("code");
-- CreateIndex
CREATE UNIQUE INDEX "State_name_countryId_key" ON "State"("name", "countryId");
-- CreateIndex
CREATE UNIQUE INDEX "City_name_stateId_key" ON "City"("name", "stateId");
-- AddForeignKey
ALTER TABLE "State" ADD CONSTRAINT "State_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "City" ADD CONSTRAINT "City_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "State"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

@ -106,6 +106,32 @@ model Company {
OffersOffer OffersOffer[]
}
model Country {
id String @id
name String @unique
code String @unique
states State[]
}
model State {
id String @id
name String
countryId String
cities City[]
country Country @relation(fields: [countryId], references: [id])
@@unique([name, countryId])
}
model City {
id String @id
name String
stateId String
state State @relation(fields: [stateId], references: [id])
@@unique([name, stateId])
}
// Start of Resumes project models.
// Add Resumes project models here, prefix all models with "Resumes",
// use camelCase for field names, and try to name them consistently

@ -0,0 +1,207 @@
import { PrismaClient } from '@prisma/client';
import { JobTitleLabels } from '../src/components/shared/JobTitles';
const prisma = new PrismaClient();
type QuestionCreateData = Parameters<
typeof prisma.questionsQuestion.create
>[0]['data'];
function selectRandomRole() {
const roles = Object.keys(JobTitleLabels);
const randomIndex = Math.floor(Math.random() * roles.length);
return roles[randomIndex];
}
function generateRandomDate() {
// Return a date between 2020 and 2022.
const start = new Date(2020, 0, 1);
const end = new Date(2022, 0, 1);
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime()),
);
}
function generateRandomCodingAnswer() {
return CODING_ANSWER_CONTENT[
Math.floor(Math.random() * CODING_ANSWER_CONTENT.length)
];
}
function generateRandomBehavioralAnswer() {
return BEHAVIORAL_ANSWER_CONTENT[
Math.floor(Math.random() * BEHAVIORAL_ANSWER_CONTENT.length)
];
}
const CODING_QUESTION_CONTENT = [
'Given a string, find the length of the longest substring without repeating characters.',
'Given an array of integers, return indices of the two numbers such that they add up to a specific target.',
'Given a contiguous sequence of numbers in which each number repeats thrice, there is exactly one missing number. Find the missing number.',
'Find the contiguous subarray within an array (containing at least one number) which has the largest product.',
'Find a contiguous subarray which has the largest sum.',
];
const BEHAVIORAL_QUESTION_CONTENT = [
'Tell me about a time you had to work with a difficult person.',
'Rate your communication skills on a scale of 1 to 10.',
'Are you a team player?',
'What is your greatest weakness?',
'What is your greatest strength?',
'What is your biggest accomplishment?',
'What is your biggest failure?',
'Be honest, how would your friends describe you?',
'How do you handle stress?',
'Lets say you have a deadline to meet. How do you prioritize your work?',
];
const CODING_ANSWER_CONTENT = [
'This question is easy. Just use a hash map.',
'This question is hard. I have no idea how to solve it.',
'This question is medium. I can solve it in 30 minutes.',
'Can be done with a simple for loop.',
'Simple recursion can solve this.',
'Please explain the question again.',
'Question is not clear.',
'Brute force solution is the best.',
];
const BEHAVIORAL_ANSWER_CONTENT = [
'This is a very common question. I have a lot of experience with this.',
"I don't think this is a good question to ask. However, I can answer it.",
'Most companies ask this question. I think you should ask something else.',
'I try to take a step back and assess the situation. I figure out what is the most important thing to do and what can wait. I also try to delegate or ask for help when needed.',
'I try to have a discussion with my manager or the person who I feel is not valuing my work. I try to explain how I feel and what I would like to see change.',
'I try to have a discussion with the coworker. I try to understand their perspective and see if there is a way to resolve the issue.',
];
const CODING_QUESTIONS: Array<QuestionCreateData> = CODING_QUESTION_CONTENT.map(
(content) => ({
content,
questionType: 'CODING',
userId: null,
encounters: {
create: {
location: 'Singapore',
role: selectRandomRole(),
seenAt: generateRandomDate(),
},
},
}),
);
const BEHAVIORAL_QUESTIONS: Array<QuestionCreateData> =
BEHAVIORAL_QUESTION_CONTENT.map((content) => ({
content,
questionType: 'BEHAVIORAL',
userId: null,
encounters: {
create: {
location: 'Singapore',
role: selectRandomRole(),
seenAt: generateRandomDate(),
},
},
}));
const QUESTIONS: Array<QuestionCreateData> = [
...CODING_QUESTIONS,
...BEHAVIORAL_QUESTIONS,
];
async function main() {
console.log('Performing preliminary checks...');
const firstCompany = await prisma.company.findFirst();
if (!firstCompany) {
throw new Error(
'No company found. Please seed db with some companies first.',
);
}
// Generate random answers to the questions
const users = await prisma.user.findMany();
if (users.length === 0) {
throw new Error('No users found. Please seed db with some users first.');
}
console.log('Seeding started...');
console.log('Creating coding and behavioral questions...');
await Promise.all([
QUESTIONS.map(async (question) => {
await prisma.questionsQuestion.create({
data: {
...question,
encounters: {
create: {
...question.encounters!.create,
companyId: firstCompany.id,
} as any,
},
},
});
}),
]);
console.log('Creating answers to coding questions...');
const codingQuestions = await prisma.questionsQuestion.findMany({
where: {
questionType: 'CODING',
},
});
await Promise.all(
codingQuestions.map(async (question) => {
const answers = Array.from(
{ length: Math.floor(Math.random() * 5) },
() => ({
content: generateRandomCodingAnswer(),
userId: users[Math.floor(Math.random() * users.length)].id,
questionId: question.id,
}),
);
await prisma.questionsAnswer.createMany({
data: answers,
});
}),
);
console.log('Creating answers to behavioral questions...');
const behavioralQuestions = await prisma.questionsQuestion.findMany({
where: {
questionType: 'BEHAVIORAL',
},
});
await Promise.all(
behavioralQuestions.map(async (question) => {
const answers = Array.from(
{ length: Math.floor(Math.random() * 5) },
() => ({
content: generateRandomBehavioralAnswer(),
userId: users[Math.floor(Math.random() * users.length)].id,
questionId: question.id,
}),
);
await prisma.questionsAnswer.createMany({
data: answers,
});
}),
);
console.log('Seeding completed.');
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

@ -1,5 +1,9 @@
const { PrismaClient } = require('@prisma/client');
const cities = require('./data/cities.json');
const countries = require('./data/countries.json');
const states = require('./data/states.json');
const prisma = new PrismaClient();
const COMPANIES = [
@ -33,19 +37,58 @@ const COMPANIES = [
description: `Microsoft Corporation is an American multinational technology corporation which produces computer software, consumer electronics, personal computers, and related services headquartered at the Microsoft Redmond campus located in Redmond, Washington, United States.`,
logoUrl: 'https://logo.clearbit.com/microsoft.com',
},
{
name: 'Netflix',
slug: 'netflix',
description: null,
logoUrl: 'https://logo.clearbit.com/netflix.com',
},
];
async function main() {
console.log('Seeding started...');
await Promise.all([
COMPANIES.map(async (company) => {
await prisma.company.upsert({
where: { slug: company.slug },
update: company,
create: company,
console.info('Seeding companies');
await prisma.company.createMany({
data: COMPANIES.map((company) => ({
name: company.name,
slug: company.slug,
description: company.description,
logoUrl: company.logoUrl,
})),
skipDuplicates: true,
});
console.info('Seeding countries');
await prisma.country.createMany({
data: countries.data.map((country) => ({
id: country.country_id,
code: country.sortname,
name: country.country_name,
})),
skipDuplicates: true,
});
}),
]);
console.info('Seeding states');
await prisma.state.createMany({
data: states.data.map((state) => ({
id: state.state_id,
countryId: state.country_id,
name: state.state_name,
})),
skipDuplicates: true,
});
console.info('Seeding cities');
await prisma.city.createMany({
data: cities.data.map((city) => ({
id: city.city_id,
stateId: city.state_id,
name: city.city_name,
})),
skipDuplicates: true,
});
console.log('Seeding completed.');
}

@ -108,6 +108,7 @@ export default function AppShell({ children }: Props) {
const currentProductNavigation: Readonly<{
googleAnalyticsMeasurementID: string;
logo?: React.ReactNode;
navigation: ProductNavigationItems;
showGlobalNav: boolean;
title: string;
@ -173,6 +174,7 @@ export default function AppShell({ children }: Props) {
<MobileNavigation
globalNavigationItems={GlobalNavigation}
isShown={mobileMenuOpen}
logo={currentProductNavigation.logo}
productNavigationItems={currentProductNavigation.navigation}
productTitle={currentProductNavigation.title}
setIsShown={setMobileMenuOpen}
@ -192,6 +194,7 @@ export default function AppShell({ children }: Props) {
<div className="flex flex-1 items-center">
<ProductNavigation
items={currentProductNavigation.navigation}
logo={currentProductNavigation.logo}
title={currentProductNavigation.title}
titleHref={currentProductNavigation.titleHref}
/>

@ -1,5 +1,5 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
import { createContext, useContext, useEffect } from 'react';
type Context = Readonly<{
@ -78,25 +78,26 @@ export default function GoogleAnalytics({ children, measurementID }: Props) {
return (
<GoogleAnalyticsContext.Provider value={{ event }}>
{children}
<Head>
{/* TODO(yangshun): Change back to next/script in future. */}
{/* Global Site Tag (gtag.js) - Google Analytics */}
<Script
<script
async={true}
src={`https://www.googletagmanager.com/gtag/js?id=${measurementID}`}
strategy="afterInteractive"
/>
<Script
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
window.gtag = function(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${measurementID}', {
page_path: window.location.pathname,
});
`,
}}
id="gtag-init"
strategy="afterInteractive"
/>
</Head>
</GoogleAnalyticsContext.Provider>
);
}

@ -11,6 +11,7 @@ import type { ProductNavigationItems } from './ProductNavigation';
type Props = Readonly<{
globalNavigationItems: GlobalNavigationItems;
isShown?: boolean;
logo?: React.ReactNode;
productNavigationItems: ProductNavigationItems;
productTitle: string;
setIsShown: (isShown: boolean) => void;
@ -19,6 +20,7 @@ type Props = Readonly<{
export default function MobileNavigation({
globalNavigationItems,
isShown,
logo,
productNavigationItems,
productTitle,
setIsShown,
@ -69,11 +71,13 @@ export default function MobileNavigation({
</Transition.Child>
<div className="flex flex-shrink-0 items-center px-4">
<Link href="/">
{logo ?? (
<img
alt="Tech Interview Handbook"
className="h-8 w-auto"
src="/logo.svg"
/>
)}
</Link>
</div>
<div className="mt-5 h-0 flex-1 overflow-y-auto px-2">

@ -17,11 +17,17 @@ export type ProductNavigationItems = ReadonlyArray<NavigationItem>;
type Props = Readonly<{
items: ProductNavigationItems;
logo?: React.ReactNode;
title: string;
titleHref: string;
}>;
export default function ProductNavigation({ items, title, titleHref }: Props) {
export default function ProductNavigation({
items,
logo,
title,
titleHref,
}: Props) {
const router = useRouter();
return (
@ -29,13 +35,14 @@ export default function ProductNavigation({ items, title, titleHref }: Props) {
<Link
className="hover:text-primary-700 flex items-center gap-2 text-base font-medium"
href={titleHref}>
{titleHref !== '/' && (
{titleHref !== '/' &&
(logo ?? (
<img
alt="Tech Interview Handbook"
className="h-8 w-auto"
src="/logo.svg"
/>
)}
))}
{title}
</Link>
<div className="hidden h-full items-center space-x-8 md:flex">

@ -1,3 +1,5 @@
import { CurrencyDollarIcon } from '@heroicons/react/24/outline';
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
const navigation: ProductNavigationItems = [
@ -7,7 +9,13 @@ const navigation: ProductNavigationItems = [
const config = {
// TODO: Change this to your own GA4 measurement ID.
googleAnalyticsMeasurementID: 'G-DBLZDQ2ZZN',
googleAnalyticsMeasurementID: 'G-34XRGLEVCF',
logo: (
<CurrencyDollarIcon
aria-label="Tech Interview Handbook Offers"
className="h-8 w-8"
/>
),
navigation,
showGlobalNav: false,
title: 'Tech Offers Repo',

@ -97,6 +97,7 @@ function FullTimeJobFields() {
<div className="mb-5 grid grid-cols-2 space-x-3">
<div>
<JobTitlesTypeahead
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`background.experiences.0.title`, value)
}
@ -104,6 +105,7 @@ function FullTimeJobFields() {
</div>
<div>
<CompaniesTypeahead
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`background.experiences.0.companyId`, value)
}
@ -178,6 +180,7 @@ function InternshipJobFields() {
<div className="mb-5 grid grid-cols-2 space-x-3">
<div>
<JobTitlesTypeahead
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`background.experiences.0.title`, value)
}
@ -185,6 +188,7 @@ function InternshipJobFields() {
</div>
<div>
<CompaniesTypeahead
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`background.experiences.0.companyId`, value)
}

@ -70,6 +70,7 @@ function FullTimeOfferDetailsForm({
<div>
<JobTitlesTypeahead
required={true}
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`offers.${index}.offersFullTime.title`, value)
}
@ -89,6 +90,7 @@ function FullTimeOfferDetailsForm({
<div>
<CompaniesTypeahead
required={true}
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`offers.${index}.companyId`, value)
}
@ -277,6 +279,7 @@ function InternshipOfferDetailsForm({
<div>
<JobTitlesTypeahead
required={true}
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`offers.${index}.offersIntern.title`, value)
}
@ -287,6 +290,7 @@ function InternshipOfferDetailsForm({
<div>
<CompaniesTypeahead
required={true}
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) =>
setValue(`offers.${index}.companyId`, value)
}

@ -30,7 +30,7 @@ export default function ContributeQuestionCard({
return (
<div className="w-full">
<button
className="w-full flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
type="button"
onClick={handleOpenContribute}>
<TextInput
@ -72,12 +72,12 @@ export default function ContributeQuestionCard({
Contribute
</h1>
</div>
</button>
<ContributeQuestionDialog
show={showDraftDialog}
onCancel={handleDraftDialogCancel}
onSubmit={onSubmit}
/>
</button>
</div>
);
}

@ -97,6 +97,7 @@ export default function LandingComponent({
isLabelHidden={true}
value={company}
onSelect={(value) => {
// @ts-ignore TODO(questions): handle potentially null value.
handleChangeCompany(value);
}}
/>
@ -105,6 +106,7 @@ export default function LandingComponent({
isLabelHidden={true}
value={location}
onSelect={(value) => {
// @ts-ignore TODO(questions): handle potentially null value.
handleChangeLocation(value);
}}
/>

@ -81,6 +81,7 @@ export default function ContributeQuestionForm({
<LocationTypeahead
required={true}
onSelect={(option) => {
// @ts-ignore TODO(questions): handle potentially null value.
field.onChange(option.value);
}}
{...field}
@ -119,6 +120,7 @@ export default function ContributeQuestionForm({
render={({ field }) => (
<CompanyTypeahead
required={true}
// @ts-ignore TODO(questions): handle potentially null value.
onSelect={({ id }) => {
field.onChange(id);
}}
@ -134,6 +136,7 @@ export default function ContributeQuestionForm({
<RoleTypeahead
required={true}
onSelect={(option) => {
// @ts-ignore TODO(questions): handle potentially null value.
field.onChange(option.value);
}}
{...field}

@ -43,6 +43,7 @@ export default function CreateQuestionEncounterForm({
isLabelHidden={true}
placeholder="Other company"
suggestedCount={3}
// @ts-ignore TODO(questions): handle potentially null value.
onSelect={({ value: company }) => {
setSelectedCompany(company);
}}
@ -59,6 +60,7 @@ export default function CreateQuestionEncounterForm({
isLabelHidden={true}
placeholder="Other location"
suggestedCount={3}
// @ts-ignore TODO(questions): handle potentially null value.
onSelect={({ value: location }) => {
setSelectedLocation(location);
}}
@ -75,6 +77,7 @@ export default function CreateQuestionEncounterForm({
isLabelHidden={true}
placeholder="Other role"
suggestedCount={3}
// @ts-ignore TODO(questions): handle potentially null value.
onSelect={({ value: role }) => {
setSelectedRole(role);
}}

@ -1,13 +1,21 @@
import { ROLES } from '~/utils/questions/constants';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
import ExpandedTypeahead from './ExpandedTypeahead';
import type { FilterChoices } from '../filter/FilterSection';
export type RoleTypeaheadProps = Omit<
ExpandedTypeaheadProps,
'label' | 'onQueryChange' | 'options'
>;
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
([slug, label]) => ({
id: slug,
label,
value: slug,
}),
);
export default function RoleTypeahead(props: RoleTypeaheadProps) {
return (
<ExpandedTypeahead

@ -15,7 +15,9 @@ export default function ResumeFilterPill({
<button
className={clsx(
'border-primary-500 focus:bg-primary-500 rounded-xl border border-transparent px-2 py-1 text-xs font-medium focus:text-white',
isSelected ? 'bg-primary-500 text-white' : 'text-primary-500 bg-white',
isSelected
? 'bg-primary-500 text-white'
: 'text-primary-500 hover:text-primary-700 bg-white hover:bg-slate-100',
)}
type="button"
onClick={onClick}>

@ -3,6 +3,8 @@ import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { Button, Dialog, TextArea } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { trpc } from '~/utils/trpc';
type ResumeCommentsFormProps = Readonly<{
@ -25,6 +27,8 @@ export default function ResumeCommentsForm({
setShowCommentsForm,
}: ResumeCommentsFormProps) {
const [showDialog, setShowDialog] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const {
register,
handleSubmit,
@ -50,6 +54,11 @@ export default function ResumeCommentsForm({
trpcContext.invalidateQueries(['resumes.resume.findAll']);
trpcContext.invalidateQueries(['resumes.resume.user.findUserStarred']);
trpcContext.invalidateQueries(['resumes.resume.user.findUserCreated']);
gaEvent({
action: 'resumes.comment_submit',
category: 'engagement',
label: 'Submit comment',
});
},
},
);

@ -3,6 +3,8 @@ import { useForm } from 'react-hook-form';
import type { ResumesSection } from '@prisma/client';
import { Button, TextArea } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { trpc } from '~/utils/trpc';
type ResumeCommentEditFormProps = {
@ -33,6 +35,7 @@ export default function ResumeCommentReplyForm({
description: '',
},
});
const { event: gaEvent } = useGoogleAnalytics();
const trpcContext = trpc.useContext();
const commentReplyMutation = trpc.useMutation('resumes.comments.user.reply', {
@ -58,6 +61,12 @@ export default function ResumeCommentReplyForm({
{
onSuccess: () => {
setIsReplyingComment(false);
gaEvent({
action: 'resumes.comment_reply',
category: 'engagement',
label: 'Reply comment',
});
},
},
);

@ -7,6 +7,8 @@ import {
} from '@heroicons/react/20/solid';
import { Vote } from '@prisma/client';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { trpc } from '~/utils/trpc';
type ResumeCommentVoteButtonsProps = {
@ -20,6 +22,7 @@ export default function ResumeCommentVoteButtons({
}: ResumeCommentVoteButtonsProps) {
const [upvoteAnimation, setUpvoteAnimation] = useState(false);
const [downvoteAnimation, setDownvoteAnimation] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const trpcContext = trpc.useContext();
const router = useRouter();
@ -35,6 +38,11 @@ export default function ResumeCommentVoteButtons({
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
gaEvent({
action: 'resumes.comment_vote',
category: 'engagement',
label: 'Upvote/Downvote comment',
});
},
},
);
@ -44,6 +52,11 @@ export default function ResumeCommentVoteButtons({
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
gaEvent({
action: 'resumes.comment_unvote',
category: 'engagement',
label: 'Unvote comment',
});
},
},
);

@ -0,0 +1,59 @@
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { trpc } from '~/utils/trpc';
type Props = Readonly<{
disabled?: boolean;
errorMessage?: string;
isLabelHidden?: boolean;
label?: string;
onSelect: (option: TypeaheadOption | null) => void;
placeholder?: string;
required?: boolean;
value?: TypeaheadOption | null;
}>;
export default function CitiesTypeahead({
disabled,
label = 'City',
onSelect,
isLabelHidden,
placeholder,
required,
value,
}: Props) {
const [query, setQuery] = useState('');
const cities = trpc.useQuery([
'locations.cities.list',
{
name: query,
},
]);
const { data } = cities;
return (
<Typeahead
disabled={disabled}
isLabelHidden={isLabelHidden}
label={label}
noResultsMessage="No cities found"
nullable={true}
options={
data?.map(({ id, name, state }) => ({
id,
label: `${name}, ${state?.name}, ${state?.country?.name}`,
value: id,
})) ?? []
}
placeholder={placeholder}
required={required}
textSize="inherit"
value={value}
onQueryChange={setQuery}
onSelect={onSelect}
/>
);
}

@ -8,17 +8,19 @@ type Props = Readonly<{
disabled?: boolean;
errorMessage?: string;
isLabelHidden?: boolean;
onSelect: (option: TypeaheadOption) => void;
placeHolder?: string;
onSelect: (option: TypeaheadOption | null) => void;
placeholder?: string;
required?: boolean;
value?: TypeaheadOption | null;
}>;
export default function CompaniesTypeahead({
disabled,
onSelect,
isLabelHidden,
placeHolder,
placeholder,
required,
value,
}: Props) {
const [query, setQuery] = useState('');
const companies = trpc.useQuery([
@ -44,9 +46,10 @@ export default function CompaniesTypeahead({
value: id,
})) ?? []
}
placeholder={placeHolder}
placeholder={placeholder}
required={required}
textSize="inherit"
value={value}
onQueryChange={setQuery}
onSelect={onSelect}
/>

@ -0,0 +1,57 @@
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { trpc } from '~/utils/trpc';
type Props = Readonly<{
disabled?: boolean;
errorMessage?: string;
isLabelHidden?: boolean;
onSelect: (option: TypeaheadOption | null) => void;
placeholder?: string;
required?: boolean;
value?: TypeaheadOption | null;
}>;
export default function CountriesTypeahead({
disabled,
onSelect,
isLabelHidden,
placeholder,
required,
value,
}: Props) {
const [query, setQuery] = useState('');
const countries = trpc.useQuery([
'locations.countries.list',
{
name: query,
},
]);
const { data } = countries;
return (
<Typeahead
disabled={disabled}
isLabelHidden={isLabelHidden}
label="Country"
noResultsMessage="No countries found"
nullable={true}
options={
data?.map(({ id, name }) => ({
id,
label: name,
value: id,
})) ?? []
}
placeholder={placeholder}
required={required}
textSize="inherit"
value={value}
onQueryChange={setQuery}
onSelect={onSelect}
/>
);
}

@ -7,17 +7,19 @@ import { JobTitleLabels } from './JobTitles';
type Props = Readonly<{
disabled?: boolean;
isLabelHidden?: boolean;
onSelect: (option: TypeaheadOption) => void;
placeHolder?: string;
onSelect: (option: TypeaheadOption | null) => void;
placeholder?: string;
required?: boolean;
value?: TypeaheadOption | null;
}>;
export default function JobTitlesTypeahead({
disabled,
onSelect,
isLabelHidden,
placeHolder,
placeholder,
required,
value,
}: Props) {
const [query, setQuery] = useState('');
const options = Object.entries(JobTitleLabels)
@ -39,9 +41,10 @@ export default function JobTitlesTypeahead({
noResultsMessage="No available job titles."
nullable={true}
options={options}
placeholder={placeHolder}
placeholder={placeholder}
required={required}
textSize="inherit"
value={value}
onQueryChange={setQuery}
onSelect={onSelect}
/>

@ -36,13 +36,15 @@ export default function OffersHomePage() {
<div className="flex items-center space-x-4">
<JobTitlesTypeahead
isLabelHidden={true}
placeHolder="Software Engineer"
placeholder="Software Engineer"
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) => setjobTitleFilter(value)}
/>
<span>in</span>
<CompaniesTypeahead
isLabelHidden={true}
placeHolder="All Companies"
placeholder="All Companies"
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) => setCompanyFilter(value)}
/>
</div>

@ -205,6 +205,22 @@ function Test() {
},
);
trpc.useQuery(
[
`offers.profile.isValidToken`,
{
profileId: 'cl9scdzuh0000tt727ipone1k',
token:
'aa628d0db3ad7a5f84895537d4cca38edd0a9b8b96d869cddeb967fccf068c08',
},
],
{
onError(err) {
setError(err.shape?.message || '');
},
},
);
const replies = trpc.useQuery(
['offers.comments.getComments', { profileId }],
{

@ -1,6 +1,6 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui';
@ -13,6 +13,7 @@ import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useFormRegister } from '~/utils/questions/useFormRegister';
import { trpc } from '~/utils/trpc';
@ -69,6 +70,14 @@ export default function QuestionPage() {
{ questionId: questionId as string },
]);
const relabeledAggregatedEncounters = useMemo(() => {
if (!aggregatedEncounters) {
return aggregatedEncounters;
}
return relabelQuestionAggregates(aggregatedEncounters);
}, [aggregatedEncounters]);
const utils = trpc.useContext();
const { data: commentData } = trpc.useInfiniteQuery(
@ -175,11 +184,11 @@ export default function QuestionPage() {
<div className="flex max-w-7xl flex-1 flex-col gap-2">
<FullQuestionCard
{...question}
companies={aggregatedEncounters?.companyCounts ?? {}}
locations={aggregatedEncounters?.locationCounts ?? {}}
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
locations={relabeledAggregatedEncounters?.locationCounts ?? {}}
questionId={question.id}
receivedCount={undefined}
roles={aggregatedEncounters?.roleCounts ?? {}}
roles={relabeledAggregatedEncounters?.roleCounts ?? {}}
timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',

@ -14,11 +14,13 @@ import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead';
import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { QuestionAge } from '~/utils/questions/constants';
import { APP_TITLE } from '~/utils/questions/constants';
import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import {
useSearchParam,
useSearchParamSingle,
@ -174,7 +176,7 @@ export default function QuestionsBrowsePage() {
return undefined;
}
return questionsQueryData.pages.reduce(
(acc, page) => acc + page.data.length,
(acc, page) => acc + (page.data.length as number),
0,
);
}, [questionsQueryData]);
@ -273,7 +275,7 @@ export default function QuestionsBrowsePage() {
return selectedRoles.map((role) => ({
checked: true,
id: role,
label: role,
label: JobTitleLabels[role as keyof typeof JobTitleLabels],
value: role,
}));
}, [selectedRoles]);
@ -323,6 +325,7 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true}
placeholder="Search companies"
onSelect={(option) => {
// @ts-ignore TODO(questions): handle potentially null value.
onOptionChange({
...option,
checked: true,
@ -355,6 +358,7 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true}
placeholder="Search roles"
onSelect={(option) => {
// @ts-ignore TODO(questions): handle potentially null value.
onOptionChange({
...option,
checked: true,
@ -367,7 +371,7 @@ export default function QuestionsBrowsePage() {
setSelectedRoles([...selectedRoles, option.value]);
} else {
setSelectedRoles(
selectedCompanies.filter((role) => role !== option.value),
selectedRoles.filter((role) => role !== option.value),
);
}
}}
@ -412,6 +416,7 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true}
placeholder="Search locations"
onSelect={(option) => {
// @ts-ignore TODO(offers): fix potentially empty value.
onOptionChange({
...option,
checked: true,
@ -440,8 +445,7 @@ export default function QuestionsBrowsePage() {
<main className="flex flex-1 flex-col items-stretch">
<div className="flex h-full flex-1">
<section className="flex min-h-0 flex-1 flex-col items-center overflow-auto">
<div className="flex min-h-0 max-w-3xl flex-1 p-4">
<div className="flex flex-1 flex-col items-stretch justify-start gap-8">
<div className="m-4 flex max-w-3xl flex-1 flex-col items-stretch justify-start gap-8">
<ContributeQuestionCard
onSubmit={(data) => {
createQuestion({
@ -454,6 +458,7 @@ export default function QuestionsBrowsePage() {
});
}}
/>
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
<QuestionSearchBar
sortOrderValue={sortOrder}
sortTypeValue={sortType}
@ -463,28 +468,29 @@ export default function QuestionsBrowsePage() {
onSortOrderChange={setSortOrder}
onSortTypeChange={setSortType}
/>
</div>
<div className="flex flex-col gap-2 pb-4">
{(questionsQueryData?.pages ?? []).flatMap(
({ data: questions }) =>
questions.map((question) => (
questions.map((question) => {
const { companyCounts, locationCounts, roleCounts } =
relabelQuestionAggregates(
question.aggregatedQuestionEncounters,
);
return (
<QuestionOverviewCard
key={question.id}
answerCount={question.numAnswers}
companies={
question.aggregatedQuestionEncounters.companyCounts
}
companies={companyCounts}
content={question.content}
href={`/questions/${question.id}/${createSlug(
question.content,
)}`}
locations={
question.aggregatedQuestionEncounters.locationCounts
}
locations={locationCounts}
questionId={question.id}
receivedCount={question.receivedCount}
roles={
question.aggregatedQuestionEncounters.roleCounts
}
roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString(
undefined,
{
@ -495,7 +501,8 @@ export default function QuestionsBrowsePage() {
type={question.type}
upvoteCount={question.numVotes}
/>
)),
);
}),
)}
<Button
disabled={!hasNextPage || isFetchingNextPage}
@ -515,7 +522,6 @@ export default function QuestionsBrowsePage() {
)}
</div>
</div>
</div>
</section>
<aside className="hidden w-[300px] overflow-y-auto border-l bg-white py-4 lg:block">
<h2 className="px-4 text-xl font-semibold">Filter by</h2>

@ -15,6 +15,7 @@ import DeleteListDialog from '~/components/questions/DeleteListDialog';
import { Button } from '~/../../../packages/ui/dist';
import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { trpc } from '~/utils/trpc';
export default function ListPage() {
@ -172,24 +173,24 @@ export default function ListPage() {
{lists?.[selectedListIndex] && (
<div className="flex flex-col gap-4 pb-4">
{lists[selectedListIndex].questionEntries.map(
({ question, id: entryId }) => (
({ question, id: entryId }) => {
const { companyCounts, locationCounts, roleCounts } =
relabelQuestionAggregates(
question.aggregatedQuestionEncounters,
);
return (
<QuestionListCard
key={question.id}
companies={
question.aggregatedQuestionEncounters.companyCounts
}
companies={companyCounts}
content={question.content}
href={`/questions/${question.id}/${createSlug(
question.content,
)}`}
locations={
question.aggregatedQuestionEncounters.locationCounts
}
locations={locationCounts}
questionId={question.id}
receivedCount={question.receivedCount}
roles={
question.aggregatedQuestionEncounters.roleCounts
}
roles={roleCounts}
timestamp={question.seenAt.toLocaleDateString(
undefined,
{
@ -202,7 +203,8 @@ export default function ListPage() {
deleteQuestionEntry({ id: entryId });
}}
/>
),
);
},
)}
{lists[selectedListIndex].questionEntries?.length === 0 && (
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">

@ -142,6 +142,11 @@ export default function ResumeReviewPage() {
pathname: '/resumes',
query: {
currentPage: JSON.stringify(1),
isFiltersOpen: JSON.stringify({
experience: experienceLabel !== undefined,
location: locationLabel !== undefined,
role: roleLabel !== undefined,
}),
searchValue: JSON.stringify(''),
shortcutSelected: JSON.stringify('all'),
sortOrder: JSON.stringify('latest'),

@ -127,6 +127,13 @@ export default function ResumeHomePage() {
'userFilters',
INITIAL_FILTER_STATE,
);
const [isFiltersOpen, setIsFiltersOpen, isFiltersOpenInit] = useSearchParams<
Record<FilterId, boolean>
>('isFiltersOpen', {
experience: false,
location: false,
role: false,
});
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const skip = (currentPage - 1) * PAGE_LIMIT;
@ -137,7 +144,8 @@ export default function ResumeHomePage() {
isSearchValueInit &&
isShortcutInit &&
isCurrentPageInit &&
isUserFiltersInit
isUserFiltersInit &&
isFiltersOpenInit
);
}, [
isTabsValueInit,
@ -146,6 +154,7 @@ export default function ResumeHomePage() {
isShortcutInit,
isCurrentPageInit,
isUserFiltersInit,
isFiltersOpenInit,
]);
useEffect(() => {
@ -164,6 +173,7 @@ export default function ResumeHomePage() {
pathname: router.pathname,
query: {
currentPage: JSON.stringify(currentPage),
isFiltersOpen: JSON.stringify(isFiltersOpen),
searchValue: JSON.stringify(searchValue),
shortcutSelected: JSON.stringify(shortcutSelected),
sortOrder: JSON.stringify(sortOrder),
@ -180,6 +190,7 @@ export default function ResumeHomePage() {
currentPage,
router.pathname,
isSearchOptionsInit,
isFiltersOpen,
]);
const allResumesQuery = trpc.useQuery(
@ -399,11 +410,19 @@ export default function ResumeHomePage() {
<Disclosure
key={filter.id}
as="div"
className="border-t border-slate-200 px-4 pt-6 pb-4">
className="border-t border-slate-200 px-4 pt-6 pb-4"
defaultOpen={isFiltersOpen[filter.id]}>
{({ open }) => (
<>
<h3 className="-mx-2 -my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white px-2 py-3 text-slate-400 hover:text-slate-500">
<Disclosure.Button
className="flex w-full items-center justify-between bg-white px-2 py-3 text-slate-400 hover:text-slate-500"
onClick={() =>
setIsFiltersOpen({
...isFiltersOpen,
[filter.id]: !isFiltersOpen[filter.id],
})
}>
<span className="font-medium text-slate-900">
{filter.label}
</span>
@ -427,12 +446,9 @@ export default function ResumeHomePage() {
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 [&>div>div:nth-child(2)>label]:font-normal">
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 flex items-center px-1 text-sm [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={`${option.label} (${getFilterCount(
filter.label,
option.label,
)})`}
label={option.label}
value={userFilters[filter.id].includes(
option.value,
)}
@ -444,11 +460,19 @@ export default function ResumeHomePage() {
)
}
/>
<span className="ml-1 text-slate-500">
(
{getFilterCount(
filter.label,
option.label,
)}
)
</span>
</div>
))}
</div>
<p
className="cursor-pointer text-sm text-slate-500 underline"
className="inline-block cursor-pointer text-sm text-slate-500 underline hover:text-slate-700"
onClick={() => onClearFilterClick(filter.id)}>
Clear
</p>
@ -491,15 +515,24 @@ export default function ResumeHomePage() {
<h3 className="text-md font-medium tracking-tight text-slate-900">
Explore these filters
</h3>
{filters.map((filter) => (
{isFiltersOpenInit &&
filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-b border-slate-200 pt-6 pb-4">
className="border-b border-slate-200 pt-6 pb-4"
defaultOpen={isFiltersOpen[filter.id]}>
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500">
<Disclosure.Button
className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500"
onClick={() =>
setIsFiltersOpen({
...isFiltersOpen,
[filter.id]: !isFiltersOpen[filter.id],
})
}>
<span className="font-medium text-slate-900">
{filter.label}
</span>
@ -527,12 +560,9 @@ export default function ResumeHomePage() {
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 px-1 [&>div>div:nth-child(2)>label]:font-normal">
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 flex items-center px-1 text-sm [&>div>div:nth-child(2)>label]:font-normal">
<CheckboxInput
label={`${option.label} (${getFilterCount(
filter.label,
option.label,
)})`}
label={option.label}
value={userFilters[filter.id].includes(
option.value,
)}
@ -544,11 +574,16 @@ export default function ResumeHomePage() {
)
}
/>
<span className="ml-1 text-slate-500">
(
{getFilterCount(filter.label, option.label)}
)
</span>
</div>
))}
</CheckboxList>
<p
className="cursor-pointer text-sm text-slate-500 underline"
className="inline-block cursor-pointer text-sm text-slate-500 underline hover:text-slate-700"
onClick={() => onClearFilterClick(filter.id)}>
Clear
</p>

@ -171,9 +171,6 @@ export default function SubmitResumeForm({
onSuccess() {
if (isNewForm) {
trpcContext.invalidateQueries('resumes.resume.findAll');
trpcContext.invalidateQueries(
'resumes.resume.getTotalFilterCounts',
);
router.push('/resumes');
gaEvent({
action: 'resumes.submit_button_click',

@ -4,7 +4,9 @@ import { Button } from '@tih/ui';
import { useToast } from '@tih/ui';
import { HorizontalDivider } from '@tih/ui';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import CountriesTypeahead from '~/components/shared/CountriesTypeahead';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
import type {
Month,
@ -15,6 +17,11 @@ import MonthYearPicker from '~/components/shared/MonthYearPicker';
export default function HomePage() {
const [selectedCompany, setSelectedCompany] =
useState<TypeaheadOption | null>(null);
const [selectedCountry, setSelectedCountry] =
useState<TypeaheadOption | null>(null);
const [selectedCity, setSelectedCity] = useState<TypeaheadOption | null>(
null,
);
const [selectedJobTitle, setSelectedJobTitle] =
useState<TypeaheadOption | null>(null);
@ -26,12 +33,12 @@ export default function HomePage() {
const { showToast } = useToast();
return (
<main className="flex-1 overflow-y-auto">
<div className="flex h-full items-center justify-center">
<div className="space-y-4">
<main className="mx-auto max-w-5xl flex-1 overflow-y-auto py-24">
<h1 className="text-primary-600 text-center text-4xl font-bold">
Test Page
</h1>
<div className="mt-8 grid grid-cols-2 gap-8">
<div className="space-y-4">
<CompaniesTypeahead
onSelect={(option) => setSelectedCompany(option)}
/>
@ -42,6 +49,15 @@ export default function HomePage() {
/>
<pre>{JSON.stringify(selectedJobTitle, null, 2)}</pre>
<HorizontalDivider />
<CountriesTypeahead
onSelect={(option) => setSelectedCountry(option)}
/>
<pre>{JSON.stringify(selectedCountry, null, 2)}</pre>
<HorizontalDivider />
<CitiesTypeahead onSelect={(option) => setSelectedCity(option)} />
<pre>{JSON.stringify(selectedCity, null, 2)}</pre>
</div>
<div className="space-y-4">
<MonthYearPicker
errorMessage={
monthYear.month == null || monthYear.year == null

@ -2,6 +2,7 @@ import superjson from 'superjson';
import { companiesRouter } from './companies-router';
import { createRouter } from './context';
import { locationsRouter } from './locations-router';
import { offersRouter } from './offers/offers';
import { offersAnalysisRouter } from './offers/offers-analysis-router';
import { offersCommentsRouter } from './offers/offers-comments-router';
@ -38,6 +39,7 @@ export const appRouter = createRouter()
.merge('todos.', todosRouter)
.merge('todos.user.', todosUserRouter)
.merge('companies.', companiesRouter)
.merge('locations.', locationsRouter)
.merge('resumes.resume.', resumesRouter)
.merge('resumes.resume.user.', resumesResumeUserRouter)
.merge('resumes.resume.', resumesStarUserRouter)

@ -0,0 +1,57 @@
import { z } from 'zod';
import { createRouter } from './context';
export const locationsRouter = createRouter()
.query('cities.list', {
input: z.object({
name: z.string(),
}),
async resolve({ ctx, input }) {
return await ctx.prisma.city.findMany({
orderBy: {
name: 'asc',
},
select: {
id: true,
name: true,
state: {
select: {
country: {
select: {
name: true,
},
},
name: true,
},
},
},
take: 10,
where: {
name: {
contains: input.name,
mode: 'insensitive',
},
},
});
},
})
.query('countries.list', {
input: z.object({
name: z.string(),
}),
async resolve({ ctx, input }) {
return await ctx.prisma.country.findMany({
orderBy: {
name: 'asc',
},
take: 10,
where: {
name: {
contains: input.name,
mode: 'insensitive',
},
},
});
},
});

@ -102,6 +102,21 @@ const education = z.object({
});
export const offersProfileRouter = createRouter()
.query('isValidToken', {
input: z.object({
profileId: z.string(),
token: z.string(),
}),
async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId
}
})
return profile?.editToken === input.token
}
})
.query('listOne', {
input: z.object({
profileId: z.string(),

@ -47,7 +47,7 @@ export const offersUserProfileRouter = createProtectedRouter()
});
},
})
.mutation('getUserProfiles', {
.query('getUserProfiles', {
async resolve({ ctx }) {
const userId = ctx.session.user.id
const result = await ctx.prisma.user.findFirst({

@ -1,275 +0,0 @@
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
import { createProtectedRouter } from './context';
export const questionsListRouter = createProtectedRouter()
.query('getListsByUser', {
async resolve({ ctx }) {
const userId = ctx.session?.user?.id;
// TODO: Optimize by not returning question entries
const questionsLists = await ctx.prisma.questionsList.findMany({
include: {
questionEntries: {
include: {
question: {
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
},
},
},
},
orderBy: {
createdAt: 'asc',
},
where: {
userId,
},
});
const lists = questionsLists.map((list) => ({
...list,
questionEntries: list.questionEntries.map((entry) => ({
...entry,
question: createQuestionWithAggregateData(entry.question),
})),
}));
return lists;
},
})
.query('getListById', {
input: z.object({
listId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { listId } = input;
const questionList = await ctx.prisma.questionsList.findFirst({
include: {
questionEntries: {
include: {
question: {
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
},
},
},
},
orderBy: {
createdAt: 'asc',
},
where: {
id: listId,
userId,
},
});
if (!questionList) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Question list not found',
});
}
return {
...questionList,
questionEntries: questionList.questionEntries.map((questionEntry) => ({
...questionEntry,
question: createQuestionWithAggregateData(questionEntry.question),
})),
};
},
})
.mutation('create', {
input: z.object({
name: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { name } = input;
return await ctx.prisma.questionsList.create({
data: {
name,
userId,
},
});
},
})
.mutation('update', {
input: z.object({
id: z.string(),
name: z.string().optional(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { name, id } = input;
const listToUpdate = await ctx.prisma.questionsList.findUnique({
where: {
id: input.id,
},
});
if (listToUpdate?.id !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
return await ctx.prisma.questionsList.update({
data: {
name,
},
where: {
id,
},
});
},
})
.mutation('delete', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const listToDelete = await ctx.prisma.questionsList.findUnique({
where: {
id: input.id,
},
});
if (listToDelete?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
return await ctx.prisma.questionsList.delete({
where: {
id: input.id,
},
});
},
})
.mutation('createQuestionEntry', {
input: z.object({
listId: z.string(),
questionId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const listToAugment = await ctx.prisma.questionsList.findUnique({
where: {
id: input.listId,
},
});
if (listToAugment?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
const { questionId, listId } = input;
return await ctx.prisma.questionsListQuestionEntry.create({
data: {
listId,
questionId,
},
});
},
})
.mutation('deleteQuestionEntry', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const entryToDelete =
await ctx.prisma.questionsListQuestionEntry.findUnique({
where: {
id: input.id,
},
});
if (entryToDelete === null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Entry not found.',
});
}
const listToAugment = await ctx.prisma.questionsList.findUnique({
where: {
id: entryToDelete.listId,
},
});
if (listToAugment?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
return await ctx.prisma.questionsListQuestionEntry.delete({
where: {
id: input.id,
},
});
},
});

@ -1,437 +0,0 @@
import { z } from 'zod';
import { QuestionsQuestionType, Vote } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
import { createProtectedRouter } from './context';
import { SortOrder, SortType } from '~/types/questions.d';
export const questionsQuestionRouter = createProtectedRouter()
.query('getQuestionsByFilter', {
input: z.object({
companyNames: z.string().array(),
cursor: z
.object({
idCursor: z.string().optional(),
lastSeenCursor: z.date().nullish().optional(),
upvoteCursor: z.number().optional(),
})
.nullish(),
endDate: z.date().default(new Date()),
limit: z.number().min(1).default(50),
locations: z.string().array(),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
sortOrder: z.nativeEnum(SortOrder),
sortType: z.nativeEnum(SortType),
startDate: z.date().optional(),
}),
async resolve({ ctx, input }) {
const { cursor } = input;
const sortCondition =
input.sortType === SortType.TOP
? [
{
upvotes: input.sortOrder,
},
{
id: input.sortOrder,
},
]
: [
{
lastSeenAt: input.sortOrder,
},
{
id: input.sortOrder,
},
];
const questionsData = await ctx.prisma.questionsQuestion.findMany({
cursor:
cursor !== undefined
? {
id: cursor ? cursor!.idCursor : undefined,
}
: undefined,
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
orderBy: sortCondition,
take: input.limit + 1,
where: {
...(input.questionTypes.length > 0
? {
questionType: {
in: input.questionTypes,
},
}
: {}),
encounters: {
some: {
seenAt: {
gte: input.startDate,
lte: input.endDate,
},
...(input.companyNames.length > 0
? {
company: {
name: {
in: input.companyNames,
},
},
}
: {}),
...(input.locations.length > 0
? {
location: {
in: input.locations,
},
}
: {}),
...(input.roles.length > 0
? {
role: {
in: input.roles,
},
}
: {}),
},
},
},
});
const processedQuestionsData = questionsData.map(
createQuestionWithAggregateData,
);
let nextCursor: typeof cursor | undefined = undefined;
if (questionsData.length > input.limit) {
const nextItem = questionsData.pop()!;
processedQuestionsData.pop();
const nextIdCursor: string | undefined = nextItem.id;
const nextLastSeenCursor =
input.sortType === SortType.NEW ? nextItem.lastSeenAt : undefined;
const nextUpvoteCursor =
input.sortType === SortType.TOP ? nextItem.upvotes : undefined;
nextCursor = {
idCursor: nextIdCursor,
lastSeenCursor: nextLastSeenCursor,
upvoteCursor: nextUpvoteCursor,
};
}
return {
data: processedQuestionsData,
nextCursor,
};
},
})
.query('getQuestionById', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const questionData = await ctx.prisma.questionsQuestion.findUnique({
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
where: {
id: input.id,
},
});
if (!questionData) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Question not found',
});
}
return createQuestionWithAggregateData(questionData);
},
})
.mutation('create', {
input: z.object({
companyId: z.string(),
content: z.string(),
location: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType),
role: z.string(),
seenAt: z.date(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
return await ctx.prisma.questionsQuestion.create({
data: {
content: input.content,
encounters: {
create: {
company: {
connect: {
id: input.companyId,
},
},
location: input.location,
role: input.role,
seenAt: input.seenAt,
user: {
connect: {
id: userId,
},
},
},
},
lastSeenAt: input.seenAt,
questionType: input.questionType,
userId,
},
});
},
})
.mutation('update', {
input: z.object({
content: z.string().optional(),
id: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType).optional(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const questionToUpdate = await ctx.prisma.questionsQuestion.findUnique({
where: {
id: input.id,
},
});
if (questionToUpdate?.id !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
// Optional: pass the original error to retain stack trace
});
}
const { content, questionType } = input;
return await ctx.prisma.questionsQuestion.update({
data: {
content,
questionType,
},
where: {
id: input.id,
},
});
},
})
.mutation('delete', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const questionToDelete = await ctx.prisma.questionsQuestion.findUnique({
where: {
id: input.id,
},
});
if (questionToDelete?.id !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
// Optional: pass the original error to retain stack trace
});
}
return await ctx.prisma.questionsQuestion.delete({
where: {
id: input.id,
},
});
},
})
.query('getVote', {
input: z.object({
questionId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { questionId } = input;
return await ctx.prisma.questionsQuestionVote.findUnique({
where: {
questionId_userId: { questionId, userId },
},
});
},
})
.mutation('createVote', {
input: z.object({
questionId: z.string(),
vote: z.nativeEnum(Vote),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { questionId, vote } = input;
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.create({
data: {
questionId,
userId,
vote,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: questionId,
},
}),
]);
return questionVote;
},
})
.mutation('updateVote', {
input: z.object({
id: z.string(),
vote: z.nativeEnum(Vote),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const { id, vote } = input;
const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({
where: {
id: input.id,
},
});
if (voteToUpdate?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.update({
data: {
vote,
},
where: {
id,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: voteToUpdate.questionId,
},
}),
]);
return questionVote;
},
})
.mutation('deleteVote', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session?.user?.id;
const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({
where: {
id: input.id,
},
});
if (voteToDelete?.userId !== userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User have no authorization to record.',
});
}
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.delete({
where: {
id: input.id,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: voteToDelete.questionId,
},
}),
]);
return questionVote;
},
});

@ -127,7 +127,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
where: {
answerCommentId_userId: { answerCommentId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsAnswerCommentVote.create({
@ -149,7 +149,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -164,18 +164,15 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
}
if (vote.vote === Vote.DOWNVOTE) {
tx.questionsAnswerCommentVote.delete({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerCommentVote.create({
const updatedVote = await tx.questionsAnswerCommentVote.update({
data: {
answerCommentId,
userId,
vote: Vote.UPVOTE,
},
where: {
id: vote.id,
},
});
await tx.questionsAnswerComment.update({
@ -189,7 +186,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return updatedVote;
}
});
},
@ -221,7 +218,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
where: {
answerCommentId_userId: { answerCommentId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsAnswerCommentVote.create({
@ -243,7 +240,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -258,18 +255,15 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
}
if (vote.vote === Vote.UPVOTE) {
tx.questionsAnswerCommentVote.delete({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerCommentVote.create({
const updatedVote = await tx.questionsAnswerCommentVote.update({
data: {
answerCommentId,
userId,
vote: Vote.DOWNVOTE,
},
where: {
id: vote.id,
},
});
await tx.questionsAnswerComment.update({
@ -283,7 +277,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return updatedVote;
}
});
},
@ -315,7 +309,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
where: {
answerCommentId_userId: { answerCommentId, userId },
},
})
});
if (voteToDelete === null) {
return null;
@ -330,7 +324,7 @@ export const questionsAnswerCommentUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsAnswerCommentVote.delete({
await tx.questionsAnswerCommentVote.delete({
where: {
id: voteToDelete.id,
},

@ -107,8 +107,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const { answerId } = input;
return await ctx.prisma.$transaction(async (tx) => {
const answerToUpdate =
await tx.questionsAnswer.findUnique({
const answerToUpdate = await tx.questionsAnswer.findUnique({
where: {
id: answerId,
},
@ -125,7 +124,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
where: {
answerId_userId: { answerId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsAnswerVote.create({
@ -147,7 +146,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -162,18 +161,15 @@ export const questionsAnswerUserRouter = createProtectedRouter()
}
if (vote.vote === Vote.DOWNVOTE) {
tx.questionsAnswerVote.delete({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerVote.create({
const updatedVote = await tx.questionsAnswerVote.update({
data: {
answerId,
userId,
vote: Vote.UPVOTE,
},
where: {
id: vote.id,
},
});
await tx.questionsAnswer.update({
@ -187,7 +183,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
},
});
return createdVote
return updatedVote;
}
});
},
@ -201,8 +197,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const { answerId } = input;
return await ctx.prisma.$transaction(async (tx) => {
const answerToUpdate =
await tx.questionsAnswer.findUnique({
const answerToUpdate = await tx.questionsAnswer.findUnique({
where: {
id: answerId,
},
@ -219,7 +214,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
where: {
answerId_userId: { answerId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsAnswerVote.create({
@ -241,7 +236,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -256,18 +251,15 @@ export const questionsAnswerUserRouter = createProtectedRouter()
}
if (vote.vote === Vote.UPVOTE) {
tx.questionsAnswerVote.delete({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsAnswerVote.create({
const updatedVote = await tx.questionsAnswerVote.update({
data: {
answerId,
userId,
vote: Vote.DOWNVOTE,
},
where: {
id: vote.id,
},
});
await tx.questionsAnswer.update({
@ -281,7 +273,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
},
});
return createdVote
return updatedVote;
}
});
},
@ -295,8 +287,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const { answerId } = input;
return await ctx.prisma.$transaction(async (tx) => {
const answerToUpdate =
await tx.questionsAnswer.findUnique({
const answerToUpdate = await tx.questionsAnswer.findUnique({
where: {
id: answerId,
},
@ -313,7 +304,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
where: {
answerId_userId: { answerId, userId },
},
})
});
if (voteToDelete === null) {
return null;
@ -328,7 +319,7 @@ export const questionsAnswerUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsAnswerVote.delete({
await tx.questionsAnswerVote.delete({
where: {
id: voteToDelete.id,
},

@ -128,7 +128,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
where: {
questionCommentId_userId: { questionCommentId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsQuestionCommentVote.create({
@ -150,7 +150,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -165,18 +165,15 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
}
if (vote.vote === Vote.DOWNVOTE) {
tx.questionsQuestionCommentVote.delete({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsQuestionCommentVote.create({
const updatedVote = await tx.questionsQuestionCommentVote.update({
data: {
questionCommentId,
userId,
vote: Vote.UPVOTE,
},
where: {
id: vote.id,
},
});
await tx.questionsQuestionComment.update({
@ -190,7 +187,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return updatedVote;
}
});
},
@ -222,7 +219,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
where: {
questionCommentId_userId: { questionCommentId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsQuestionCommentVote.create({
@ -244,7 +241,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -284,7 +281,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
});
},
@ -316,7 +313,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
where: {
questionCommentId_userId: { questionCommentId, userId },
},
})
});
if (voteToDelete === null) {
return null;
@ -331,7 +328,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsQuestionCommentVote.delete({
await tx.questionsQuestionCommentVote.delete({
where: {
id: voteToDelete.id,
},

@ -174,4 +174,64 @@ export const questionsQuestionRouter = createRouter()
return createQuestionWithAggregateData(questionData);
},
})
.query('getRelatedQuestions', {
input: z.object({
content: z.string(),
}),
async resolve({ ctx, input }) {
const escapeChars = /[()|&:*!]/g;
const query =
input.content
.replace(escapeChars, " ")
.trim()
.split(/\s+/)
.join(" | ");
const relatedQuestionsId : Array<{id:string}> = await ctx.prisma.$queryRaw`
SELECT id FROM "QuestionsQuestion"
WHERE
to_tsvector("content") @@ to_tsquery('english', ${query})
ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC;
`;
const relatedQuestionsIdArray = relatedQuestionsId.map(current => current.id);
const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
company: true,
location: true,
role: true,
seenAt: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
where: {
id : {
in : relatedQuestionsIdArray,
}
},
});
const processedQuestionsData = relatedQuestionsData.map(
createQuestionWithAggregateData,
);
return processedQuestionsData;
}
});

@ -132,8 +132,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const { questionId } = input;
return await ctx.prisma.$transaction(async (tx) => {
const questionToUpdate =
await tx.questionsQuestion.findUnique({
const questionToUpdate = await tx.questionsQuestion.findUnique({
where: {
id: questionId,
},
@ -150,7 +149,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
where: {
questionId_userId: { questionId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsQuestionVote.create({
@ -172,7 +171,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -187,18 +186,15 @@ export const questionsQuestionUserRouter = createProtectedRouter()
}
if (vote.vote === Vote.DOWNVOTE) {
tx.questionsQuestionVote.delete({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsQuestionVote.create({
const updatedVote = await tx.questionsQuestionVote.update({
data: {
questionId,
userId,
vote: Vote.UPVOTE,
},
where: {
id: vote.id,
},
});
await tx.questionsQuestion.update({
@ -212,7 +208,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
},
});
return createdVote
return updatedVote;
}
});
},
@ -226,8 +222,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const { questionId } = input;
return await ctx.prisma.$transaction(async (tx) => {
const questionToUpdate =
await tx.questionsQuestion.findUnique({
const questionToUpdate = await tx.questionsQuestion.findUnique({
where: {
id: questionId,
},
@ -244,7 +239,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
where: {
questionId_userId: { questionId, userId },
},
})
});
if (vote === null) {
const createdVote = await tx.questionsQuestionVote.create({
@ -266,7 +261,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
},
});
return createdVote
return createdVote;
}
if (vote!.userId !== userId) {
@ -276,23 +271,20 @@ export const questionsQuestionUserRouter = createProtectedRouter()
});
}
if (vote!.vote === Vote.DOWNVOTE) {
if (vote.vote === Vote.DOWNVOTE) {
return vote;
}
if (vote.vote === Vote.UPVOTE) {
tx.questionsQuestionVote.delete({
where: {
id: vote.id,
},
});
const createdVote = await tx.questionsQuestionVote.create({
const updatedVote = await tx.questionsQuestionVote.update({
data: {
questionId,
userId,
vote: Vote.DOWNVOTE,
},
where: {
id: vote.id,
},
});
await tx.questionsQuestion.update({
@ -306,7 +298,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
},
});
return createdVote
return updatedVote;
}
});
},
@ -320,8 +312,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const { questionId } = input;
return await ctx.prisma.$transaction(async (tx) => {
const questionToUpdate =
await tx.questionsQuestion.findUnique({
const questionToUpdate = await tx.questionsQuestion.findUnique({
where: {
id: questionId,
},
@ -338,7 +329,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
where: {
questionId_userId: { questionId, userId },
},
})
});
if (voteToDelete === null) {
return null;
@ -353,7 +344,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
const incrementValue = voteToDelete!.vote === Vote.UPVOTE ? -1 : 1;
tx.questionsQuestionVote.delete({
await tx.questionsQuestionVote.delete({
where: {
id: voteToDelete.id,
},

@ -113,7 +113,6 @@ export const resumesRouter = createRouter()
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
});
@ -143,7 +142,6 @@ export const resumesRouter = createRouter()
},
by: ['experience'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
@ -171,7 +169,6 @@ export const resumesRouter = createRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
},
@ -343,72 +340,4 @@ export const resumesRouter = createRouter()
return topUpvotedCommentCount;
},
})
.query('getTotalFilterCounts', {
async resolve({ ctx }) {
const roleCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
by: ['role'],
});
const mappedRoleCounts = Object.fromEntries(
roleCounts.map((rc) => [rc.role, rc._count._all]),
);
const zeroRoleCounts = Object.fromEntries(
ROLES.filter((r) => !(r.value in mappedRoleCounts)).map((r) => [
r.value,
0,
]),
);
const processedRoleCounts = {
...mappedRoleCounts,
...zeroRoleCounts,
};
const experienceCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
by: ['experience'],
});
const mappedExperienceCounts = Object.fromEntries(
experienceCounts.map((ec) => [ec.experience, ec._count._all]),
);
const zeroExperienceCounts = Object.fromEntries(
EXPERIENCES.filter((e) => !(e.value in mappedExperienceCounts)).map(
(e) => [e.value, 0],
),
);
const processedExperienceCounts = {
...mappedExperienceCounts,
...zeroExperienceCounts,
};
const locationCounts = await ctx.prisma.resumesResume.groupBy({
_count: {
_all: true,
},
by: ['location'],
});
const mappedLocationCounts = Object.fromEntries(
locationCounts.map((lc) => [lc.location, lc._count._all]),
);
const zeroLocationCounts = Object.fromEntries(
LOCATIONS.filter((l) => !(l.value in mappedLocationCounts)).map((l) => [
l.value,
0,
]),
);
const processedLocationCounts = {
...mappedLocationCounts,
...zeroLocationCounts,
};
return {
Experience: processedExperienceCounts,
Location: processedLocationCounts,
Role: processedRoleCounts,
};
},
});

@ -180,7 +180,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
stars: {
some: {
userId,
@ -209,7 +208,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
by: ['experience'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
@ -242,7 +240,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
stars: {
some: {
@ -378,7 +375,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,
},
@ -403,7 +399,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
},
by: ['experience'],
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
@ -432,7 +427,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' },
userId,

@ -188,7 +188,7 @@ export type AddToProfileResponse = {
export type UserProfile = {
createdAt: Date;
id: string;
offers: Array<UserProfileOffers>;
offers: Array<UserProfileOffer>;
profileName: string;
token: string;
}

@ -0,0 +1,26 @@
import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { AggregatedQuestionEncounter } from '~/types/questions';
export default function relabelQuestionAggregates({
locationCounts,
companyCounts,
roleCounts,
latestSeenAt,
}: AggregatedQuestionEncounter) {
const newRoleCounts = Object.fromEntries(
Object.entries(roleCounts).map(([roleId, count]) => [
JobTitleLabels[roleId as keyof typeof JobTitleLabels],
count,
]),
);
const relabeledAggregate: AggregatedQuestionEncounter = {
companyCounts,
latestSeenAt,
locationCounts,
roleCounts: newRoleCounts,
};
return relabeledAggregate;
}

@ -5,9 +5,9 @@ import type { Vote } from '@prisma/client';
import { trpc } from '../trpc';
type UseVoteOptions = {
createVote: (opts: { vote: Vote }) => void;
deleteVote: (opts: { id: string }) => void;
updateVote: (opts: BackendVote) => void;
setDownVote: () => void;
setNoVote: () => void;
setUpVote: () => void;
};
type BackendVote = {
@ -19,47 +19,23 @@ const createVoteCallbacks = (
vote: BackendVote | null,
opts: UseVoteOptions,
) => {
const { createVote, updateVote, deleteVote } = opts;
const { setDownVote, setNoVote, setUpVote } = opts;
const handleUpvote = () => {
// Either upvote or remove upvote
if (vote) {
if (vote.vote === 'DOWNVOTE') {
updateVote({
id: vote.id,
vote: 'UPVOTE',
});
if (vote && vote.vote === 'UPVOTE') {
setNoVote();
} else {
deleteVote({
id: vote.id,
});
}
// Update vote to an upvote
} else {
createVote({
vote: 'UPVOTE',
});
setUpVote();
}
};
const handleDownvote = () => {
// Either downvote or remove downvote
if (vote) {
if (vote.vote === 'UPVOTE') {
updateVote({
id: vote.id,
vote: 'DOWNVOTE',
});
if (vote && vote.vote === 'DOWNVOTE') {
setNoVote();
} else {
deleteVote({
id: vote.id,
});
}
// Update vote to an upvote
} else {
createVote({
vote: 'DOWNVOTE',
});
setDownVote();
}
};
@ -71,61 +47,61 @@ type QueryKey = Parameters<typeof trpc.useQuery>[0][0];
export const useQuestionVote = (id: string) => {
return useVote(id, {
create: 'questions.questions.user.createVote',
deleteKey: 'questions.questions.user.deleteVote',
idKey: 'questionId',
invalidateKeys: [
'questions.questions.getQuestionsByFilter',
'questions.questions.getQuestionById',
],
query: 'questions.questions.user.getVote',
update: 'questions.questions.user.updateVote',
setDownVoteKey: 'questions.questions.user.setDownVote',
setNoVoteKey: 'questions.questions.user.setNoVote',
setUpVoteKey: 'questions.questions.user.setUpVote',
});
};
export const useAnswerVote = (id: string) => {
return useVote(id, {
create: 'questions.answers.user.createVote',
deleteKey: 'questions.answers.user.deleteVote',
idKey: 'answerId',
invalidateKeys: [
'questions.answers.getAnswers',
'questions.answers.getAnswerById',
],
query: 'questions.answers.user.getVote',
update: 'questions.answers.user.updateVote',
setDownVoteKey: 'questions.answers.user.setDownVote',
setNoVoteKey: 'questions.answers.user.setNoVote',
setUpVoteKey: 'questions.answers.user.setUpVote',
});
};
export const useQuestionCommentVote = (id: string) => {
return useVote(id, {
create: 'questions.questions.comments.user.createVote',
deleteKey: 'questions.questions.comments.user.deleteVote',
idKey: 'questionCommentId',
invalidateKeys: ['questions.questions.comments.getQuestionComments'],
query: 'questions.questions.comments.user.getVote',
update: 'questions.questions.comments.user.updateVote',
setDownVoteKey: 'questions.questions.comments.user.setDownVote',
setNoVoteKey: 'questions.questions.comments.user.setNoVote',
setUpVoteKey: 'questions.questions.comments.user.setUpVote',
});
};
export const useAnswerCommentVote = (id: string) => {
return useVote(id, {
create: 'questions.answers.comments.user.createVote',
deleteKey: 'questions.answers.comments.user.deleteVote',
idKey: 'answerCommentId',
invalidateKeys: ['questions.answers.comments.getAnswerComments'],
query: 'questions.answers.comments.user.getVote',
update: 'questions.answers.comments.user.updateVote',
setDownVoteKey: 'questions.answers.comments.user.setDownVote',
setNoVoteKey: 'questions.answers.comments.user.setNoVote',
setUpVoteKey: 'questions.answers.comments.user.setUpVote',
});
};
type VoteProps<VoteQueryKey extends QueryKey = QueryKey> = {
create: MutationKey;
deleteKey: MutationKey;
idKey: string;
invalidateKeys: Array<VoteQueryKey>;
query: VoteQueryKey;
update: MutationKey;
setDownVoteKey: MutationKey;
setNoVoteKey: MutationKey;
setUpVoteKey: MutationKey;
};
type UseVoteMutationContext = {
@ -137,7 +113,14 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
id: string,
opts: VoteProps<VoteQueryKey>,
) => {
const { create, deleteKey, query, update, idKey, invalidateKeys } = opts;
const {
idKey,
invalidateKeys,
query,
setDownVoteKey,
setNoVoteKey,
setUpVoteKey,
} = opts;
const utils = trpc.useContext();
const onVoteUpdate = useCallback(() => {
@ -157,8 +140,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
const backendVote = data as BackendVote;
const { mutate: createVote } = trpc.useMutation<any, UseVoteMutationContext>(
create,
const { mutate: setUpVote } = trpc.useMutation<any, UseVoteMutationContext>(
setUpVoteKey,
{
onError: (err, variables, context) => {
if (context !== undefined) {
@ -185,8 +168,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
onSettled: onVoteUpdate,
},
);
const { mutate: updateVote } = trpc.useMutation<any, UseVoteMutationContext>(
update,
const { mutate: setDownVote } = trpc.useMutation<any, UseVoteMutationContext>(
setDownVoteKey,
{
onError: (error, variables, context) => {
if (context !== undefined) {
@ -214,8 +197,8 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
},
);
const { mutate: deleteVote } = trpc.useMutation<any, UseVoteMutationContext>(
deleteKey,
const { mutate: setNoVote } = trpc.useMutation<any, UseVoteMutationContext>(
setNoVoteKey,
{
onError: (err, variables, context) => {
if (context !== undefined) {
@ -242,14 +225,21 @@ export const useVote = <VoteQueryKey extends QueryKey = QueryKey>(
const { handleDownvote, handleUpvote } = createVoteCallbacks(
backendVote ?? null,
{
createVote: ({ vote }) => {
createVote({
setDownVote: () => {
setDownVote({
[idKey]: id,
vote,
} as any);
});
},
setNoVote: () => {
setNoVote({
[idKey]: id,
});
},
setUpVote: () => {
setUpVote({
[idKey]: id,
});
},
deleteVote,
updateVote,
},
);

@ -59,7 +59,7 @@ export function Basic({
{ id: '5', label: 'Tanya Fox', value: '5' },
{ id: '6', label: 'Hellen Schmidt', value: '6' },
];
const [selectedEntry, setSelectedEntry] = useState<TypeaheadOption>(
const [selectedEntry, setSelectedEntry] = useState<TypeaheadOption | null>(
people[0],
);
const [query, setQuery] = useState('');
@ -102,7 +102,7 @@ export function Required() {
{ id: '5', label: 'Tanya Fox', value: '5' },
{ id: '6', label: 'Hellen Schmidt', value: '6' },
];
const [selectedEntry, setSelectedEntry] = useState<TypeaheadOption>(
const [selectedEntry, setSelectedEntry] = useState<TypeaheadOption | null>(
people[0],
);
const [query, setQuery] = useState('');
@ -153,7 +153,7 @@ export function Error() {
{ id: '5', label: 'Tanya Fox', value: '5' },
{ id: '6', label: 'Hellen Schmidt', value: '6' },
];
const [selectedEntry, setSelectedEntry] = useState<TypeaheadOption>(
const [selectedEntry, setSelectedEntry] = useState<TypeaheadOption | null>(
people[0],
);
const [query, setQuery] = useState('');
@ -171,7 +171,7 @@ export function Error() {
return (
<Typeahead
errorMessage={
selectedEntry.id === '1' ? 'Cannot select Wade Cooper' : undefined
selectedEntry?.id === '1' ? 'Cannot select Wade Cooper' : undefined
}
label="Author"
options={filteredPeople}

@ -34,10 +34,10 @@ type Props = Readonly<{
value: string,
event: React.ChangeEvent<HTMLInputElement>,
) => void;
onSelect: (option: TypeaheadOption) => void;
onSelect: (option: TypeaheadOption | null) => void;
options: ReadonlyArray<TypeaheadOption>;
textSize?: TypeaheadTextSize;
value?: TypeaheadOption;
value?: TypeaheadOption | null;
}> &
Readonly<Attributes>;
@ -90,6 +90,8 @@ export default function Typeahead({
return (
<div>
<Combobox
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
by="id"
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -102,13 +104,9 @@ export default function Typeahead({
// @ts-ignore
value={value}
onChange={(newValue) => {
if (newValue == null) {
return;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
onSelect(newValue as TypeaheadOption);
onSelect(newValue as TypeaheadOption | null);
}}>
<Combobox.Label
className={clsx(

Loading…
Cancel
Save