Merge branch 'main' into hongpo/sorting-pagination

pull/457/head
hpkoh 3 years ago
commit 373e249014

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

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[] 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. // Start of Resumes project models.
// Add Resumes project models here, prefix all models with "Resumes", // Add Resumes project models here, prefix all models with "Resumes",
// use camelCase for field names, and try to name them consistently // use camelCase for field names, and try to name them consistently

@ -1,5 +1,9 @@
const { PrismaClient } = require('@prisma/client'); 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 prisma = new PrismaClient();
const COMPANIES = [ 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.`, 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', logoUrl: 'https://logo.clearbit.com/microsoft.com',
}, },
{
name: 'Netflix',
slug: 'netflix',
description: null,
logoUrl: 'https://logo.clearbit.com/netflix.com',
},
]; ];
async function main() { async function main() {
console.log('Seeding started...'); console.log('Seeding started...');
await Promise.all([
COMPANIES.map(async (company) => { console.info('Seeding companies');
await prisma.company.upsert({ await prisma.company.createMany({
where: { slug: company.slug }, data: COMPANIES.map((company) => ({
update: company, name: company.name,
create: company, 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.'); console.log('Seeding completed.');
} }

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

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

@ -11,6 +11,7 @@ import type { ProductNavigationItems } from './ProductNavigation';
type Props = Readonly<{ type Props = Readonly<{
globalNavigationItems: GlobalNavigationItems; globalNavigationItems: GlobalNavigationItems;
isShown?: boolean; isShown?: boolean;
logo?: React.ReactNode;
productNavigationItems: ProductNavigationItems; productNavigationItems: ProductNavigationItems;
productTitle: string; productTitle: string;
setIsShown: (isShown: boolean) => void; setIsShown: (isShown: boolean) => void;
@ -19,6 +20,7 @@ type Props = Readonly<{
export default function MobileNavigation({ export default function MobileNavigation({
globalNavigationItems, globalNavigationItems,
isShown, isShown,
logo,
productNavigationItems, productNavigationItems,
productTitle, productTitle,
setIsShown, setIsShown,
@ -69,11 +71,13 @@ export default function MobileNavigation({
</Transition.Child> </Transition.Child>
<div className="flex flex-shrink-0 items-center px-4"> <div className="flex flex-shrink-0 items-center px-4">
<Link href="/"> <Link href="/">
<img {logo ?? (
alt="Tech Interview Handbook" <img
className="h-8 w-auto" alt="Tech Interview Handbook"
src="/logo.svg" className="h-8 w-auto"
/> src="/logo.svg"
/>
)}
</Link> </Link>
</div> </div>
<div className="mt-5 h-0 flex-1 overflow-y-auto px-2"> <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<{ type Props = Readonly<{
items: ProductNavigationItems; items: ProductNavigationItems;
logo?: React.ReactNode;
title: string; title: string;
titleHref: string; titleHref: string;
}>; }>;
export default function ProductNavigation({ items, title, titleHref }: Props) { export default function ProductNavigation({
items,
logo,
title,
titleHref,
}: Props) {
const router = useRouter(); const router = useRouter();
return ( return (
@ -29,13 +35,14 @@ export default function ProductNavigation({ items, title, titleHref }: Props) {
<Link <Link
className="hover:text-primary-700 flex items-center gap-2 text-base font-medium" className="hover:text-primary-700 flex items-center gap-2 text-base font-medium"
href={titleHref}> href={titleHref}>
{titleHref !== '/' && ( {titleHref !== '/' &&
<img (logo ?? (
alt="Tech Interview Handbook" <img
className="h-8 w-auto" alt="Tech Interview Handbook"
src="/logo.svg" className="h-8 w-auto"
/> src="/logo.svg"
)} />
))}
{title} {title}
</Link> </Link>
<div className="hidden h-full items-center space-x-8 md:flex"> <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'; import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
const navigation: ProductNavigationItems = [ const navigation: ProductNavigationItems = [
@ -7,7 +9,13 @@ const navigation: ProductNavigationItems = [
const config = { const config = {
// TODO: Change this to your own GA4 measurement ID. // 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, navigation,
showGlobalNav: false, showGlobalNav: false,
title: 'Tech Offers Repo', title: 'Tech Offers Repo',

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

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

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

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

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

@ -15,7 +15,9 @@ export default function ResumeFilterPill({
<button <button
className={clsx( 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', '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" type="button"
onClick={onClick}> onClick={onClick}>

@ -3,6 +3,8 @@ import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Button, Dialog, TextArea } from '@tih/ui'; import { Button, Dialog, TextArea } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
type ResumeCommentsFormProps = Readonly<{ type ResumeCommentsFormProps = Readonly<{
@ -25,6 +27,8 @@ export default function ResumeCommentsForm({
setShowCommentsForm, setShowCommentsForm,
}: ResumeCommentsFormProps) { }: ResumeCommentsFormProps) {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const { const {
register, register,
handleSubmit, handleSubmit,
@ -50,6 +54,11 @@ export default function ResumeCommentsForm({
trpcContext.invalidateQueries(['resumes.resume.findAll']); trpcContext.invalidateQueries(['resumes.resume.findAll']);
trpcContext.invalidateQueries(['resumes.resume.user.findUserStarred']); trpcContext.invalidateQueries(['resumes.resume.user.findUserStarred']);
trpcContext.invalidateQueries(['resumes.resume.user.findUserCreated']); 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 type { ResumesSection } from '@prisma/client';
import { Button, TextArea } from '@tih/ui'; import { Button, TextArea } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
type ResumeCommentEditFormProps = { type ResumeCommentEditFormProps = {
@ -33,6 +35,7 @@ export default function ResumeCommentReplyForm({
description: '', description: '',
}, },
}); });
const { event: gaEvent } = useGoogleAnalytics();
const trpcContext = trpc.useContext(); const trpcContext = trpc.useContext();
const commentReplyMutation = trpc.useMutation('resumes.comments.user.reply', { const commentReplyMutation = trpc.useMutation('resumes.comments.user.reply', {
@ -58,6 +61,12 @@ export default function ResumeCommentReplyForm({
{ {
onSuccess: () => { onSuccess: () => {
setIsReplyingComment(false); setIsReplyingComment(false);
gaEvent({
action: 'resumes.comment_reply',
category: 'engagement',
label: 'Reply comment',
});
}, },
}, },
); );

@ -7,6 +7,8 @@ import {
} from '@heroicons/react/20/solid'; } from '@heroicons/react/20/solid';
import { Vote } from '@prisma/client'; import { Vote } from '@prisma/client';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
type ResumeCommentVoteButtonsProps = { type ResumeCommentVoteButtonsProps = {
@ -20,6 +22,7 @@ export default function ResumeCommentVoteButtons({
}: ResumeCommentVoteButtonsProps) { }: ResumeCommentVoteButtonsProps) {
const [upvoteAnimation, setUpvoteAnimation] = useState(false); const [upvoteAnimation, setUpvoteAnimation] = useState(false);
const [downvoteAnimation, setDownvoteAnimation] = useState(false); const [downvoteAnimation, setDownvoteAnimation] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const trpcContext = trpc.useContext(); const trpcContext = trpc.useContext();
const router = useRouter(); const router = useRouter();
@ -35,6 +38,11 @@ export default function ResumeCommentVoteButtons({
onSuccess: () => { onSuccess: () => {
// Comment updated, invalidate query to trigger refetch // Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']); 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: () => { onSuccess: () => {
// Comment updated, invalidate query to trigger refetch // Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']); 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; disabled?: boolean;
errorMessage?: string; errorMessage?: string;
isLabelHidden?: boolean; isLabelHidden?: boolean;
onSelect: (option: TypeaheadOption) => void; onSelect: (option: TypeaheadOption | null) => void;
placeHolder?: string; placeholder?: string;
required?: boolean; required?: boolean;
value?: TypeaheadOption | null;
}>; }>;
export default function CompaniesTypeahead({ export default function CompaniesTypeahead({
disabled, disabled,
onSelect, onSelect,
isLabelHidden, isLabelHidden,
placeHolder, placeholder,
required, required,
value,
}: Props) { }: Props) {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const companies = trpc.useQuery([ const companies = trpc.useQuery([
@ -44,9 +46,10 @@ export default function CompaniesTypeahead({
value: id, value: id,
})) ?? [] })) ?? []
} }
placeholder={placeHolder} placeholder={placeholder}
required={required} required={required}
textSize="inherit" textSize="inherit"
value={value}
onQueryChange={setQuery} onQueryChange={setQuery}
onSelect={onSelect} 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<{ type Props = Readonly<{
disabled?: boolean; disabled?: boolean;
isLabelHidden?: boolean; isLabelHidden?: boolean;
onSelect: (option: TypeaheadOption) => void; onSelect: (option: TypeaheadOption | null) => void;
placeHolder?: string; placeholder?: string;
required?: boolean; required?: boolean;
value?: TypeaheadOption | null;
}>; }>;
export default function JobTitlesTypeahead({ export default function JobTitlesTypeahead({
disabled, disabled,
onSelect, onSelect,
isLabelHidden, isLabelHidden,
placeHolder, placeholder,
required, required,
value,
}: Props) { }: Props) {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const options = Object.entries(JobTitleLabels) const options = Object.entries(JobTitleLabels)
@ -39,9 +41,10 @@ export default function JobTitlesTypeahead({
noResultsMessage="No available job titles." noResultsMessage="No available job titles."
nullable={true} nullable={true}
options={options} options={options}
placeholder={placeHolder} placeholder={placeholder}
required={required} required={required}
textSize="inherit" textSize="inherit"
value={value}
onQueryChange={setQuery} onQueryChange={setQuery}
onSelect={onSelect} onSelect={onSelect}
/> />

@ -36,13 +36,15 @@ export default function OffersHomePage() {
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<JobTitlesTypeahead <JobTitlesTypeahead
isLabelHidden={true} isLabelHidden={true}
placeHolder="Software Engineer" placeholder="Software Engineer"
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) => setjobTitleFilter(value)} onSelect={({ value }) => setjobTitleFilter(value)}
/> />
<span>in</span> <span>in</span>
<CompaniesTypeahead <CompaniesTypeahead
isLabelHidden={true} isLabelHidden={true}
placeHolder="All Companies" placeholder="All Companies"
// @ts-ignore TODO(offers): handle potentially null value.
onSelect={({ value }) => setCompanyFilter(value)} onSelect={({ value }) => setCompanyFilter(value)}
/> />
</div> </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( const replies = trpc.useQuery(
['offers.comments.getComments', { profileId }], ['offers.comments.getComments', { profileId }],
{ {

@ -323,6 +323,7 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true} isLabelHidden={true}
placeholder="Search companies" placeholder="Search companies"
onSelect={(option) => { onSelect={(option) => {
// @ts-ignore TODO(questions): handle potentially null value.
onOptionChange({ onOptionChange({
...option, ...option,
checked: true, checked: true,
@ -355,6 +356,7 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true} isLabelHidden={true}
placeholder="Search roles" placeholder="Search roles"
onSelect={(option) => { onSelect={(option) => {
// @ts-ignore TODO(questions): handle potentially null value.
onOptionChange({ onOptionChange({
...option, ...option,
checked: true, checked: true,
@ -412,6 +414,7 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true} isLabelHidden={true}
placeholder="Search locations" placeholder="Search locations"
onSelect={(option) => { onSelect={(option) => {
// @ts-ignore TODO(offers): fix potentially empty value.
onOptionChange({ onOptionChange({
...option, ...option,
checked: true, checked: true,

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

@ -127,6 +127,13 @@ export default function ResumeHomePage() {
'userFilters', 'userFilters',
INITIAL_FILTER_STATE, INITIAL_FILTER_STATE,
); );
const [isFiltersOpen, setIsFiltersOpen, isFiltersOpenInit] = useSearchParams<
Record<FilterId, boolean>
>('isFiltersOpen', {
experience: false,
location: false,
role: false,
});
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const skip = (currentPage - 1) * PAGE_LIMIT; const skip = (currentPage - 1) * PAGE_LIMIT;
@ -137,7 +144,8 @@ export default function ResumeHomePage() {
isSearchValueInit && isSearchValueInit &&
isShortcutInit && isShortcutInit &&
isCurrentPageInit && isCurrentPageInit &&
isUserFiltersInit isUserFiltersInit &&
isFiltersOpenInit
); );
}, [ }, [
isTabsValueInit, isTabsValueInit,
@ -146,6 +154,7 @@ export default function ResumeHomePage() {
isShortcutInit, isShortcutInit,
isCurrentPageInit, isCurrentPageInit,
isUserFiltersInit, isUserFiltersInit,
isFiltersOpenInit,
]); ]);
useEffect(() => { useEffect(() => {
@ -164,6 +173,7 @@ export default function ResumeHomePage() {
pathname: router.pathname, pathname: router.pathname,
query: { query: {
currentPage: JSON.stringify(currentPage), currentPage: JSON.stringify(currentPage),
isFiltersOpen: JSON.stringify(isFiltersOpen),
searchValue: JSON.stringify(searchValue), searchValue: JSON.stringify(searchValue),
shortcutSelected: JSON.stringify(shortcutSelected), shortcutSelected: JSON.stringify(shortcutSelected),
sortOrder: JSON.stringify(sortOrder), sortOrder: JSON.stringify(sortOrder),
@ -180,6 +190,7 @@ export default function ResumeHomePage() {
currentPage, currentPage,
router.pathname, router.pathname,
isSearchOptionsInit, isSearchOptionsInit,
isFiltersOpen,
]); ]);
const allResumesQuery = trpc.useQuery( const allResumesQuery = trpc.useQuery(
@ -399,11 +410,19 @@ export default function ResumeHomePage() {
<Disclosure <Disclosure
key={filter.id} key={filter.id}
as="div" 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 }) => ( {({ open }) => (
<> <>
<h3 className="-mx-2 -my-3 flow-root"> <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"> <span className="font-medium text-slate-900">
{filter.label} {filter.label}
</span> </span>
@ -427,12 +446,9 @@ export default function ResumeHomePage() {
{filter.options.map((option) => ( {filter.options.map((option) => (
<div <div
key={option.value} 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 <CheckboxInput
label={`${option.label} (${getFilterCount( label={option.label}
filter.label,
option.label,
)})`}
value={userFilters[filter.id].includes( value={userFilters[filter.id].includes(
option.value, 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>
))} ))}
</div> </div>
<p <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)}> onClick={() => onClearFilterClick(filter.id)}>
Clear Clear
</p> </p>
@ -491,72 +515,83 @@ export default function ResumeHomePage() {
<h3 className="text-md font-medium tracking-tight text-slate-900"> <h3 className="text-md font-medium tracking-tight text-slate-900">
Explore these filters Explore these filters
</h3> </h3>
{filters.map((filter) => ( {isFiltersOpenInit &&
<Disclosure filters.map((filter) => (
key={filter.id} <Disclosure
as="div" key={filter.id}
className="border-b border-slate-200 pt-6 pb-4"> as="div"
{({ open }) => ( className="border-b border-slate-200 pt-6 pb-4"
<> defaultOpen={isFiltersOpen[filter.id]}>
<h3 className="-my-3 flow-root"> {({ open }) => (
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500"> <>
<span className="font-medium text-slate-900"> <h3 className="-my-3 flow-root">
{filter.label} <Disclosure.Button
</span> className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500"
<span className="ml-6 flex items-center"> onClick={() =>
{open ? ( setIsFiltersOpen({
<MinusIcon ...isFiltersOpen,
aria-hidden="true" [filter.id]: !isFiltersOpen[filter.id],
className="h-5 w-5" })
/> }>
) : ( <span className="font-medium text-slate-900">
<PlusIcon {filter.label}
aria-hidden="true" </span>
className="h-5 w-5" <span className="ml-6 flex items-center">
/> {open ? (
)} <MinusIcon
</span> aria-hidden="true"
</Disclosure.Button> className="h-5 w-5"
</h3> />
<Disclosure.Panel className="space-y-4 pt-4"> ) : (
<CheckboxList <PlusIcon
description="" aria-hidden="true"
isLabelHidden={true} className="h-5 w-5"
label="" />
orientation="vertical"> )}
{filter.options.map((option) => ( </span>
<div </Disclosure.Button>
key={option.value} </h3>
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"> <Disclosure.Panel className="space-y-4 pt-4">
<CheckboxInput <CheckboxList
label={`${option.label} (${getFilterCount( description=""
filter.label, isLabelHidden={true}
option.label, label=""
)})`} orientation="vertical">
value={userFilters[filter.id].includes( {filter.options.map((option) => (
option.value, <div
)} key={option.value}
onChange={(isChecked) => 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">
onFilterCheckboxChange( <CheckboxInput
isChecked, label={option.label}
filter.id, value={userFilters[filter.id].includes(
option.value, option.value,
)}
onChange={(isChecked) =>
onFilterCheckboxChange(
isChecked,
filter.id,
option.value,
)
}
/>
<span className="ml-1 text-slate-500">
(
{getFilterCount(filter.label, option.label)}
) )
} </span>
/> </div>
</div> ))}
))} </CheckboxList>
</CheckboxList> <p
<p className="inline-block cursor-pointer text-sm text-slate-500 underline hover:text-slate-700"
className="cursor-pointer text-sm text-slate-500 underline" onClick={() => onClearFilterClick(filter.id)}>
onClick={() => onClearFilterClick(filter.id)}> Clear
Clear </p>
</p> </Disclosure.Panel>
</Disclosure.Panel> </>
</> )}
)} </Disclosure>
</Disclosure> ))}
))}
</form> </form>
</div> </div>
</div> </div>

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

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

@ -2,6 +2,7 @@ import superjson from 'superjson';
import { companiesRouter } from './companies-router'; import { companiesRouter } from './companies-router';
import { createRouter } from './context'; import { createRouter } from './context';
import { locationsRouter } from './locations-router';
import { offersRouter } from './offers/offers'; import { offersRouter } from './offers/offers';
import { offersAnalysisRouter } from './offers/offers-analysis-router'; import { offersAnalysisRouter } from './offers/offers-analysis-router';
import { offersCommentsRouter } from './offers/offers-comments-router'; import { offersCommentsRouter } from './offers/offers-comments-router';
@ -38,6 +39,7 @@ export const appRouter = createRouter()
.merge('todos.', todosRouter) .merge('todos.', todosRouter)
.merge('todos.user.', todosUserRouter) .merge('todos.user.', todosUserRouter)
.merge('companies.', companiesRouter) .merge('companies.', companiesRouter)
.merge('locations.', locationsRouter)
.merge('resumes.resume.', resumesRouter) .merge('resumes.resume.', resumesRouter)
.merge('resumes.resume.user.', resumesResumeUserRouter) .merge('resumes.resume.user.', resumesResumeUserRouter)
.merge('resumes.resume.', resumesStarUserRouter) .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() 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', { .query('listOne', {
input: z.object({ input: z.object({
profileId: z.string(), profileId: z.string(),

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

@ -113,7 +113,6 @@ export const resumesRouter = createRouter()
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
}, },
}); });
@ -143,7 +142,6 @@ export const resumesRouter = createRouter()
}, },
by: ['experience'], by: ['experience'],
where: { where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
@ -171,7 +169,6 @@ export const resumesRouter = createRouter()
where: { where: {
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
}, },
@ -343,72 +340,4 @@ export const resumesRouter = createRouter()
return topUpvotedCommentCount; 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 }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters },
stars: { stars: {
some: { some: {
userId, userId,
@ -209,7 +208,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
}, },
by: ['experience'], by: ['experience'],
where: { where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
@ -242,7 +240,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: { where: {
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
stars: { stars: {
some: { some: {
@ -378,7 +375,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
userId, userId,
}, },
@ -403,7 +399,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
}, },
by: ['experience'], by: ['experience'],
where: { where: {
experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters }, location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
@ -432,7 +427,6 @@ export const resumesResumeUserRouter = createProtectedRouter()
where: { where: {
experience: { in: experienceFilters }, experience: { in: experienceFilters },
isResolved: isUnreviewed ? false : {}, isResolved: isUnreviewed ? false : {},
location: { in: locationFilters },
role: { in: roleFilters }, role: { in: roleFilters },
title: { contains: searchValue, mode: 'insensitive' }, title: { contains: searchValue, mode: 'insensitive' },
userId, userId,

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

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

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

Loading…
Cancel
Save