Merge branch 'main' into hongpo/sorting-pagination

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

@ -5,4 +5,7 @@ module.exports = {
tsconfigRootDir: __dirname,
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[]
}
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

@ -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)
}

@ -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);
}}

@ -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 }],
{

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

@ -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({

@ -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;
}

@ -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