Merge branch 'hongpo/update-search' of https://github.com/yangshun/tech-interview-handbook into hongpo/update-search

pull/478/head
hpkoh 3 years ago
commit 695f03b5a9

@ -13054,20 +13054,6 @@
{ "city_id": "11164", "city_name": "Shucheng", "state_id": "731" },
{ "city_id": "11165", "city_name": "Xinchang", "state_id": "731" },
{ "city_id": "11166", "city_name": "Xuancheng", "state_id": "731" },
{ "city_id": "11167", "city_name": "Fengyang", "state_id": "732" },
{ "city_id": "11168", "city_name": "Guangde", "state_id": "732" },
{ "city_id": "11169", "city_name": "Liuan", "state_id": "732" },
{ "city_id": "11170", "city_name": "Ningguo", "state_id": "732" },
{ "city_id": "11171", "city_name": "Shucheng", "state_id": "732" },
{ "city_id": "11172", "city_name": "Xinchang", "state_id": "732" },
{ "city_id": "11173", "city_name": "Xuancheng", "state_id": "732" },
{ "city_id": "11174", "city_name": "Fengyang", "state_id": "733" },
{ "city_id": "11175", "city_name": "Guangde", "state_id": "733" },
{ "city_id": "11176", "city_name": "Liuan", "state_id": "733" },
{ "city_id": "11177", "city_name": "Ningguo", "state_id": "733" },
{ "city_id": "11178", "city_name": "Shucheng", "state_id": "733" },
{ "city_id": "11179", "city_name": "Xinchang", "state_id": "733" },
{ "city_id": "11180", "city_name": "Xuancheng", "state_id": "733" },
{ "city_id": "11181", "city_name": "Aomen", "state_id": "734" },
{ "city_id": "11182", "city_name": "Beijing", "state_id": "735" },
{ "city_id": "11183", "city_name": "Changping", "state_id": "735" },
@ -13077,14 +13063,6 @@
{ "city_id": "11187", "city_name": "Mentougou", "state_id": "735" },
{ "city_id": "11188", "city_name": "Shunyi", "state_id": "735" },
{ "city_id": "11189", "city_name": "Tongzhou", "state_id": "735" },
{ "city_id": "11190", "city_name": "Beijing", "state_id": "736" },
{ "city_id": "11191", "city_name": "Changping", "state_id": "736" },
{ "city_id": "11192", "city_name": "Fangshan", "state_id": "736" },
{ "city_id": "11193", "city_name": "Huangcun", "state_id": "736" },
{ "city_id": "11194", "city_name": "Liangxiang", "state_id": "736" },
{ "city_id": "11195", "city_name": "Mentougou", "state_id": "736" },
{ "city_id": "11196", "city_name": "Shunyi", "state_id": "736" },
{ "city_id": "11197", "city_name": "Tongzhou", "state_id": "736" },
{ "city_id": "11198", "city_name": "Beibei", "state_id": "737" },
{ "city_id": "11199", "city_name": "Chongqing", "state_id": "737" },
{ "city_id": "11200", "city_name": "Fuling", "state_id": "737" },
@ -13129,41 +13107,6 @@
{ "city_id": "11239", "city_name": "Yongan", "state_id": "738" },
{ "city_id": "11240", "city_name": "Zhangzhou", "state_id": "738" },
{ "city_id": "11241", "city_name": "Zhicheng", "state_id": "738" },
{ "city_id": "11242", "city_name": "Bantou", "state_id": "739" },
{ "city_id": "11243", "city_name": "Dongshan", "state_id": "739" },
{ "city_id": "11244", "city_name": "Fuan", "state_id": "739" },
{ "city_id": "11245", "city_name": "Fujian", "state_id": "739" },
{ "city_id": "11246", "city_name": "Fuqing", "state_id": "739" },
{ "city_id": "11247", "city_name": "Fuzhou", "state_id": "739" },
{ "city_id": "11248", "city_name": "Gantou", "state_id": "739" },
{ "city_id": "11249", "city_name": "Hanyang", "state_id": "739" },
{ "city_id": "11250", "city_name": "Jiangkou", "state_id": "739" },
{ "city_id": "11251", "city_name": "Jiaocheng", "state_id": "739" },
{ "city_id": "11252", "city_name": "Jinjiang", "state_id": "739" },
{ "city_id": "11253", "city_name": "Jinshang", "state_id": "739" },
{ "city_id": "11254", "city_name": "Longhai", "state_id": "739" },
{ "city_id": "11255", "city_name": "Longyan", "state_id": "739" },
{ "city_id": "11256", "city_name": "Luoyang", "state_id": "739" },
{ "city_id": "11257", "city_name": "Nanan", "state_id": "739" },
{ "city_id": "11258", "city_name": "Nanping", "state_id": "739" },
{ "city_id": "11259", "city_name": "Nanpu", "state_id": "739" },
{ "city_id": "11260", "city_name": "Putian", "state_id": "739" },
{ "city_id": "11261", "city_name": "Qingyang", "state_id": "739" },
{ "city_id": "11262", "city_name": "Quanzhou", "state_id": "739" },
{ "city_id": "11263", "city_name": "Rongcheng", "state_id": "739" },
{ "city_id": "11264", "city_name": "Sanming", "state_id": "739" },
{ "city_id": "11265", "city_name": "Shaowu", "state_id": "739" },
{ "city_id": "11266", "city_name": "Shima", "state_id": "739" },
{ "city_id": "11267", "city_name": "Shishi", "state_id": "739" },
{ "city_id": "11268", "city_name": "Tantou", "state_id": "739" },
{ "city_id": "11269", "city_name": "Tongshan", "state_id": "739" },
{ "city_id": "11270", "city_name": "Xiamen", "state_id": "739" },
{ "city_id": "11271", "city_name": "Xiapu", "state_id": "739" },
{ "city_id": "11272", "city_name": "Xiapu Ningde", "state_id": "739" },
{ "city_id": "11273", "city_name": "Ximei", "state_id": "739" },
{ "city_id": "11274", "city_name": "Yongan", "state_id": "739" },
{ "city_id": "11275", "city_name": "Zhangzhou", "state_id": "739" },
{ "city_id": "11276", "city_name": "Zhicheng", "state_id": "739" },
{ "city_id": "11277", "city_name": "Baiyin", "state_id": "740" },
{ "city_id": "11278", "city_name": "Baoji", "state_id": "740" },
{ "city_id": "11279", "city_name": "Beidao", "state_id": "740" },
@ -13265,92 +13208,6 @@
{ "city_id": "11375", "city_name": "Zhilong", "state_id": "741" },
{ "city_id": "11376", "city_name": "Zhongshan", "state_id": "741" },
{ "city_id": "11377", "city_name": "Zhuhai", "state_id": "741" },
{ "city_id": "11378", "city_name": "Anbu", "state_id": "742" },
{ "city_id": "11379", "city_name": "Chaozhou", "state_id": "742" },
{ "city_id": "11380", "city_name": "Chenghai", "state_id": "742" },
{ "city_id": "11381", "city_name": "Chuncheng", "state_id": "742" },
{ "city_id": "11382", "city_name": "Daliang", "state_id": "742" },
{ "city_id": "11383", "city_name": "Danshui", "state_id": "742" },
{ "city_id": "11384", "city_name": "Dongguan", "state_id": "742" },
{ "city_id": "11385", "city_name": "Donghai", "state_id": "742" },
{ "city_id": "11386", "city_name": "Dongli", "state_id": "742" },
{ "city_id": "11387", "city_name": "Dongzhen", "state_id": "742" },
{ "city_id": "11388", "city_name": "Ducheng", "state_id": "742" },
{ "city_id": "11389", "city_name": "Encheng", "state_id": "742" },
{ "city_id": "11390", "city_name": "Foahn", "state_id": "742" },
{ "city_id": "11391", "city_name": "Foshan", "state_id": "742" },
{ "city_id": "11392", "city_name": "Gaozhou", "state_id": "742" },
{ "city_id": "11393", "city_name": "Guangdong", "state_id": "742" },
{ "city_id": "11394", "city_name": "Guangzhou", "state_id": "742" },
{ "city_id": "11395", "city_name": "Guanjiao", "state_id": "742" },
{ "city_id": "11396", "city_name": "Haicheng", "state_id": "742" },
{ "city_id": "11397", "city_name": "Haimen", "state_id": "742" },
{ "city_id": "11398", "city_name": "Hepo", "state_id": "742" },
{ "city_id": "11399", "city_name": "Houpu", "state_id": "742" },
{ "city_id": "11400", "city_name": "Huaicheng", "state_id": "742" },
{ "city_id": "11401", "city_name": "Huanggang", "state_id": "742" },
{ "city_id": "11402", "city_name": "Huangpu", "state_id": "742" },
{ "city_id": "11403", "city_name": "Huazhou", "state_id": "742" },
{ "city_id": "11404", "city_name": "Huicheng", "state_id": "742" },
{ "city_id": "11405", "city_name": "Huizhou", "state_id": "742" },
{ "city_id": "11406", "city_name": "Humen", "state_id": "742" },
{ "city_id": "11407", "city_name": "Jiangmen", "state_id": "742" },
{ "city_id": "11408", "city_name": "Jiazi", "state_id": "742" },
{ "city_id": "11409", "city_name": "Jieshi", "state_id": "742" },
{ "city_id": "11410", "city_name": "Jieyang", "state_id": "742" },
{ "city_id": "11411", "city_name": "Lecheng", "state_id": "742" },
{ "city_id": "11412", "city_name": "Leicheng", "state_id": "742" },
{ "city_id": "11413", "city_name": "Liancheng", "state_id": "742" },
{ "city_id": "11414", "city_name": "Lianzhou", "state_id": "742" },
{ "city_id": "11415", "city_name": "Licheng", "state_id": "742" },
{ "city_id": "11416", "city_name": "Liusha", "state_id": "742" },
{ "city_id": "11417", "city_name": "Longgang", "state_id": "742" },
{ "city_id": "11418", "city_name": "Lubu", "state_id": "742" },
{ "city_id": "11419", "city_name": "Luocheng", "state_id": "742" },
{ "city_id": "11420", "city_name": "Luohu", "state_id": "742" },
{ "city_id": "11421", "city_name": "Luoyang", "state_id": "742" },
{ "city_id": "11422", "city_name": "Maba", "state_id": "742" },
{ "city_id": "11423", "city_name": "Maoming", "state_id": "742" },
{ "city_id": "11424", "city_name": "Mata", "state_id": "742" },
{ "city_id": "11425", "city_name": "Meilu", "state_id": "742" },
{ "city_id": "11426", "city_name": "Meizhou", "state_id": "742" },
{ "city_id": "11427", "city_name": "Mianchang", "state_id": "742" },
{ "city_id": "11428", "city_name": "Nanfeng", "state_id": "742" },
{ "city_id": "11429", "city_name": "Nanhai", "state_id": "742" },
{ "city_id": "11430", "city_name": "Pingshan", "state_id": "742" },
{ "city_id": "11431", "city_name": "Qingtang", "state_id": "742" },
{ "city_id": "11432", "city_name": "Qingyuan", "state_id": "742" },
{ "city_id": "11433", "city_name": "Rongcheng", "state_id": "742" },
{ "city_id": "11434", "city_name": "Sanbu", "state_id": "742" },
{ "city_id": "11435", "city_name": "Shantou", "state_id": "742" },
{ "city_id": "11436", "city_name": "Shanwei", "state_id": "742" },
{ "city_id": "11437", "city_name": "Shaoguan", "state_id": "742" },
{ "city_id": "11438", "city_name": "Shaping", "state_id": "742" },
{ "city_id": "11439", "city_name": "Shenzhen", "state_id": "742" },
{ "city_id": "11440", "city_name": "Shilong", "state_id": "742" },
{ "city_id": "11441", "city_name": "Shiqiao", "state_id": "742" },
{ "city_id": "11442", "city_name": "Shiwan", "state_id": "742" },
{ "city_id": "11443", "city_name": "Shuizhai", "state_id": "742" },
{ "city_id": "11444", "city_name": "Shunde", "state_id": "742" },
{ "city_id": "11445", "city_name": "Suicheng", "state_id": "742" },
{ "city_id": "11446", "city_name": "Taicheng", "state_id": "742" },
{ "city_id": "11447", "city_name": "Tangping", "state_id": "742" },
{ "city_id": "11448", "city_name": "Xiaolan", "state_id": "742" },
{ "city_id": "11449", "city_name": "Xinan", "state_id": "742" },
{ "city_id": "11450", "city_name": "Xingcheng", "state_id": "742" },
{ "city_id": "11451", "city_name": "Xiongzhou", "state_id": "742" },
{ "city_id": "11452", "city_name": "Xucheng", "state_id": "742" },
{ "city_id": "11453", "city_name": "Yangjiang", "state_id": "742" },
{ "city_id": "11454", "city_name": "Yingcheng", "state_id": "742" },
{ "city_id": "11455", "city_name": "Yuancheng", "state_id": "742" },
{ "city_id": "11456", "city_name": "Yuncheng", "state_id": "742" },
{ "city_id": "11457", "city_name": "Yunfu", "state_id": "742" },
{ "city_id": "11458", "city_name": "Zengcheng", "state_id": "742" },
{ "city_id": "11459", "city_name": "Zhanjiang", "state_id": "742" },
{ "city_id": "11460", "city_name": "Zhaoqing", "state_id": "742" },
{ "city_id": "11461", "city_name": "Zhilong", "state_id": "742" },
{ "city_id": "11462", "city_name": "Zhongshan", "state_id": "742" },
{ "city_id": "11463", "city_name": "Zhuhai", "state_id": "742" },
{ "city_id": "11464", "city_name": "Babu", "state_id": "743" },
{ "city_id": "11465", "city_name": "Baihe", "state_id": "743" },
{ "city_id": "11466", "city_name": "Baise", "state_id": "743" },
@ -13727,75 +13584,6 @@
{ "city_id": "11837", "city_name": "Zhaoyang", "state_id": "751" },
{ "city_id": "11838", "city_name": "Zhenjiang", "state_id": "751" },
{ "city_id": "11839", "city_name": "Zhongxing", "state_id": "751" },
{ "city_id": "11840", "city_name": "Baoying", "state_id": "752" },
{ "city_id": "11841", "city_name": "Changzhou", "state_id": "752" },
{ "city_id": "11842", "city_name": "Dachang", "state_id": "752" },
{ "city_id": "11843", "city_name": "Dafeng", "state_id": "752" },
{ "city_id": "11844", "city_name": "Danyang", "state_id": "752" },
{ "city_id": "11845", "city_name": "Dingshu", "state_id": "752" },
{ "city_id": "11846", "city_name": "Dongkan", "state_id": "752" },
{ "city_id": "11847", "city_name": "Dongtai", "state_id": "752" },
{ "city_id": "11848", "city_name": "Fengxian", "state_id": "752" },
{ "city_id": "11849", "city_name": "Gaogou", "state_id": "752" },
{ "city_id": "11850", "city_name": "Gaoyou", "state_id": "752" },
{ "city_id": "11851", "city_name": "Guiren", "state_id": "752" },
{ "city_id": "11852", "city_name": "Haian", "state_id": "752" },
{ "city_id": "11853", "city_name": "Haizhou", "state_id": "752" },
{ "city_id": "11854", "city_name": "Hede", "state_id": "752" },
{ "city_id": "11855", "city_name": "Huaicheng", "state_id": "752" },
{ "city_id": "11856", "city_name": "Huaiyin", "state_id": "752" },
{ "city_id": "11857", "city_name": "Huilong", "state_id": "752" },
{ "city_id": "11858", "city_name": "Hutang", "state_id": "752" },
{ "city_id": "11859", "city_name": "Jiangdu", "state_id": "752" },
{ "city_id": "11860", "city_name": "Jiangyan", "state_id": "752" },
{ "city_id": "11861", "city_name": "Jiangyin", "state_id": "752" },
{ "city_id": "11862", "city_name": "Jiangyuan", "state_id": "752" },
{ "city_id": "11863", "city_name": "Jianhu", "state_id": "752" },
{ "city_id": "11864", "city_name": "Jingcheng", "state_id": "752" },
{ "city_id": "11865", "city_name": "Jinsha", "state_id": "752" },
{ "city_id": "11866", "city_name": "Jintan", "state_id": "752" },
{ "city_id": "11867", "city_name": "Juegang", "state_id": "752" },
{ "city_id": "11868", "city_name": "Jurong", "state_id": "752" },
{ "city_id": "11869", "city_name": "Kunshan", "state_id": "752" },
{ "city_id": "11870", "city_name": "Lianyungang", "state_id": "752" },
{ "city_id": "11871", "city_name": "Liucheng", "state_id": "752" },
{ "city_id": "11872", "city_name": "Liyang", "state_id": "752" },
{ "city_id": "11873", "city_name": "Luodu", "state_id": "752" },
{ "city_id": "11874", "city_name": "Mudu", "state_id": "752" },
{ "city_id": "11875", "city_name": "Nanjing", "state_id": "752" },
{ "city_id": "11876", "city_name": "Nantong", "state_id": "752" },
{ "city_id": "11877", "city_name": "Pecheng", "state_id": "752" },
{ "city_id": "11878", "city_name": "Pukou", "state_id": "752" },
{ "city_id": "11879", "city_name": "Qidong", "state_id": "752" },
{ "city_id": "11880", "city_name": "Qinnan", "state_id": "752" },
{ "city_id": "11881", "city_name": "Qixia", "state_id": "752" },
{ "city_id": "11882", "city_name": "Rucheng", "state_id": "752" },
{ "city_id": "11883", "city_name": "Songling", "state_id": "752" },
{ "city_id": "11884", "city_name": "Sucheng", "state_id": "752" },
{ "city_id": "11885", "city_name": "Suicheng", "state_id": "752" },
{ "city_id": "11886", "city_name": "Suqian", "state_id": "752" },
{ "city_id": "11887", "city_name": "Suzhou", "state_id": "752" },
{ "city_id": "11888", "city_name": "Taicang", "state_id": "752" },
{ "city_id": "11889", "city_name": "Taixing", "state_id": "752" },
{ "city_id": "11890", "city_name": "Wujiang", "state_id": "752" },
{ "city_id": "11891", "city_name": "Wuxi", "state_id": "752" },
{ "city_id": "11892", "city_name": "Xiaolingwei", "state_id": "752" },
{ "city_id": "11893", "city_name": "Xiaoshi", "state_id": "752" },
{ "city_id": "11894", "city_name": "Xinan", "state_id": "752" },
{ "city_id": "11895", "city_name": "Xinpu", "state_id": "752" },
{ "city_id": "11896", "city_name": "Xuzhou", "state_id": "752" },
{ "city_id": "11897", "city_name": "Yancheng", "state_id": "752" },
{ "city_id": "11898", "city_name": "Yangshe", "state_id": "752" },
{ "city_id": "11899", "city_name": "Yangzhou", "state_id": "752" },
{ "city_id": "11900", "city_name": "Yizheng", "state_id": "752" },
{ "city_id": "11901", "city_name": "Yunhe", "state_id": "752" },
{ "city_id": "11902", "city_name": "Yunyang", "state_id": "752" },
{ "city_id": "11903", "city_name": "Yushan", "state_id": "752" },
{ "city_id": "11904", "city_name": "Zhangjiagang", "state_id": "752" },
{ "city_id": "11905", "city_name": "Zhangjiangang", "state_id": "752" },
{ "city_id": "11906", "city_name": "Zhaoyang", "state_id": "752" },
{ "city_id": "11907", "city_name": "Zhenjiang", "state_id": "752" },
{ "city_id": "11908", "city_name": "Zhongxing", "state_id": "752" },
{ "city_id": "11909", "city_name": "Fengxin", "state_id": "753" },
{ "city_id": "11910", "city_name": "Fenyi", "state_id": "753" },
{ "city_id": "11911", "city_name": "Ganzhou", "state_id": "753" },
@ -13920,54 +13708,6 @@
{ "city_id": "12030", "city_name": "Yingkou", "state_id": "755" },
{ "city_id": "12031", "city_name": "Yuhong", "state_id": "755" },
{ "city_id": "12032", "city_name": "Zhuanghe", "state_id": "755" },
{ "city_id": "12033", "city_name": "Anshan", "state_id": "756" },
{ "city_id": "12034", "city_name": "Beipiao", "state_id": "756" },
{ "city_id": "12035", "city_name": "Benxi", "state_id": "756" },
{ "city_id": "12036", "city_name": "Changtu", "state_id": "756" },
{ "city_id": "12037", "city_name": "Chaoyang", "state_id": "756" },
{ "city_id": "12038", "city_name": "Dalian", "state_id": "756" },
{ "city_id": "12039", "city_name": "Dalianwan", "state_id": "756" },
{ "city_id": "12040", "city_name": "Dalinghe", "state_id": "756" },
{ "city_id": "12041", "city_name": "Dandong", "state_id": "756" },
{ "city_id": "12042", "city_name": "Dashiqiao", "state_id": "756" },
{ "city_id": "12043", "city_name": "Dongling", "state_id": "756" },
{ "city_id": "12044", "city_name": "Fengcheng", "state_id": "756" },
{ "city_id": "12045", "city_name": "Fushun", "state_id": "756" },
{ "city_id": "12046", "city_name": "Fuxin", "state_id": "756" },
{ "city_id": "12047", "city_name": "Haicheng", "state_id": "756" },
{ "city_id": "12048", "city_name": "Heishan", "state_id": "756" },
{ "city_id": "12049", "city_name": "Huanren", "state_id": "756" },
{ "city_id": "12050", "city_name": "Huludao", "state_id": "756" },
{ "city_id": "12051", "city_name": "Hushitai", "state_id": "756" },
{ "city_id": "12052", "city_name": "Jinxi", "state_id": "756" },
{ "city_id": "12053", "city_name": "Jinzhou", "state_id": "756" },
{ "city_id": "12054", "city_name": "Jiupu", "state_id": "756" },
{ "city_id": "12055", "city_name": "Kaiyuan", "state_id": "756" },
{ "city_id": "12056", "city_name": "Kuandian", "state_id": "756" },
{ "city_id": "12057", "city_name": "Langtou", "state_id": "756" },
{ "city_id": "12058", "city_name": "Liaoyang", "state_id": "756" },
{ "city_id": "12059", "city_name": "Liaozhong", "state_id": "756" },
{ "city_id": "12060", "city_name": "Lingyuan", "state_id": "756" },
{ "city_id": "12061", "city_name": "Liuerbao", "state_id": "756" },
{ "city_id": "12062", "city_name": "Lushunkou", "state_id": "756" },
{ "city_id": "12063", "city_name": "Nantai", "state_id": "756" },
{ "city_id": "12064", "city_name": "Panjin", "state_id": "756" },
{ "city_id": "12065", "city_name": "Pulandian", "state_id": "756" },
{ "city_id": "12066", "city_name": "Shenyang", "state_id": "756" },
{ "city_id": "12067", "city_name": "Sujiatun", "state_id": "756" },
{ "city_id": "12068", "city_name": "Tieling", "state_id": "756" },
{ "city_id": "12069", "city_name": "Wafangdian", "state_id": "756" },
{ "city_id": "12070", "city_name": "Xiaoshi", "state_id": "756" },
{ "city_id": "12071", "city_name": "Xifeng", "state_id": "756" },
{ "city_id": "12072", "city_name": "Xinchengxi", "state_id": "756" },
{ "city_id": "12073", "city_name": "Xingcheng", "state_id": "756" },
{ "city_id": "12074", "city_name": "Xinmin", "state_id": "756" },
{ "city_id": "12075", "city_name": "Xiongyue", "state_id": "756" },
{ "city_id": "12076", "city_name": "Xiuyan", "state_id": "756" },
{ "city_id": "12077", "city_name": "Yebaishou", "state_id": "756" },
{ "city_id": "12078", "city_name": "Yingkou", "state_id": "756" },
{ "city_id": "12079", "city_name": "Yuhong", "state_id": "756" },
{ "city_id": "12080", "city_name": "Zhuanghe", "state_id": "756" },
{ "city_id": "12081", "city_name": "Qiatou", "state_id": "759" },
{ "city_id": "12082", "city_name": "Xining", "state_id": "759" },
{ "city_id": "12083", "city_name": "Ankang", "state_id": "760" },
@ -14087,108 +13827,6 @@
{ "city_id": "12197", "city_name": "Zicheng", "state_id": "761" },
{ "city_id": "12198", "city_name": "Zouping", "state_id": "761" },
{ "city_id": "12199", "city_name": "Zouxian", "state_id": "761" },
{ "city_id": "12200", "city_name": "Anqiu", "state_id": "762" },
{ "city_id": "12201", "city_name": "Bianzhuang", "state_id": "762" },
{ "city_id": "12202", "city_name": "Binzhou", "state_id": "762" },
{ "city_id": "12203", "city_name": "Boshan", "state_id": "762" },
{ "city_id": "12204", "city_name": "Boxing County", "state_id": "762" },
{ "city_id": "12205", "city_name": "Caocheng", "state_id": "762" },
{ "city_id": "12206", "city_name": "Changqing", "state_id": "762" },
{ "city_id": "12207", "city_name": "Chengyang", "state_id": "762" },
{ "city_id": "12208", "city_name": "Dengzhou", "state_id": "762" },
{ "city_id": "12209", "city_name": "Dezhou", "state_id": "762" },
{ "city_id": "12210", "city_name": "Dingtao", "state_id": "762" },
{ "city_id": "12211", "city_name": "Dongcun", "state_id": "762" },
{ "city_id": "12212", "city_name": "Dongdu", "state_id": "762" },
{ "city_id": "12213", "city_name": "Donge County", "state_id": "762" },
{ "city_id": "12214", "city_name": "Dongying", "state_id": "762" },
{ "city_id": "12215", "city_name": "Feicheng", "state_id": "762" },
{ "city_id": "12216", "city_name": "Fushan", "state_id": "762" },
{ "city_id": "12217", "city_name": "Gaomi", "state_id": "762" },
{ "city_id": "12218", "city_name": "Haiyang", "state_id": "762" },
{ "city_id": "12219", "city_name": "Hanting", "state_id": "762" },
{ "city_id": "12220", "city_name": "Hekou", "state_id": "762" },
{ "city_id": "12221", "city_name": "Heze", "state_id": "762" },
{ "city_id": "12222", "city_name": "Jiaonan", "state_id": "762" },
{ "city_id": "12223", "city_name": "Jiaozhou", "state_id": "762" },
{ "city_id": "12224", "city_name": "Jiehu", "state_id": "762" },
{ "city_id": "12225", "city_name": "Jimo", "state_id": "762" },
{ "city_id": "12226", "city_name": "Jinan", "state_id": "762" },
{ "city_id": "12227", "city_name": "Jining", "state_id": "762" },
{ "city_id": "12228", "city_name": "Juxian", "state_id": "762" },
{ "city_id": "12229", "city_name": "Juye", "state_id": "762" },
{ "city_id": "12230", "city_name": "Kunlun", "state_id": "762" },
{ "city_id": "12231", "city_name": "Laiwu", "state_id": "762" },
{ "city_id": "12232", "city_name": "Laiyang", "state_id": "762" },
{ "city_id": "12233", "city_name": "Laizhou", "state_id": "762" },
{ "city_id": "12234", "city_name": "Leling", "state_id": "762" },
{ "city_id": "12235", "city_name": "Liaocheng", "state_id": "762" },
{ "city_id": "12236", "city_name": "Licung", "state_id": "762" },
{ "city_id": "12237", "city_name": "Linqing", "state_id": "762" },
{ "city_id": "12238", "city_name": "Linqu", "state_id": "762" },
{ "city_id": "12239", "city_name": "Linshu", "state_id": "762" },
{ "city_id": "12240", "city_name": "Linyi", "state_id": "762" },
{ "city_id": "12241", "city_name": "Longkou", "state_id": "762" },
{ "city_id": "12242", "city_name": "Mengyin", "state_id": "762" },
{ "city_id": "12243", "city_name": "Mingshui", "state_id": "762" },
{ "city_id": "12244", "city_name": "Nanchou", "state_id": "762" },
{ "city_id": "12245", "city_name": "Nanding", "state_id": "762" },
{ "city_id": "12246", "city_name": "Nanma", "state_id": "762" },
{ "city_id": "12247", "city_name": "Ninghai", "state_id": "762" },
{ "city_id": "12248", "city_name": "Ningyang", "state_id": "762" },
{ "city_id": "12249", "city_name": "Pingdu", "state_id": "762" },
{ "city_id": "12250", "city_name": "Pingyi", "state_id": "762" },
{ "city_id": "12251", "city_name": "Pingyin", "state_id": "762" },
{ "city_id": "12252", "city_name": "Qingdao", "state_id": "762" },
{ "city_id": "12253", "city_name": "Qingzhou", "state_id": "762" },
{ "city_id": "12254", "city_name": "Qixia", "state_id": "762" },
{ "city_id": "12255", "city_name": "Qufu", "state_id": "762" },
{ "city_id": "12256", "city_name": "Rizhao", "state_id": "762" },
{ "city_id": "12257", "city_name": "Rongcheng", "state_id": "762" },
{ "city_id": "12258", "city_name": "Shancheng", "state_id": "762" },
{ "city_id": "12259", "city_name": "Shanting", "state_id": "762" },
{ "city_id": "12260", "city_name": "Shengzhuang", "state_id": "762" },
{ "city_id": "12261", "city_name": "Shenxian", "state_id": "762" },
{ "city_id": "12262", "city_name": "Shizilu", "state_id": "762" },
{ "city_id": "12263", "city_name": "Shouguang", "state_id": "762" },
{ "city_id": "12264", "city_name": "Shuiji", "state_id": "762" },
{ "city_id": "12265", "city_name": "Sishui", "state_id": "762" },
{ "city_id": "12266", "city_name": "Suozhen", "state_id": "762" },
{ "city_id": "12267", "city_name": "Taian", "state_id": "762" },
{ "city_id": "12268", "city_name": "Tancheng", "state_id": "762" },
{ "city_id": "12269", "city_name": "Taozhuang", "state_id": "762" },
{ "city_id": "12270", "city_name": "Tengzhou", "state_id": "762" },
{ "city_id": "12271", "city_name": "Weifang", "state_id": "762" },
{ "city_id": "12272", "city_name": "Weihai", "state_id": "762" },
{ "city_id": "12273", "city_name": "Wencheng", "state_id": "762" },
{ "city_id": "12274", "city_name": "Wendeng", "state_id": "762" },
{ "city_id": "12275", "city_name": "Wenshang", "state_id": "762" },
{ "city_id": "12276", "city_name": "Wudi", "state_id": "762" },
{ "city_id": "12277", "city_name": "Xiazhen", "state_id": "762" },
{ "city_id": "12278", "city_name": "Xincheng", "state_id": "762" },
{ "city_id": "12279", "city_name": "Xindian", "state_id": "762" },
{ "city_id": "12280", "city_name": "Xintai", "state_id": "762" },
{ "city_id": "12281", "city_name": "Yanggu", "state_id": "762" },
{ "city_id": "12282", "city_name": "Yangshan", "state_id": "762" },
{ "city_id": "12283", "city_name": "Yantai", "state_id": "762" },
{ "city_id": "12284", "city_name": "Yanzhou", "state_id": "762" },
{ "city_id": "12285", "city_name": "Yatou", "state_id": "762" },
{ "city_id": "12286", "city_name": "Yidu", "state_id": "762" },
{ "city_id": "12287", "city_name": "Yishui", "state_id": "762" },
{ "city_id": "12288", "city_name": "Yucheng", "state_id": "762" },
{ "city_id": "12289", "city_name": "Yuncheng", "state_id": "762" },
{ "city_id": "12290", "city_name": "Zaozhuang", "state_id": "762" },
{ "city_id": "12291", "city_name": "Zhangdian", "state_id": "762" },
{ "city_id": "12292", "city_name": "Zhangjiawa", "state_id": "762" },
{ "city_id": "12293", "city_name": "Zhangqiu", "state_id": "762" },
{ "city_id": "12294", "city_name": "Zhaocheng", "state_id": "762" },
{ "city_id": "12295", "city_name": "Zhoucheng", "state_id": "762" },
{ "city_id": "12296", "city_name": "Zhoucun", "state_id": "762" },
{ "city_id": "12297", "city_name": "Zhucheng", "state_id": "762" },
{ "city_id": "12298", "city_name": "Zhuwang", "state_id": "762" },
{ "city_id": "12299", "city_name": "Zicheng", "state_id": "762" },
{ "city_id": "12300", "city_name": "Zouping", "state_id": "762" },
{ "city_id": "12301", "city_name": "Zouxian", "state_id": "762" },
{ "city_id": "12302", "city_name": "Jiading", "state_id": "763" },
{ "city_id": "12303", "city_name": "Minhang", "state_id": "763" },
{ "city_id": "12304", "city_name": "Shanghai", "state_id": "763" },
@ -14363,61 +14001,6 @@
{ "city_id": "12473", "city_name": "Zhuji", "state_id": "771" },
{ "city_id": "12474", "city_name": "fenghua", "state_id": "771" },
{ "city_id": "12475", "city_name": "jiashan", "state_id": "771" },
{ "city_id": "12476", "city_name": "Aojiang", "state_id": "772" },
{ "city_id": "12477", "city_name": "Choucheng", "state_id": "772" },
{ "city_id": "12478", "city_name": "Cixi", "state_id": "772" },
{ "city_id": "12479", "city_name": "Daqiao", "state_id": "772" },
{ "city_id": "12480", "city_name": "Deqing", "state_id": "772" },
{ "city_id": "12481", "city_name": "Dinghai", "state_id": "772" },
{ "city_id": "12482", "city_name": "Dongyang", "state_id": "772" },
{ "city_id": "12483", "city_name": "Fuyang", "state_id": "772" },
{ "city_id": "12484", "city_name": "Haining", "state_id": "772" },
{ "city_id": "12485", "city_name": "Haiyan", "state_id": "772" },
{ "city_id": "12486", "city_name": "Hangzhou", "state_id": "772" },
{ "city_id": "12487", "city_name": "Huangyan", "state_id": "772" },
{ "city_id": "12488", "city_name": "Hushan", "state_id": "772" },
{ "city_id": "12489", "city_name": "Huzhou", "state_id": "772" },
{ "city_id": "12490", "city_name": "Jiaojiang", "state_id": "772" },
{ "city_id": "12491", "city_name": "Jiaxing", "state_id": "772" },
{ "city_id": "12492", "city_name": "Jinhua", "state_id": "772" },
{ "city_id": "12493", "city_name": "Jinxiang", "state_id": "772" },
{ "city_id": "12494", "city_name": "Kaihua", "state_id": "772" },
{ "city_id": "12495", "city_name": "Kunyang", "state_id": "772" },
{ "city_id": "12496", "city_name": "Lanxi", "state_id": "772" },
{ "city_id": "12497", "city_name": "Linan City", "state_id": "772" },
{ "city_id": "12498", "city_name": "Linhai", "state_id": "772" },
{ "city_id": "12499", "city_name": "Linping", "state_id": "772" },
{ "city_id": "12500", "city_name": "Lishui", "state_id": "772" },
{ "city_id": "12501", "city_name": "Liushi", "state_id": "772" },
{ "city_id": "12502", "city_name": "Ningbo", "state_id": "772" },
{ "city_id": "12503", "city_name": "Ninghai", "state_id": "772" },
{ "city_id": "12504", "city_name": "Pinghu", "state_id": "772" },
{ "city_id": "12505", "city_name": "Quzhou", "state_id": "772" },
{ "city_id": "12506", "city_name": "Ruian", "state_id": "772" },
{ "city_id": "12507", "city_name": "Shangyu", "state_id": "772" },
{ "city_id": "12508", "city_name": "Shaoxing", "state_id": "772" },
{ "city_id": "12509", "city_name": "Shenjiamen", "state_id": "772" },
{ "city_id": "12510", "city_name": "Taizhou City", "state_id": "772" },
{ "city_id": "12511", "city_name": "Tonglu", "state_id": "772" },
{ "city_id": "12512", "city_name": "Wenling", "state_id": "772" },
{ "city_id": "12513", "city_name": "Wenzhou", "state_id": "772" },
{ "city_id": "12514", "city_name": "Wuning", "state_id": "772" },
{ "city_id": "12515", "city_name": "Wuyi", "state_id": "772" },
{ "city_id": "12516", "city_name": "Xianju", "state_id": "772" },
{ "city_id": "12517", "city_name": "Xiaoshan", "state_id": "772" },
{ "city_id": "12518", "city_name": "Xiashi", "state_id": "772" },
{ "city_id": "12519", "city_name": "Xushan", "state_id": "772" },
{ "city_id": "12520", "city_name": "Yiwu", "state_id": "772" },
{ "city_id": "12521", "city_name": "Yongkang", "state_id": "772" },
{ "city_id": "12522", "city_name": "Yueqing", "state_id": "772" },
{ "city_id": "12523", "city_name": "Yuhuan", "state_id": "772" },
{ "city_id": "12524", "city_name": "Yuyao", "state_id": "772" },
{ "city_id": "12525", "city_name": "Zhejiang", "state_id": "772" },
{ "city_id": "12526", "city_name": "Zhenhai", "state_id": "772" },
{ "city_id": "12527", "city_name": "Zhicheng", "state_id": "772" },
{ "city_id": "12528", "city_name": "Zhuji", "state_id": "772" },
{ "city_id": "12529", "city_name": "fenghua", "state_id": "772" },
{ "city_id": "12530", "city_name": "jiashan", "state_id": "772" },
{ "city_id": "12531", "city_name": "Leticia", "state_id": "775" },
{ "city_id": "12532", "city_name": "Puerto Narino", "state_id": "775" },
{ "city_id": "12533", "city_name": "Abejorral", "state_id": "776" },
@ -59007,7 +58590,6 @@
{ "city_id": "46278", "city_name": "Atascocita", "state_id": "3970" },
{ "city_id": "46279", "city_name": "Athens", "state_id": "3970" },
{ "city_id": "46280", "city_name": "Austin", "state_id": "3970" },
{ "city_id": "46281", "city_name": "Austinn", "state_id": "3970" },
{ "city_id": "46282", "city_name": "Azle", "state_id": "3970" },
{ "city_id": "46283", "city_name": "Balch Springs", "state_id": "3970" },
{ "city_id": "46284", "city_name": "Barry", "state_id": "3970" },
@ -59306,7 +58888,6 @@
{ "city_id": "46537", "city_name": "Woodway", "state_id": "3970" },
{ "city_id": "46538", "city_name": "Wylie", "state_id": "3970" },
{ "city_id": "46539", "city_name": "Yoakum", "state_id": "3970" },
{ "city_id": "46540", "city_name": "austinn", "state_id": "3970" },
{
"city_id": "46541",
"city_name": "Bedford Kentucky",

@ -903,17 +903,12 @@
{ "state_id": "729", "state_name": "Tarapaca", "country_id": "43" },
{ "state_id": "730", "state_name": "Valparaiso", "country_id": "43" },
{ "state_id": "731", "state_name": "Anhui", "country_id": "44" },
{ "state_id": "732", "state_name": "Anhui Province", "country_id": "44" },
{ "state_id": "733", "state_name": "Anhui Sheng", "country_id": "44" },
{ "state_id": "734", "state_name": "Aomen", "country_id": "44" },
{ "state_id": "735", "state_name": "Beijing", "country_id": "44" },
{ "state_id": "736", "state_name": "Beijing Shi", "country_id": "44" },
{ "state_id": "737", "state_name": "Chongqing", "country_id": "44" },
{ "state_id": "738", "state_name": "Fujian", "country_id": "44" },
{ "state_id": "739", "state_name": "Fujian Sheng", "country_id": "44" },
{ "state_id": "740", "state_name": "Gansu", "country_id": "44" },
{ "state_id": "741", "state_name": "Guangdong", "country_id": "44" },
{ "state_id": "742", "state_name": "Guangdong Sheng", "country_id": "44" },
{ "state_id": "743", "state_name": "Guangxi", "country_id": "44" },
{ "state_id": "744", "state_name": "Guizhou", "country_id": "44" },
{ "state_id": "745", "state_name": "Hainan", "country_id": "44" },
@ -923,17 +918,14 @@
{ "state_id": "749", "state_name": "Hubei", "country_id": "44" },
{ "state_id": "750", "state_name": "Hunan", "country_id": "44" },
{ "state_id": "751", "state_name": "Jiangsu", "country_id": "44" },
{ "state_id": "752", "state_name": "Jiangsu Sheng", "country_id": "44" },
{ "state_id": "753", "state_name": "Jiangxi", "country_id": "44" },
{ "state_id": "754", "state_name": "Jilin", "country_id": "44" },
{ "state_id": "755", "state_name": "Liaoning", "country_id": "44" },
{ "state_id": "756", "state_name": "Liaoning Sheng", "country_id": "44" },
{ "state_id": "757", "state_name": "Nei Monggol", "country_id": "44" },
{ "state_id": "758", "state_name": "Ningxia Hui", "country_id": "44" },
{ "state_id": "759", "state_name": "Qinghai", "country_id": "44" },
{ "state_id": "760", "state_name": "Shaanxi", "country_id": "44" },
{ "state_id": "761", "state_name": "Shandong", "country_id": "44" },
{ "state_id": "762", "state_name": "Shandong Sheng", "country_id": "44" },
{ "state_id": "763", "state_name": "Shanghai", "country_id": "44" },
{ "state_id": "764", "state_name": "Shanxi", "country_id": "44" },
{ "state_id": "765", "state_name": "Sichuan", "country_id": "44" },
@ -943,7 +935,6 @@
{ "state_id": "769", "state_name": "Xizang", "country_id": "44" },
{ "state_id": "770", "state_name": "Yunnan", "country_id": "44" },
{ "state_id": "771", "state_name": "Zhejiang", "country_id": "44" },
{ "state_id": "772", "state_name": "Zhejiang Sheng", "country_id": "44" },
{ "state_id": "773", "state_name": "Christmas Island", "country_id": "45" },
{
"state_id": "774",

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "QuestionsQuestion" ADD COLUMN "numEncounters" INTEGER NOT NULL DEFAULT 0;
-- CreateIndex
CREATE INDEX "QuestionsQuestion_numEncounters_id_idx" ON "QuestionsQuestion"("numEncounters", "id");

@ -438,14 +438,15 @@ enum QuestionsQuestionType {
}
model QuestionsQuestion {
id String @id @default(cuid())
userId String?
content String @db.Text
questionType QuestionsQuestionType
lastSeenAt DateTime?
upvotes Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
userId String?
content String @db.Text
questionType QuestionsQuestionType
lastSeenAt DateTime?
upvotes Int @default(0)
numEncounters Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
encounters QuestionsQuestionEncounter[]
@ -455,6 +456,7 @@ model QuestionsQuestion {
questionsListQuestionEntries QuestionsListQuestionEntry[]
@@index([lastSeenAt, id])
@@index([numEncounters, id])
@@index([upvotes, id])
}

@ -1,7 +1,7 @@
import clsx from 'clsx';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { signIn, signOut, useSession } from 'next-auth/react';
import { signOut, useSession } from 'next-auth/react';
import type { ReactNode } from 'react';
import { Fragment, useState } from 'react';
import { Menu, Transition } from '@headlessui/react';
@ -19,12 +19,14 @@ import GoogleAnalytics from './GoogleAnalytics';
import MobileNavigation from './MobileNavigation';
import type { ProductNavigationItems } from './ProductNavigation';
import ProductNavigation from './ProductNavigation';
import loginPageHref from '../shared/loginPageHref';
type Props = Readonly<{
children: ReactNode;
}>;
function ProfileJewel() {
const router = useRouter();
const { data: session, status } = useSession();
const isSessionLoading = status === 'loading';
@ -32,25 +34,20 @@ function ProfileJewel() {
return null;
}
const loginHref = loginPageHref();
if (session == null) {
return (
<Link
className="text-base"
href="/api/auth/signin"
onClick={(event) => {
event.preventDefault();
signIn();
}}>
Sign in
return router.pathname !== loginHref.pathname ? (
<Link className="text-base" href={loginHref}>
Log In
</Link>
);
) : null;
}
const userNavigation = [
{ href: '/profile', name: 'Profile' },
{
href: '/api/auth/signout',
name: 'Sign out',
name: 'Log out',
onClick: (event: MouseEvent) => {
event.preventDefault();
signOut();

@ -1,22 +1,21 @@
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
const navigation: ProductNavigationItems = [
{ href: '/offers', name: 'Offers' },
{ href: '/questions', name: 'Question Bank' },
{
children: [
{ href: '/resumes', name: 'View Resumes' },
{ href: '/resumes/submit', name: 'Submit Resume' },
],
href: '#',
name: 'Resumes',
},
];
// Not using this for now.
// const navigation: ProductNavigationItems = [
// { href: '/offers', name: 'Offers' },
// { href: '/questions', name: 'Question Bank' },
// {
// children: [
// { href: '/resumes', name: 'View Resumes' },
// { href: '/resumes/submit', name: 'Submit Resume' },
// ],
// href: '#',
// name: 'Resumes',
// },
// ];
const config = {
googleAnalyticsMeasurementID: 'G-DBLZDQ2ZZN',
navigation,
showGlobalNav: true,
navigation: [],
showGlobalNav: false,
title: 'Tech Interview Handbook',
titleHref: '/',
};

@ -35,14 +35,16 @@ export default function ProductNavigation({
<Link
className="hover:text-primary-700 flex items-center gap-2 text-base font-medium"
href={titleHref}>
{titleHref !== '/' &&
(logo ?? (
<img
alt="Tech Interview Handbook"
className="h-8 w-auto"
src="/logo.svg"
/>
))}
<div>
{titleHref !== '/' &&
(logo ?? (
<img
alt="Tech Interview Handbook"
className="h-8 w-auto"
src="/logo.svg"
/>
))}
</div>
{title}
</Link>
<div className="hidden h-full items-center space-x-8 md:flex">

@ -1,5 +1,4 @@
// Import { useState } from 'react';
// import { setTimeout } from 'timers';
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
import { BookmarkSquareIcon, CheckIcon } from '@heroicons/react/24/outline';
@ -22,17 +21,23 @@ export default function OffersProfileSave({
const { showToast } = useToast();
const { event: gaEvent } = useGoogleAnalytics();
const [isSaved, setSaved] = useState(false);
const { data: session, status } = useSession();
const saveMutation = trpc.useMutation(
['offers.user.profile.addToUserProfile'],
{
onError: () => {
showToast({
subtitle: 'Please check that you are logged in.',
title: `Failed to saved to dashboard!`,
variant: 'failure',
});
},
onSuccess: () => {
trpcContext.invalidateQueries([
'offers.profile.isSaved',
{ profileId, userId: session?.user?.id },
]);
showToast({
title: `Saved to your dashboard!`,
variant: 'success',
@ -41,17 +46,30 @@ export default function OffersProfileSave({
},
);
const isSavedQuery = trpc.useQuery(
[`offers.profile.isSaved`, { profileId, userId: session?.user?.id }],
{
onSuccess: (res) => {
setSaved(res);
},
},
);
const trpcContext = trpc.useContext();
const handleSave = () => {
saveMutation.mutate({
profileId,
token: token as string,
});
setSaved(true);
gaEvent({
action: 'offers.profile_submission_save_to_profile',
category: 'engagement',
label: 'Save to profile in profile submission',
});
if (status === 'unauthenticated') {
signIn();
} else {
saveMutation.mutate({
profileId,
token: token as string,
});
gaEvent({
action: 'offers.profile_submission_save_to_profile',
category: 'engagement',
label: 'Save to profile in profile submission',
});
}
};
return (
@ -100,9 +118,9 @@ export default function OffersProfileSave({
</p>
<div className="mb-20">
<Button
disabled={isSaved}
disabled={isSavedQuery.isLoading || isSaved}
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
isLoading={saveMutation.isLoading}
isLoading={saveMutation.isLoading || isSavedQuery.isLoading}
label={isSaved ? 'Saved to user profile' : 'Save to user profile'}
variant="primary"
onClick={handleSave}

@ -12,6 +12,7 @@ import {
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
import Tooltip from '~/components/offers/util/Tooltip';
import loginPageHref from '~/components/shared/loginPageHref';
import { copyProfileLink } from '~/utils/offers/link';
import { trpc } from '~/utils/trpc';
@ -195,7 +196,7 @@ export default function ProfileComments({
<Button
className="mb-5"
display="block"
href="/api/auth/signin"
href={loginPageHref()}
label="Sign in to join discussion"
variant="tertiary"
/>

@ -1,4 +1,5 @@
import { useRouter } from 'next/router';
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import {
BookmarkIcon as BookmarkIconOutline,
@ -10,23 +11,22 @@ import {
import { BookmarkIcon as BookmarkIconSolid } from '@heroicons/react/24/solid';
import { Button, Dialog, Spinner, Tabs, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import type { ProfileDetailTab } from '~/components/offers/constants';
import { profileDetailTabs } from '~/components/offers/constants';
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
import type { BackgroundDisplayData } from '~/components/offers/types';
import { JobTypeLabel } from '~/components/offers/types';
import Tooltip from '~/components/offers/util/Tooltip';
import { getProfileEditPath } from '~/utils/offers/link';
import { trpc } from '~/utils/trpc';
import type { ProfileDetailTab } from '../constants';
import { profileDetailTabs } from '../constants';
import Tooltip from '../util/Tooltip';
type ProfileHeaderProps = Readonly<{
background?: BackgroundDisplayData;
handleDelete: () => void;
isEditable: boolean;
isLoading: boolean;
isSaved?: boolean;
selectedTab: ProfileDetailTab;
setSelectedTab: (tab: ProfileDetailTab) => void;
}>;
@ -36,31 +36,59 @@ export default function ProfileHeader({
handleDelete,
isEditable,
isLoading,
isSaved = false,
selectedTab,
setSelectedTab,
}: ProfileHeaderProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [saved, setSaved] = useState(isSaved);
const [saved, setSaved] = useState(false);
const router = useRouter();
const trpcContext = trpc.useContext();
const { offerProfileId = '', token = '' } = router.query;
const { showToast } = useToast();
const { data: session, status } = useSession();
const { event: gaEvent } = useGoogleAnalytics();
const handleEditClick = () => {
gaEvent({
action: 'offers.edit_profile',
category: 'engagement',
label: 'Edit profile',
});
router.push(getProfileEditPath(offerProfileId as string, token as string));
};
const isSavedQuery = trpc.useQuery(
[
`offers.profile.isSaved`,
{ profileId: offerProfileId as string, userId: session?.user?.id },
],
{
onSuccess: (res) => {
setSaved(res);
},
},
);
const saveMutation = trpc.useMutation(
['offers.user.profile.addToUserProfile'],
{
onError: () => {
showToast({
subtitle: 'Please check that you are logged in.',
title: `Failed to saved to dashboard!`,
variant: 'failure',
});
},
onSuccess: () => {
setSaved(true);
trpcContext.invalidateQueries([
'offers.profile.isSaved',
{
profileId: offerProfileId as string,
userId: session?.user?.id,
},
]);
showToast({
title: `Saved to dashboard!`,
variant: 'success',
@ -79,18 +107,25 @@ export default function ProfileHeader({
});
},
onSuccess: () => {
setSaved(false);
trpcContext.invalidateQueries([
'offers.profile.isSaved',
{
profileId: offerProfileId as string,
userId: session?.user?.id,
},
]);
showToast({
title: `Removed from dashboard!`,
variant: 'success',
});
trpcContext.invalidateQueries(['offers.profile.listOne']);
},
},
);
const toggleSaved = () => {
if (saved) {
if (status === 'unauthenticated') {
signIn();
} else if (saved) {
unsaveMutation.mutate({ profileId: offerProfileId as string });
} else {
saveMutation.mutate({
@ -105,15 +140,22 @@ export default function ProfileHeader({
<div className="flex justify-center space-x-2">
<Tooltip
tooltipContent={
isSaved ? 'Remove from account' : 'Save to your account'
saved ? 'Remove from account' : 'Save to your account'
}>
<Button
disabled={
isLoading || saveMutation.isLoading || unsaveMutation.isLoading
isLoading ||
saveMutation.isLoading ||
unsaveMutation.isLoading ||
isSavedQuery.isLoading
}
icon={saved ? BookmarkIconSolid : BookmarkIconOutline}
isLabelHidden={true}
isLoading={saveMutation.isLoading || unsaveMutation.isLoading}
isLoading={
isSavedQuery.isLoading ||
saveMutation.isLoading ||
unsaveMutation.isLoading
}
label={saved ? 'Remove from account' : 'Save to your account'}
size="md"
variant="tertiary"
@ -193,7 +235,7 @@ export default function ProfileHeader({
return (
<div className="h-40 bg-white p-4">
<div className="justify-left flex h-1/2">
<div className="mx-4 mt-2">
<div className="mx-4 mt-2 h-16 w-16">
<ProfilePhotoHolder />
</div>
<div className="w-full">

@ -5,6 +5,7 @@ import { Fragment, useRef, useState } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc';
export type AddToListDropdownProps = {
@ -85,14 +86,16 @@ export default function AddToListDropdown({
});
};
const handleMenuButtonClick = useProtectedCallback(() => {
addClickOutsideListener();
setMenuOpened(!menuOpened);
});
const CustomMenuButton = ({ children }: PropsWithChildren<unknown>) => (
<button
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100"
type="button"
onClick={() => {
addClickOutsideListener();
setMenuOpened(!menuOpened);
}}>
onClick={handleMenuButtonClick}>
{children}
</button>
);

@ -6,6 +6,8 @@ import {
} from '@heroicons/react/24/outline';
import { TextInput } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import ContributeQuestionDialog from './ContributeQuestionDialog';
import type { ContributeQuestionFormProps } from './forms/ContributeQuestionForm';
@ -23,14 +25,14 @@ export default function ContributeQuestionCard({
setShowDraftDialog(false);
};
const handleOpenContribute = () => {
const handleOpenContribute = useProtectedCallback(() => {
setShowDraftDialog(true);
};
});
return (
<div className="w-full">
<button
className="flex flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
className="flex w-full flex-col items-stretch justify-center gap-2 rounded-md border border-slate-300 bg-white p-4 text-left hover:bg-slate-100"
type="button"
onClick={handleOpenContribute}>
<TextInput
@ -40,7 +42,7 @@ export default function ContributeQuestionCard({
placeholder="Contribute a question"
onChange={handleOpenContribute}
/>
<div className="flex flex-wrap items-end justify-center gap-x-2">
<div className="flex flex-wrap items-end justify-start gap-2">
<div className="min-w-[150px] flex-1">
<TextInput
disabled={true}
@ -72,12 +74,12 @@ export default function ContributeQuestionCard({
Contribute
</h1>
</div>
<ContributeQuestionDialog
show={showDraftDialog}
onCancel={handleDraftDialogCancel}
onSubmit={onSubmit}
/>
</button>
<ContributeQuestionDialog
show={showDraftDialog}
onCancel={handleDraftDialogCancel}
onSubmit={onSubmit}
/>
</div>
);
}

@ -35,7 +35,12 @@ export default function ContributeQuestionDialog({
return (
<div>
<Transition.Root as={Fragment} show={show}>
<Dialog as="div" className="relative z-10" onClose={onCancel}>
<Dialog
as="div"
className="relative z-10"
onClose={() => {
onCancel();
}}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"

@ -1,9 +1,12 @@
import { useEffect, useState } from 'react';
import { ArrowSmallRightIcon } from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client';
import type { TypeaheadOption } from '@tih/ui';
import { Button, Select } from '@tih/ui';
import { companyOptionToSlug } from '~/utils/questions/companySlug';
import { QUESTION_TYPES } from '~/utils/questions/constants';
import { locationOptionToSlug } from '~/utils/questions/locationSlug';
import useDefaultCompany from '~/utils/questions/useDefaultCompany';
import useDefaultLocation from '~/utils/questions/useDefaultLocation';
@ -11,8 +14,10 @@ import type { FilterChoice } from './filter/FilterSection';
import CompanyTypeahead from './typeahead/CompanyTypeahead';
import LocationTypeahead from './typeahead/LocationTypeahead';
import type { Location } from '~/types/questions';
export type LandingQueryData = {
company: string;
companySlug: string;
location: string;
questionType: QuestionsQuestionType;
};
@ -30,9 +35,9 @@ export default function LandingComponent({
const [company, setCompany] = useState<FilterChoice | undefined>(
defaultCompany,
);
const [location, setLocation] = useState<FilterChoice | undefined>(
defaultLocation,
);
const [location, setLocation] = useState<
(Location & TypeaheadOption) | undefined
>(defaultLocation);
const [questionType, setQuestionType] =
useState<QuestionsQuestionType>('CODING');
@ -41,7 +46,7 @@ export default function LandingComponent({
setCompany(newCompany);
};
const handleChangeLocation = (newLocation: FilterChoice) => {
const handleChangeLocation = (newLocation: Location & TypeaheadOption) => {
setLocation(newLocation);
};
@ -71,7 +76,7 @@ export default function LandingComponent({
className="h-40 w-40"
src="/bank-logo.png"
/>
<h1 className="text-4xl font-bold text-slate-900 text-center">
<h1 className="text-center text-4xl font-bold text-slate-900">
Tech Interview Question Bank
</h1>
</div>
@ -124,8 +129,8 @@ export default function LandingComponent({
onClick={() => {
if (company !== undefined && location !== undefined) {
return handleLandingQuery({
company: company.label,
location: location.value,
companySlug: companyOptionToSlug(company),
location: locationOptionToSlug(location),
questionType,
});
}

@ -4,6 +4,8 @@ import type { Vote } from '@prisma/client';
import type { ButtonSize } from '@tih/ui';
import { Button } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
export type BackendVote = {
id: string;
vote: Vote;
@ -31,6 +33,15 @@ export default function VotingButtons({
vote?.vote === 'UPVOTE' ? 'secondary' : 'tertiary';
const downvoteButtonVariant =
vote?.vote === 'DOWNVOTE' ? 'secondary' : 'tertiary';
const handleUpvoteClick = useProtectedCallback(() => {
onUpvote();
});
const handleDownvoteClick = useProtectedCallback(() => {
onDownvote();
});
return (
<div className="flex flex-col items-center">
<Button
@ -42,7 +53,7 @@ export default function VotingButtons({
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onUpvote();
handleUpvoteClick();
}}
/>
<p>{upvoteCount}</p>
@ -55,7 +66,7 @@ export default function VotingButtons({
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onDownvote();
handleDownvoteClick();
}}
/>
</div>

@ -9,6 +9,7 @@ import {
import type { QuestionsQuestionType } from '@prisma/client';
import { Button } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { useQuestionVote } from '~/utils/questions/useVote';
import AddToListDropdown from '../../AddToListDropdown';
@ -168,6 +169,10 @@ export default function BaseQuestionCard({
return countryCount;
}, [countries]);
const handleCreateEncounterClick = useProtectedCallback(() => {
setShowReceivedForm(true);
});
const cardContent = (
<>
{showVoteButtons && (
@ -211,7 +216,11 @@ export default function BaseQuestionCard({
/>
)}
</div>
<p className={clsx(truncateContent && 'line-clamp-2 text-ellipsis')}>
<p
className={clsx(
'whitespace-pre-line font-semibold',
truncateContent && 'line-clamp-2 text-ellipsis',
)}>
{content}
</p>
{!showReceivedForm &&
@ -244,10 +253,7 @@ export default function BaseQuestionCard({
label={createEncounterButtonText}
size="sm"
variant="tertiary"
onClick={(event) => {
event.preventDefault();
setShowReceivedForm(true);
}}
onClick={handleCreateEncounterClick}
/>
)}
</div>

@ -252,7 +252,7 @@ export default function ContributeQuestionForm({
)}
</div>
<div
className="bg-primary-50 flex w-full justify-between gap-y-2 py-3 shadow-[0_0_0_100vmax_theme(colors.primary.50)]"
className="bg-primary-50 flex w-full flex-col gap-y-2 py-3 shadow-[0_0_0_100vmax_theme(colors.primary.50)] sm:flex-row sm:justify-between"
style={{
// Hack to make the background bleed outside the container
clipPath: 'inset(0 -100vmax)',
@ -278,6 +278,7 @@ export default function ContributeQuestionForm({
</button>
<Button
className="bg-primary-600 hover:bg-primary-700 focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-transparent px-4 py-2 text-base font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:bg-slate-400 sm:ml-3 sm:w-auto sm:text-sm"
disabled={!checkedSimilar}
label="Contribute"
type="submit"
variant="primary"></Button>

@ -42,13 +42,16 @@ export default function CreateQuestionEncounterForm({
return (
<div className="flex items-center gap-2">
<p className="font-md text-md text-slate-600">I saw this question at</p>
<p className="font-md text-md text-slate-600">
I saw this question {step <= 1 ? 'at' : step === 2 ? 'for' : 'on'}
</p>
{step === 0 && (
<div>
<CompanyTypeahead
isLabelHidden={true}
placeholder="Other company"
suggestedCount={3}
placeholder="Company"
// TODO: Fix suggestions and set count back to 3
suggestedCount={0}
// @ts-ignore TODO(questions): handle potentially null value.
onSelect={({ value: company }) => {
setSelectedCompany(company);
@ -64,8 +67,8 @@ export default function CreateQuestionEncounterForm({
<div>
<LocationTypeahead
isLabelHidden={true}
placeholder="Other location"
suggestedCount={3}
placeholder="Location"
suggestedCount={0}
// @ts-ignore TODO(questions): handle potentially null value.
onSelect={(location) => {
setSelectedLocation(location);
@ -81,8 +84,8 @@ export default function CreateQuestionEncounterForm({
<div>
<RoleTypeahead
isLabelHidden={true}
placeholder="Other role"
suggestedCount={3}
placeholder="Role"
suggestedCount={0}
// @ts-ignore TODO(questions): handle potentially null value.
onSelect={({ value: role }) => {
setSelectedRole(role);

@ -0,0 +1,40 @@
import type { PropsWithChildren } from 'react';
import { createContext, useState } from 'react';
import ProtectedDialog from './ProtectedDialog';
export type ProtectedContextData = {
showDialog: () => void;
};
export const ProtectedContext = createContext<ProtectedContextData>({
// eslint-disable-next-line @typescript-eslint/no-empty-function
showDialog: () => {},
});
export type ProtectedContextProviderProps = PropsWithChildren<
Record<string, unknown>
>;
export default function ProtectedContextProvider({
children,
}: ProtectedContextProviderProps) {
const [show, setShow] = useState(false);
return (
<ProtectedContext.Provider
value={{
showDialog: () => {
setShow(true);
},
}}>
{children}
<ProtectedDialog
show={show}
onClose={() => {
setShow(false);
}}
/>
</ProtectedContext.Provider>
);
}

@ -0,0 +1,36 @@
import { signIn } from 'next-auth/react';
import { Button, Dialog } from '@tih/ui';
export type ProtectedDialogProps = {
onClose: () => void;
show: boolean;
};
export default function ProtectedDialog({
show,
onClose,
}: ProtectedDialogProps) {
const handlePrimaryClick = () => {
signIn();
onClose();
};
return (
<Dialog
isShown={show}
primaryButton={
<Button
label="Sign in"
variant="primary"
onClick={handlePrimaryClick}
/>
}
secondaryButton={
<Button label="Cancel" variant="tertiary" onClick={onClose} />
}
title="Sign in to continue"
onClose={onClose}>
<p>This action requires you to be signed in.</p>
</Dialog>
);
}

@ -10,6 +10,8 @@ import {
} from '@heroicons/react/20/solid';
import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import type {
ExperienceFilter,
LocationFilter,
@ -30,8 +32,17 @@ type Props = Readonly<{
}>;
export default function ResumeListItem({ href, resumeInfo }: Props) {
const { event: gaEvent } = useGoogleAnalytics();
return (
<Link href={href}>
<Link
href={href}
onClick={() =>
gaEvent({
action: 'resumes.listitem_click',
category: 'engagement',
label: 'Select Resume',
})
}>
<div className="grid grid-cols-8">
<div className="col-span-7 grid gap-4 border-b border-slate-200 p-4 hover:bg-slate-100 sm:grid-cols-7">
<div className="sm:col-span-4">

@ -8,6 +8,7 @@ import {
import { Vote } from '@prisma/client';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import loginPageHref from '~/components/shared/loginPageHref';
import { trpc } from '~/utils/trpc';
@ -63,7 +64,7 @@ export default function ResumeCommentVoteButtons({
const onVote = async (value: Vote, setAnimation: (_: boolean) => void) => {
if (!userId) {
router.push('/api/auth/signin');
router.push(loginPageHref());
return;
}

@ -1,5 +1,7 @@
import clsx from 'clsx';
import { signIn } from 'next-auth/react';
import Link from 'next/link';
import loginPageHref from '~/components/shared/loginPageHref';
type Props = Readonly<{
className?: string;
@ -10,15 +12,11 @@ export default function ResumeSignInButton({ text, className }: Props) {
return (
<div className={clsx('flex justify-center', className)}>
<p>
<a
className="text-indigo-500 hover:text-indigo-600"
href="/api/auth/signin"
onClick={(event) => {
event.preventDefault();
signIn();
}}>
Sign in
</a>{' '}
<Link
className="text-primary-500 hover:text-primary-600"
href={loginPageHref()}>
Log in
</Link>{' '}
{text}
</p>
</div>

@ -0,0 +1,15 @@
export default function GitHubIcon(props: React.ComponentProps<'svg'>) {
return (
<svg
fill="currentColor"
height="1em"
stroke="currentColor"
strokeWidth={0}
viewBox="0 0 496 512"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path>
</svg>
);
}

@ -0,0 +1,8 @@
export default function loginPageHref() {
return {
pathname: '/login',
query: {
redirect: typeof window !== 'undefined' ? window.location.href : null,
},
};
}

@ -9,6 +9,7 @@ import { loggerLink } from '@trpc/client/links/loggerLink';
import { withTRPC } from '@trpc/next';
import AppShell from '~/components/global/AppShell';
import ProtectedContextProvider from '~/components/questions/protected/ProtectedContextProvider';
import type { AppRouter } from '~/server/router';
@ -21,9 +22,11 @@ const MyApp: AppType<{ session: Session | null }> = ({
return (
<SessionProvider session={session}>
<ToastsProvider>
<AppShell>
<Component {...pageProps} />
</AppShell>
<ProtectedContextProvider>
<AppShell>
<Component {...pageProps} />
</AppShell>
</ProtectedContextProvider>
</ToastsProvider>
</SessionProvider>
);

@ -0,0 +1,72 @@
import { useRouter } from 'next/router';
import type {
GetServerSideProps,
InferGetServerSidePropsType,
} from 'next/types';
import { getProviders, signIn } from 'next-auth/react';
import { Button } from '@tih/ui';
import GitHubIcon from '~/components/shared/icons/GitHubIcon';
export const getServerSideProps: GetServerSideProps<{
providers: Awaited<ReturnType<typeof getProviders>>;
}> = async () => {
const providers = await getProviders();
return {
props: { providers },
};
};
export default function LoginPage({
providers,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const router = useRouter();
return (
<div className="flex w-full justify-center">
<div className="flex min-h-full flex-col justify-center py-12 px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<img
alt="Tech Interview Handbook"
className="mx-auto h-24 w-auto"
src="/logo.svg"
/>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-slate-900">
Tech Interview Handbook Portal
</h2>
<p className="mt-2 text-center text-slate-600">
Get your resumes peer-reviewed, discuss solutions to tech interview
questions, get offer data points.
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="space-y-4">
{providers != null &&
Object.values(providers).map((provider) => (
<div key={provider.name}>
<Button
addonPosition="start"
display="block"
icon={GitHubIcon}
label={`Sign in with ${provider.name}`}
type="button"
variant="primary"
onClick={() =>
signIn(
provider.id,
router.query.redirect != null
? {
callbackUrl: String(router.query.redirect),
}
: undefined,
)
}
/>
</div>
))}
</div>
</div>
</div>
</div>
);
}

@ -47,6 +47,8 @@ export default function OffersHomePage() {
category: 'engagement',
label: 'Filter by job title',
});
} else {
setjobTitleFilter('');
}
}}
/>
@ -62,6 +64,8 @@ export default function OffersHomePage() {
category: 'engagement',
label: 'Filter by company',
});
} else {
setCompanyFilter('');
}
}}
/>

@ -4,6 +4,7 @@ import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { Spinner, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { ProfileDetailTab } from '~/components/offers/constants';
import ProfileComments from '~/components/offers/profile/ProfileComments';
import ProfileDetails from '~/components/offers/profile/ProfileDetails';
@ -36,6 +37,7 @@ export default function OfferProfile() {
);
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
const { data: session } = useSession();
const { event: gaEvent } = useGoogleAnalytics();
const getProfileQuery = trpc.useQuery(
[
@ -176,6 +178,11 @@ export default function OfferProfile() {
profileId: offerProfileId as string,
token: token as string,
});
gaEvent({
action: 'offers.delete_profile',
category: 'engagement',
label: 'Delete profile',
});
}
}
@ -202,7 +209,6 @@ export default function OfferProfile() {
handleDelete={handleDelete}
isEditable={isEditable}
isLoading={getProfileQuery.isLoading}
isSaved={getProfileQuery.data?.isSaved}
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
/>

@ -13,6 +13,7 @@ import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
import { APP_TITLE } from '~/utils/questions/constants';
import { useFormRegister } from '~/utils/questions/useFormRegister';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d';
@ -82,13 +83,15 @@ export default function QuestionPage() {
},
);
const handleSubmitComment = (data: AnswerCommentData) => {
resetComment();
addComment({
answerId: answerId as string,
content: data.commentContent,
});
};
const handleSubmitComment = useProtectedCallback(
(data: AnswerCommentData) => {
resetComment();
addComment({
answerId: answerId as string,
content: data.commentContent,
});
},
);
if (!answer) {
return <FullScreenSpinner />;

@ -16,6 +16,7 @@ import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useFormRegister } from '~/utils/questions/useFormRegister';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d';
@ -53,10 +54,11 @@ export default function QuestionPage() {
const {
register: comRegister,
handleSubmit: handleCommentSubmit,
handleSubmit: handleCommentSubmitClick,
reset: resetComment,
formState: { isDirty: isCommentDirty, isValid: isCommentValid },
} = useForm<QuestionCommentData>({ mode: 'onChange' });
const commentRegister = useFormRegister(comRegister);
const { questionId } = router.query;
@ -149,21 +151,25 @@ export default function QuestionPage() {
},
);
const handleSubmitAnswer = (data: AnswerQuestionData) => {
addAnswer({
content: data.answerContent,
questionId: questionId as string,
});
resetAnswer();
};
const handleSubmitAnswer = useProtectedCallback(
(data: AnswerQuestionData) => {
addAnswer({
content: data.answerContent,
questionId: questionId as string,
});
resetAnswer();
},
);
const handleSubmitComment = (data: QuestionCommentData) => {
addComment({
content: data.commentContent,
questionId: questionId as string,
});
resetComment();
};
const handleSubmitComment = useProtectedCallback(
(data: QuestionCommentData) => {
addComment({
content: data.commentContent,
questionId: questionId as string,
});
resetComment();
},
);
if (!question) {
return <FullScreenSpinner />;
@ -219,7 +225,7 @@ export default function QuestionPage() {
<div className="mt-4 px-4">
<form
className="mb-2"
onSubmit={handleCommentSubmit(handleSubmitComment)}>
onSubmit={handleCommentSubmitClick(handleSubmitComment)}>
<TextArea
{...commentRegister('commentContent', {
minLength: 1,

@ -22,6 +22,7 @@ import type { QuestionAge } from '~/utils/questions/constants';
import { APP_TITLE } from '~/utils/questions/constants';
import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import { locationOptionToSlug } from '~/utils/questions/locationSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import {
useSearchParam,
@ -33,17 +34,6 @@ import type { Location } from '~/types/questions.d';
import { SortType } from '~/types/questions.d';
import { SortOrder } from '~/types/questions.d';
function locationToSlug(value: Location & TypeaheadOption): string {
return [
value.countryId,
value.stateId,
value.cityId,
value.id,
value.label,
value.value,
].join('-');
}
export default function QuestionsBrowsePage() {
const router = useRouter();
@ -88,7 +78,7 @@ export default function QuestionsBrowsePage() {
useSearchParam('roles');
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
useSearchParam<Location & TypeaheadOption>('locations', {
paramToString: locationToSlug,
paramToString: locationOptionToSlug,
stringToParam: (param) => {
const [countryId, stateId, cityId, id, label, value] = param.split('-');
return { cityId, countryId, id, label, stateId, value };
@ -266,7 +256,7 @@ export default function QuestionsBrowsePage() {
pathname,
query: {
companies: selectedCompanySlugs,
locations: selectedLocations.map(locationToSlug),
locations: selectedLocations.map(locationOptionToSlug),
questionAge: selectedQuestionAge,
questionTypes: selectedQuestionTypes,
roles: selectedRoles,

@ -10,13 +10,13 @@ export default function QuestionsHomePage() {
const router = useRouter();
const handleLandingQuery = async (data: LandingQueryData) => {
const { company, location, questionType } = data;
const { companySlug, location, questionType } = data;
// Go to browse page
router.push({
pathname: '/questions/browse',
query: {
companies: [company],
companies: [companySlug],
locations: [location],
questionTypes: [questionType],
},

@ -16,6 +16,7 @@ import { Button } from '~/../../../packages/ui/dist';
import { APP_TITLE } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc';
export default function ListPage() {
@ -77,6 +78,10 @@ export default function ListPage() {
setShowCreateListDialog(false);
};
const handleAddClick = useProtectedCallback(() => {
setShowCreateListDialog(true);
});
const listOptions = (
<>
<ul className="flex flex-1 flex-col divide-y divide-solid divide-slate-200">
@ -157,10 +162,10 @@ export default function ListPage() {
label="Create"
size="md"
variant="tertiary"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowCreateListDialog(true);
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleAddClick();
}}
/>
</div>
@ -223,11 +228,13 @@ export default function ListPage() {
onCancel={handleDeleteListCancel}
onDelete={() => {
handleDeleteList(listIdToDelete);
}}></DeleteListDialog>
}}
/>
<CreateListDialog
show={showCreateListDialog}
onCancel={handleCreateListCancel}
onSubmit={handleCreateList}></CreateListDialog>
onSubmit={handleCreateList}
/>
</section>
</div>
</main>

@ -22,6 +22,7 @@ import ResumeCommentsForm from '~/components/resumes/comments/ResumeCommentsForm
import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList';
import ResumePdf from '~/components/resumes/ResumePdf';
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
import loginPageHref from '~/components/shared/loginPageHref';
import type {
ExperienceFilter,
@ -107,7 +108,7 @@ export default function ResumeReviewPage() {
const onStarButtonClick = () => {
if (session?.user?.id == null) {
router.push('/api/auth/signin');
router.push(loginPageHref());
return;
}
@ -184,8 +185,8 @@ export default function ResumeReviewPage() {
<Button
className="h-10 shadow-md"
display="block"
href="/api/auth/signin"
label="Sign in to join discussion"
href={loginPageHref()}
label="Log in to join discussion"
variant="primary"
/>
);

@ -20,9 +20,11 @@ import {
TextInput,
} from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
import loginPageHref from '~/components/shared/loginPageHref';
import type {
Filter,
@ -135,6 +137,7 @@ export default function ResumeHomePage() {
role: false,
});
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const skip = (currentPage - 1) * PAGE_LIMIT;
const isSearchOptionsInit = useMemo(() => {
@ -255,7 +258,7 @@ export default function ResumeHomePage() {
const onSubmitResume = () => {
if (sessionData === null) {
router.push('/api/auth/signin');
router.push(loginPageHref());
} else {
router.push('/resumes/submit');
}
@ -279,6 +282,11 @@ export default function ResumeHomePage() {
),
});
}
gaEvent({
action: 'resumes.filter_checkbox_click',
category: 'engagement',
label: 'Select Filter',
});
};
const onClearFilterClick = (filterSection: FilterId) => {
@ -296,11 +304,21 @@ export default function ResumeHomePage() {
setShortcutSelected(shortcutName);
setSortOrder(shortcutSortOrder);
setUserFilters(shortcutFilters);
gaEvent({
action: 'resumes.shortcut_button_click',
category: 'engagement',
label: `Select Shortcut: ${shortcutName}`,
});
};
const onTabChange = (tab: string) => {
setTabsValue(tab);
setCurrentPage(1);
gaEvent({
action: 'resumes.tab_click',
category: 'engagement',
label: `Select Tab: ${tab}`,
});
};
const getTabQueryData = () => {
@ -631,6 +649,13 @@ export default function ResumeHomePage() {
type="text"
value={searchValue}
onChange={setSearchValue}
onFocus={() =>
gaEvent({
action: 'resumes.search_input_focus',
category: 'engagement',
label: 'Click Search',
})
}
/>
</div>
<DropdownMenu

@ -21,6 +21,7 @@ import {
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
import loginPageHref from '~/components/shared/loginPageHref';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
@ -129,7 +130,7 @@ export default function SubmitResumeForm({
// Route user to sign in if not logged in
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/api/auth/signin');
router.push(loginPageHref());
}
}, [router, status]);

@ -1,51 +1,10 @@
import { z } from 'zod';
import type {
Company,
OffersBackground,
OffersCurrency,
OffersFullTime,
OffersIntern,
OffersOffer,
OffersProfile,
} from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { profileAnalysisDtoMapper } from '~/mappers/offers-mappers';
import { createRouter } from '../context';
type Offer = OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null };
};
const searchOfferPercentile = (
offer: Offer,
similarOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & {
totalCompensation: OffersCurrency;
})
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null };
}
>,
) => {
for (let i = 0; i < similarOffers.length; i++) {
if (similarOffers[i].id === offer.id) {
return i;
}
}
return -1;
};
import { generateAnalysis } from '../../../utils/offers/analysisGeneration';
export const offersAnalysisRouter = createRouter()
.query('get', {
@ -160,362 +119,6 @@ export const offersAnalysisRouter = createRouter()
profileId: z.string(),
}),
async resolve({ ctx, input }) {
await ctx.prisma.offersAnalysis.deleteMany({
where: {
profileId: input.profileId,
},
});
const offers = await ctx.prisma.offersOffer.findMany({
include: {
company: true,
offersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
orderBy: [
{
offersFullTime: {
totalCompensation: {
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
},
},
},
],
where: {
profileId: input.profileId,
},
});
if (!offers || offers.length === 0) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No offers found on this profile',
});
}
const overallHighestOffer = offers[0];
if (
!overallHighestOffer.profile.background ||
overallHighestOffer.profile.background.totalYoe == null
) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'YOE not found',
});
}
const yoe = overallHighestOffer.profile.background.totalYoe as number;
const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
let similarOffers = await ctx.prisma.offersOffer.findMany({
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
orderBy: [
{
offersFullTime: {
totalCompensation: {
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
},
},
},
],
where: {
AND: [
{
location: overallHighestOffer.location,
},
{
monthYearReceived: {
gte: monthYearReceived,
},
},
{
OR: [
{
offersFullTime: {
title: overallHighestOffer.offersFullTime?.title,
},
offersIntern: {
title: overallHighestOffer.offersIntern?.title,
},
},
],
},
{
profile: {
background: {
AND: [
{
totalYoe: {
gte: Math.max(yoe - 1, 0),
lte: yoe + 1,
},
},
],
},
},
},
],
},
});
// COMPANY ANALYSIS
const companyMap = new Map<string, Offer>();
offers.forEach((offer) => {
if (companyMap.get(offer.companyId) == null) {
companyMap.set(offer.companyId, offer);
}
});
const companyAnalysis = Array.from(companyMap.values()).map(
(companyOffer) => {
// TODO: Refactor calculating analysis into a function
let similarCompanyOffers = similarOffers.filter(
(offer) => offer.companyId === companyOffer.companyId,
);
const companyIndex = searchOfferPercentile(
companyOffer,
similarCompanyOffers,
);
const companyPercentile =
similarCompanyOffers.length <= 1
? 100
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
// Get top offers (excluding user's offer)
similarCompanyOffers = similarCompanyOffers.filter(
(offer) => offer.id !== companyOffer.id,
);
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
const similarCompanyOffers90PercentileIndex = Math.ceil(
noOfSimilarCompanyOffers * 0.1,
);
const topPercentileCompanyOffers =
noOfSimilarCompanyOffers > 2
? similarCompanyOffers.slice(
similarCompanyOffers90PercentileIndex,
similarCompanyOffers90PercentileIndex + 2,
)
: similarCompanyOffers;
return {
companyName: companyOffer.company.name,
noOfSimilarOffers: noOfSimilarCompanyOffers,
percentile: companyPercentile,
topSimilarOffers: topPercentileCompanyOffers,
};
},
);
// OVERALL ANALYSIS
const overallIndex = searchOfferPercentile(
overallHighestOffer,
similarOffers,
);
const overallPercentile =
similarOffers.length <= 1
? 100
: 100 - (100 * overallIndex) / (similarOffers.length - 1);
similarOffers = similarOffers.filter(
(offer) => offer.id !== overallHighestOffer.id,
);
const noOfSimilarOffers = similarOffers.length;
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
const topPercentileOffers =
noOfSimilarOffers > 2
? similarOffers.slice(
similarOffers90PercentileIndex,
similarOffers90PercentileIndex + 2,
)
: similarOffers;
const analysis = await ctx.prisma.offersAnalysis.create({
data: {
companyAnalysis: {
create: companyAnalysis.map((analysisUnit) => {
return {
companyName: analysisUnit.companyName,
noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
percentile: analysisUnit.percentile,
topSimilarOffers: {
connect: analysisUnit.topSimilarOffers.map((offer) => {
return { id: offer.id };
}),
},
};
}),
},
overallAnalysis: {
create: {
companyName: overallHighestOffer.company.name,
noOfSimilarOffers,
percentile: overallPercentile,
topSimilarOffers: {
connect: topPercentileOffers.map((offer) => {
return { id: offer.id };
}),
},
},
},
overallHighestOffer: {
connect: {
id: overallHighestOffer.id,
},
},
profile: {
connect: {
id: input.profileId,
},
},
},
include: {
companyAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
},
},
});
return profileAnalysisDtoMapper(analysis);
return generateAnalysis({ ctx, input });
},
});

@ -1,5 +1,6 @@
import crypto from 'crypto';
import { z } from 'zod';
import type { OffersProfile } from '@prisma/client';
import { JobType } from '@prisma/client';
import * as trpc from '@trpc/server';
@ -108,14 +109,51 @@ export const offersProfileRouter = createRouter()
token: z.string(),
}),
async resolve({ ctx, input }) {
const profile: OffersProfile | null =
await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
return profile?.editToken === input.token;
},
})
.query('isSaved', {
input: z.object({
profileId: z.string(),
userId: z.string().nullish(),
}),
async resolve({ ctx, input }) {
if (!input.userId) {
return false;
}
const profile = await ctx.prisma.offersProfile.findFirst({
include: {
users: true,
},
where: {
id: input.profileId
id: input.profileId,
},
});
const users = profile?.users;
if (!users) {
return false;
}
let isSaved = false;
for (let i = 0; i < users.length; i++) {
if (users[i].id === input.userId) {
isSaved = true;
}
})
}
return profile?.editToken === input.token
}
return isSaved;
},
})
.query('listOne', {
input: z.object({

@ -41,19 +41,21 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter()
});
}
if (
questionToUpdate.lastSeenAt === null ||
questionToUpdate.lastSeenAt < input.seenAt
) {
await tx.questionsQuestion.update({
data: {
lastSeenAt: input.seenAt,
},
where: {
id: input.questionId,
await tx.questionsQuestion.update({
data: {
lastSeenAt: (questionToUpdate.lastSeenAt === null ||
questionToUpdate.lastSeenAt < input.seenAt)
? input.seenAt : undefined,
numEncounters: {
increment: 1,
},
});
}
},
where: {
id: input.questionId,
},
});
return questionEncounterCreated;
});
},
@ -160,6 +162,8 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter()
}),
]);
let lastSeenVal = undefined;
if (questionToUpdate!.lastSeenAt === questionEncounterToDelete.seenAt) {
const latestEncounter =
await ctx.prisma.questionsQuestionEncounter.findFirst({
@ -171,17 +175,20 @@ export const questionsQuestionEncounterUserRouter = createProtectedRouter()
},
});
const lastSeenVal = latestEncounter ? latestEncounter!.seenAt : null;
lastSeenVal = latestEncounter ? latestEncounter!.seenAt : null;
}
await tx.questionsQuestion.update({
await tx.questionsQuestion.update({
data: {
lastSeenAt: lastSeenVal,
numEncounters: {
increment: -1,
},
},
where: {
id: questionToUpdate!.id,
},
});
}
return questionEncounterDeleted;
});

@ -13,6 +13,7 @@ export const questionsQuestionRouter = createRouter()
input: z.object({
cityIds: z.string().array(),
companyIds: z.string().array(),
content: z.string().optional(),
countryIds: z.string().array(),
cursor: z.string().nullish(),
endDate: z.date().default(new Date()),
@ -27,24 +28,40 @@ export const questionsQuestionRouter = createRouter()
async resolve({ ctx, input }) {
const { cursor } = input;
const sortCondition =
input.sortType === SortType.TOP
? [
{
upvotes: input.sortOrder,
},
{
id: input.sortOrder,
},
]
: [
{
lastSeenAt: input.sortOrder,
},
{
id: input.sortOrder,
},
];
let sortCondition = undefined;
switch (input.sortType) {
case SortType.TOP:
sortCondition = [
{
upvotes: input.sortOrder,
},
{
id: input.sortOrder,
},
]
break;
case SortType.NEW:
sortCondition = [
{
lastSeenAt: input.sortOrder,
},
{
id: input.sortOrder,
},
];
break;
case SortType.ENCOUNTERS:
sortCondition = [
{
numEncounters: input.sortOrder,
},
{
id: input.sortOrder,
},
];
break;
}
const questionsData = await ctx.prisma.questionsQuestion.findMany({
cursor: cursor ? { id: cursor } : undefined,

@ -88,4 +88,5 @@ export enum SortOrder {
export enum SortType {
TOP,
NEW,
ENCOUNTERS,
}

@ -0,0 +1,421 @@
import type { Session } from 'next-auth';
import type {
Company,
OffersBackground,
OffersCurrency,
OffersFullTime,
OffersIntern,
OffersOffer,
OffersProfile,
Prisma,
PrismaClient,
} from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { profileAnalysisDtoMapper } from '../../mappers/offers-mappers';
type Offer = OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null };
};
const searchOfferPercentile = (
offer: Offer,
similarOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & {
totalCompensation: OffersCurrency;
})
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null };
}
>,
) => {
for (let i = 0; i < similarOffers.length; i++) {
if (similarOffers[i].id === offer.id) {
return i;
}
}
return -1;
};
export const generateAnalysis = async (params: {
ctx: {
prisma: PrismaClient<
Prisma.PrismaClientOptions,
never,
Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined
>;
session: Session | null;
};
input: { profileId: string };
}) => {
const { ctx, input } = params;
await ctx.prisma.offersAnalysis.deleteMany({
where: {
profileId: input.profileId,
},
});
const offers = await ctx.prisma.offersOffer.findMany({
include: {
company: true,
offersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
orderBy: [
{
offersFullTime: {
totalCompensation: {
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
},
},
},
],
where: {
profileId: input.profileId,
},
});
if (!offers || offers.length === 0) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No offers found on this profile',
});
}
const overallHighestOffer = offers[0];
if (
!overallHighestOffer.profile.background ||
overallHighestOffer.profile.background.totalYoe == null
) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'YOE not found',
});
}
const yoe = overallHighestOffer.profile.background.totalYoe as number;
const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
const similarOffers = await ctx.prisma.offersOffer.findMany({
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
orderBy: [
{
offersFullTime: {
totalCompensation: {
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
},
},
},
],
where: {
AND: [
{
location: overallHighestOffer.location,
},
{
monthYearReceived: {
gte: monthYearReceived,
},
},
{
OR: [
{
offersFullTime: {
title: overallHighestOffer.offersFullTime?.title,
},
offersIntern: {
title: overallHighestOffer.offersIntern?.title,
},
},
],
},
{
profile: {
background: {
AND: [
{
totalYoe: {
gte: Math.max(yoe - 1, 0),
lte: yoe + 1,
},
},
],
},
},
},
],
},
});
// COMPANY ANALYSIS
const companyMap = new Map<string, Offer>();
offers.forEach((offer) => {
if (companyMap.get(offer.companyId) == null) {
companyMap.set(offer.companyId, offer);
}
});
const companyAnalysis = Array.from(companyMap.values()).map(
(companyOffer) => {
// TODO: Refactor calculating analysis into a function
const similarCompanyOffers = similarOffers.filter(
(offer) => offer.companyId === companyOffer.companyId,
);
const companyIndex = searchOfferPercentile(
companyOffer,
similarCompanyOffers,
);
const companyPercentile =
similarCompanyOffers.length <= 1
? 100
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
// Get top offers (excluding user's offer)
const similarCompanyOffersWithoutUsersOffers =
similarCompanyOffers.filter(
(offer) => offer.profileId !== input.profileId,
);
const noOfSimilarCompanyOffers =
similarCompanyOffersWithoutUsersOffers.length;
const similarCompanyOffers90PercentileIndex = Math.ceil(
noOfSimilarCompanyOffers * 0.1,
);
const topPercentileCompanyOffers =
noOfSimilarCompanyOffers > 2
? similarCompanyOffersWithoutUsersOffers.slice(
similarCompanyOffers90PercentileIndex,
similarCompanyOffers90PercentileIndex + 2,
)
: similarCompanyOffersWithoutUsersOffers;
return {
companyName: companyOffer.company.name,
noOfSimilarOffers: noOfSimilarCompanyOffers,
percentile: companyPercentile,
topSimilarOffers: topPercentileCompanyOffers,
};
},
);
// OVERALL ANALYSIS
const overallIndex = searchOfferPercentile(
overallHighestOffer,
similarOffers,
);
const overallPercentile =
similarOffers.length <= 1
? 100
: 100 - (100 * overallIndex) / (similarOffers.length - 1);
const similarOffersWithoutUsersOffers = similarOffers.filter(
(similarOffer) => similarOffer.profileId !== input.profileId,
);
const noOfSimilarOffers = similarOffersWithoutUsersOffers.length;
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
const topPercentileOffers =
noOfSimilarOffers > 2
? similarOffersWithoutUsersOffers.slice(
similarOffers90PercentileIndex,
similarOffers90PercentileIndex + 2,
)
: similarOffersWithoutUsersOffers;
const analysis = await ctx.prisma.offersAnalysis.create({
data: {
companyAnalysis: {
create: companyAnalysis.map((analysisUnit) => {
return {
companyName: analysisUnit.companyName,
noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
percentile: analysisUnit.percentile,
topSimilarOffers: {
connect: analysisUnit.topSimilarOffers.map((offer) => {
return { id: offer.id };
}),
},
};
}),
},
overallAnalysis: {
create: {
companyName: overallHighestOffer.company.name,
noOfSimilarOffers,
percentile: overallPercentile,
topSimilarOffers: {
connect: topPercentileOffers.map((offer) => {
return { id: offer.id };
}),
},
},
},
overallHighestOffer: {
connect: {
id: overallHighestOffer.id,
},
},
profile: {
connect: {
id: input.profileId,
},
},
},
include: {
companyAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
},
},
});
return profileAnalysisDtoMapper(analysis);
};

@ -9,19 +9,23 @@ export function timeSinceNow(date: Date | number | string) {
let interval = seconds / 31536000;
if (interval > 1) {
return `${Math.floor(interval)} years`;
const time: number = Math.floor(interval);
return time === 1 ? `${time} year` : `${time} years`;
}
interval = seconds / 2592000;
if (interval > 1) {
return `${Math.floor(interval)} months`;
const time: number = Math.floor(interval);
return time === 1 ? `${time} month` : `${time} months`;
}
interval = seconds / 86400;
if (interval > 1) {
return `${Math.floor(interval)} days`;
const time: number = Math.floor(interval);
return time === 1 ? `${time} day` : `${time} days`;
}
interval = seconds / 3600;
if (interval > 1) {
return `${Math.floor(interval)} hours`;
const time: number = Math.floor(interval);
return time === 1 ? `${time} hour` : `${time} hours`;
}
interval = seconds / 60;
if (interval > 1) {

@ -0,0 +1,18 @@
import type {
FilterChoice,
FilterOption,
} from '~/components/questions/filter/FilterSection';
export function companyOptionToSlug(option: FilterChoice): string {
return `${option.id}_${option.label}`;
}
export function slugToCompanyOption(slug: string): FilterOption {
const [id, label] = slug.split('_');
return {
checked: true,
id,
label,
value: id,
};
}

@ -0,0 +1,16 @@
import type { TypeaheadOption } from '@tih/ui';
import type { Location } from '~/types/questions';
export function locationOptionToSlug(
value: Location & TypeaheadOption,
): string {
return [
value.countryId,
value.stateId,
value.cityId,
value.id,
value.label,
value.value,
].join('-');
}

@ -0,0 +1,22 @@
import { useSession } from 'next-auth/react';
import { useCallback, useContext } from 'react';
import { ProtectedContext } from '~/components/questions/protected/ProtectedContextProvider';
export const useProtectedCallback = <T extends Array<unknown>, U>(
callback: (...args: T) => U,
) => {
const { showDialog } = useContext(ProtectedContext);
const { status } = useSession();
const protectedCallback = useCallback(
(...args: T) => {
if (status === 'authenticated') {
return callback(...args);
}
showDialog();
},
[callback, showDialog, status],
);
return protectedCallback;
};
Loading…
Cancel
Save