* main: (55 commits) [offers][feat] add isSaved endpoint [questions][ui] minor ui fixes (#473) [offers][fix] Prevent user's offers from appearing in his own analysis [questions][feat] add useProtectedCallback hook (#472) [offers][fix] Fix save failure message (#474) [questions][feat] add encounters sorting (#458) [resumes][chore] add GA for resumes review page [offers][feat] Add multiple company analysis [questions][ui] Minor UI cleanup (#470) [offers][style] fix offer card style (#469) [questions][feat] add similar questions check (#468) [questions][fix] fix seed questions script (#467) [questions][ui] add temporary logo (#466) [questions][feat] sort answers, comments (#457) [offers][feat] add event tracking and save to profile in submisison page (#465) [offers][fix] sort profiles reverse chronological order [offers][feat] delete comment (#464) [offers][fix] fix error bug in delete comment [offers][fix] Fix comments UI (#463) [offers][feat] save to user profile (#462) ... # Conflicts: # apps/portal/package.json # apps/portal/prisma/seed.ts # apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx # apps/portal/src/components/offers/offerAnalysis/OfferPercentileAnalysisText.tsx # apps/portal/src/mappers/offers-mappers.ts # apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx # apps/portal/src/server/router/offers/offers-analysis-router.ts # apps/portal/src/server/router/offers/offers-profile-router.ts # apps/portal/src/server/router/offers/offers-user-profile-router.tspull/501/head^2
commit
9ff636ae98
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,410 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "country_id": "1", "sortname": "AF", "country_name": "Afghanistan" },
|
||||||
|
{ "country_id": "2", "sortname": "AL", "country_name": "Albania" },
|
||||||
|
{ "country_id": "3", "sortname": "DZ", "country_name": "Algeria" },
|
||||||
|
{ "country_id": "4", "sortname": "AS", "country_name": "American Samoa" },
|
||||||
|
{ "country_id": "5", "sortname": "AD", "country_name": "Andorra" },
|
||||||
|
{ "country_id": "6", "sortname": "AO", "country_name": "Angola" },
|
||||||
|
{ "country_id": "7", "sortname": "AI", "country_name": "Anguilla" },
|
||||||
|
{ "country_id": "8", "sortname": "AQ", "country_name": "Antarctica" },
|
||||||
|
{
|
||||||
|
"country_id": "9",
|
||||||
|
"sortname": "AG",
|
||||||
|
"country_name": "Antigua And Barbuda"
|
||||||
|
},
|
||||||
|
{ "country_id": "10", "sortname": "AR", "country_name": "Argentina" },
|
||||||
|
{ "country_id": "11", "sortname": "AM", "country_name": "Armenia" },
|
||||||
|
{ "country_id": "12", "sortname": "AW", "country_name": "Aruba" },
|
||||||
|
{ "country_id": "13", "sortname": "AU", "country_name": "Australia" },
|
||||||
|
{ "country_id": "14", "sortname": "AT", "country_name": "Austria" },
|
||||||
|
{ "country_id": "15", "sortname": "AZ", "country_name": "Azerbaijan" },
|
||||||
|
{ "country_id": "16", "sortname": "BS", "country_name": "The Bahamas" },
|
||||||
|
{ "country_id": "17", "sortname": "BH", "country_name": "Bahrain" },
|
||||||
|
{ "country_id": "18", "sortname": "BD", "country_name": "Bangladesh" },
|
||||||
|
{ "country_id": "19", "sortname": "BB", "country_name": "Barbados" },
|
||||||
|
{ "country_id": "20", "sortname": "BY", "country_name": "Belarus" },
|
||||||
|
{ "country_id": "21", "sortname": "BE", "country_name": "Belgium" },
|
||||||
|
{ "country_id": "22", "sortname": "BZ", "country_name": "Belize" },
|
||||||
|
{ "country_id": "23", "sortname": "BJ", "country_name": "Benin" },
|
||||||
|
{ "country_id": "24", "sortname": "BM", "country_name": "Bermuda" },
|
||||||
|
{ "country_id": "25", "sortname": "BT", "country_name": "Bhutan" },
|
||||||
|
{ "country_id": "26", "sortname": "BO", "country_name": "Bolivia" },
|
||||||
|
{
|
||||||
|
"country_id": "27",
|
||||||
|
"sortname": "BA",
|
||||||
|
"country_name": "Bosnia and Herzegovina"
|
||||||
|
},
|
||||||
|
{ "country_id": "28", "sortname": "BW", "country_name": "Botswana" },
|
||||||
|
{ "country_id": "29", "sortname": "BV", "country_name": "Bouvet Island" },
|
||||||
|
{ "country_id": "30", "sortname": "BR", "country_name": "Brazil" },
|
||||||
|
{
|
||||||
|
"country_id": "31",
|
||||||
|
"sortname": "IO",
|
||||||
|
"country_name": "British Indian Ocean Territory"
|
||||||
|
},
|
||||||
|
{ "country_id": "32", "sortname": "BN", "country_name": "Brunei" },
|
||||||
|
{ "country_id": "33", "sortname": "BG", "country_name": "Bulgaria" },
|
||||||
|
{ "country_id": "34", "sortname": "BF", "country_name": "Burkina Faso" },
|
||||||
|
{ "country_id": "35", "sortname": "BI", "country_name": "Burundi" },
|
||||||
|
{ "country_id": "36", "sortname": "KH", "country_name": "Cambodia" },
|
||||||
|
{ "country_id": "37", "sortname": "CM", "country_name": "Cameroon" },
|
||||||
|
{ "country_id": "38", "sortname": "CA", "country_name": "Canada" },
|
||||||
|
{ "country_id": "39", "sortname": "CV", "country_name": "Cape Verde" },
|
||||||
|
{ "country_id": "40", "sortname": "KY", "country_name": "Cayman Islands" },
|
||||||
|
{
|
||||||
|
"country_id": "41",
|
||||||
|
"sortname": "CF",
|
||||||
|
"country_name": "Central African Republic"
|
||||||
|
},
|
||||||
|
{ "country_id": "42", "sortname": "TD", "country_name": "Chad" },
|
||||||
|
{ "country_id": "43", "sortname": "CL", "country_name": "Chile" },
|
||||||
|
{ "country_id": "44", "sortname": "CN", "country_name": "China" },
|
||||||
|
{
|
||||||
|
"country_id": "45",
|
||||||
|
"sortname": "CX",
|
||||||
|
"country_name": "Christmas Island"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "46",
|
||||||
|
"sortname": "CC",
|
||||||
|
"country_name": "Cocos (Keeling) Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "47", "sortname": "CO", "country_name": "Colombia" },
|
||||||
|
{ "country_id": "48", "sortname": "KM", "country_name": "Comoros" },
|
||||||
|
{ "country_id": "49", "sortname": "CG", "country_name": "Congo" },
|
||||||
|
{
|
||||||
|
"country_id": "50",
|
||||||
|
"sortname": "CD",
|
||||||
|
"country_name": "Democratic Republic of The Congo"
|
||||||
|
},
|
||||||
|
{ "country_id": "51", "sortname": "CK", "country_name": "Cook Islands" },
|
||||||
|
{ "country_id": "52", "sortname": "CR", "country_name": "Costa Rica" },
|
||||||
|
{
|
||||||
|
"country_id": "53",
|
||||||
|
"sortname": "CI",
|
||||||
|
"country_name": "Cote D'Ivoire (Ivory Coast)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "54",
|
||||||
|
"sortname": "HR",
|
||||||
|
"country_name": "Croatia (Hrvatska)"
|
||||||
|
},
|
||||||
|
{ "country_id": "55", "sortname": "CU", "country_name": "Cuba" },
|
||||||
|
{ "country_id": "56", "sortname": "CY", "country_name": "Cyprus" },
|
||||||
|
{ "country_id": "57", "sortname": "CZ", "country_name": "Czech Republic" },
|
||||||
|
{ "country_id": "58", "sortname": "DK", "country_name": "Denmark" },
|
||||||
|
{ "country_id": "59", "sortname": "DJ", "country_name": "Djibouti" },
|
||||||
|
{ "country_id": "60", "sortname": "DM", "country_name": "Dominica" },
|
||||||
|
{
|
||||||
|
"country_id": "61",
|
||||||
|
"sortname": "DO",
|
||||||
|
"country_name": "Dominican Republic"
|
||||||
|
},
|
||||||
|
{ "country_id": "62", "sortname": "TP", "country_name": "East Timor" },
|
||||||
|
{ "country_id": "63", "sortname": "EC", "country_name": "Ecuador" },
|
||||||
|
{ "country_id": "64", "sortname": "EG", "country_name": "Egypt" },
|
||||||
|
{ "country_id": "65", "sortname": "SV", "country_name": "El Salvador" },
|
||||||
|
{
|
||||||
|
"country_id": "66",
|
||||||
|
"sortname": "GQ",
|
||||||
|
"country_name": "Equatorial Guinea"
|
||||||
|
},
|
||||||
|
{ "country_id": "67", "sortname": "ER", "country_name": "Eritrea" },
|
||||||
|
{ "country_id": "68", "sortname": "EE", "country_name": "Estonia" },
|
||||||
|
{ "country_id": "69", "sortname": "ET", "country_name": "Ethiopia" },
|
||||||
|
{
|
||||||
|
"country_id": "70",
|
||||||
|
"sortname": "XA",
|
||||||
|
"country_name": "External Territories of Australia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "71",
|
||||||
|
"sortname": "FK",
|
||||||
|
"country_name": "Falkland Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "72", "sortname": "FO", "country_name": "Faroe Islands" },
|
||||||
|
{ "country_id": "73", "sortname": "FJ", "country_name": "Fiji Islands" },
|
||||||
|
{ "country_id": "74", "sortname": "FI", "country_name": "Finland" },
|
||||||
|
{ "country_id": "75", "sortname": "FR", "country_name": "France" },
|
||||||
|
{ "country_id": "76", "sortname": "GF", "country_name": "French Guiana" },
|
||||||
|
{
|
||||||
|
"country_id": "77",
|
||||||
|
"sortname": "PF",
|
||||||
|
"country_name": "French Polynesia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "78",
|
||||||
|
"sortname": "TF",
|
||||||
|
"country_name": "French Southern Territories"
|
||||||
|
},
|
||||||
|
{ "country_id": "79", "sortname": "GA", "country_name": "Gabon" },
|
||||||
|
{ "country_id": "80", "sortname": "GM", "country_name": "The Gambia" },
|
||||||
|
{ "country_id": "81", "sortname": "GE", "country_name": "Georgia" },
|
||||||
|
{ "country_id": "82", "sortname": "DE", "country_name": "Germany" },
|
||||||
|
{ "country_id": "83", "sortname": "GH", "country_name": "Ghana" },
|
||||||
|
{ "country_id": "84", "sortname": "GI", "country_name": "Gibraltar" },
|
||||||
|
{ "country_id": "85", "sortname": "GR", "country_name": "Greece" },
|
||||||
|
{ "country_id": "86", "sortname": "GL", "country_name": "Greenland" },
|
||||||
|
{ "country_id": "87", "sortname": "GD", "country_name": "Grenada" },
|
||||||
|
{ "country_id": "88", "sortname": "GP", "country_name": "Guadeloupe" },
|
||||||
|
{ "country_id": "89", "sortname": "GU", "country_name": "Guam" },
|
||||||
|
{ "country_id": "90", "sortname": "GT", "country_name": "Guatemala" },
|
||||||
|
{
|
||||||
|
"country_id": "91",
|
||||||
|
"sortname": "XU",
|
||||||
|
"country_name": "Guernsey and Alderney"
|
||||||
|
},
|
||||||
|
{ "country_id": "92", "sortname": "GN", "country_name": "Guinea" },
|
||||||
|
{ "country_id": "93", "sortname": "GW", "country_name": "Guinea-Bissau" },
|
||||||
|
{ "country_id": "94", "sortname": "GY", "country_name": "Guyana" },
|
||||||
|
{ "country_id": "95", "sortname": "HT", "country_name": "Haiti" },
|
||||||
|
{
|
||||||
|
"country_id": "96",
|
||||||
|
"sortname": "HM",
|
||||||
|
"country_name": "Heard and McDonald Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "97", "sortname": "HN", "country_name": "Honduras" },
|
||||||
|
{
|
||||||
|
"country_id": "98",
|
||||||
|
"sortname": "HK",
|
||||||
|
"country_name": "Hong Kong"
|
||||||
|
},
|
||||||
|
{ "country_id": "99", "sortname": "HU", "country_name": "Hungary" },
|
||||||
|
{ "country_id": "100", "sortname": "IS", "country_name": "Iceland" },
|
||||||
|
{ "country_id": "101", "sortname": "IN", "country_name": "India" },
|
||||||
|
{ "country_id": "102", "sortname": "ID", "country_name": "Indonesia" },
|
||||||
|
{ "country_id": "103", "sortname": "IR", "country_name": "Iran" },
|
||||||
|
{ "country_id": "104", "sortname": "IQ", "country_name": "Iraq" },
|
||||||
|
{ "country_id": "105", "sortname": "IE", "country_name": "Ireland" },
|
||||||
|
{ "country_id": "106", "sortname": "IL", "country_name": "Israel" },
|
||||||
|
{ "country_id": "107", "sortname": "IT", "country_name": "Italy" },
|
||||||
|
{ "country_id": "108", "sortname": "JM", "country_name": "Jamaica" },
|
||||||
|
{ "country_id": "109", "sortname": "JP", "country_name": "Japan" },
|
||||||
|
{ "country_id": "110", "sortname": "XJ", "country_name": "Jersey" },
|
||||||
|
{ "country_id": "111", "sortname": "JO", "country_name": "Jordan" },
|
||||||
|
{ "country_id": "112", "sortname": "KZ", "country_name": "Kazakhstan" },
|
||||||
|
{ "country_id": "113", "sortname": "KE", "country_name": "Kenya" },
|
||||||
|
{ "country_id": "114", "sortname": "KI", "country_name": "Kiribati" },
|
||||||
|
{ "country_id": "115", "sortname": "KP", "country_name": "North Korea" },
|
||||||
|
{ "country_id": "116", "sortname": "KR", "country_name": "South Korea" },
|
||||||
|
{ "country_id": "117", "sortname": "KW", "country_name": "Kuwait" },
|
||||||
|
{ "country_id": "118", "sortname": "KG", "country_name": "Kyrgyzstan" },
|
||||||
|
{ "country_id": "119", "sortname": "LA", "country_name": "Laos" },
|
||||||
|
{ "country_id": "120", "sortname": "LV", "country_name": "Latvia" },
|
||||||
|
{ "country_id": "121", "sortname": "LB", "country_name": "Lebanon" },
|
||||||
|
{ "country_id": "122", "sortname": "LS", "country_name": "Lesotho" },
|
||||||
|
{ "country_id": "123", "sortname": "LR", "country_name": "Liberia" },
|
||||||
|
{ "country_id": "124", "sortname": "LY", "country_name": "Libya" },
|
||||||
|
{ "country_id": "125", "sortname": "LI", "country_name": "Liechtenstein" },
|
||||||
|
{ "country_id": "126", "sortname": "LT", "country_name": "Lithuania" },
|
||||||
|
{ "country_id": "127", "sortname": "LU", "country_name": "Luxembourg" },
|
||||||
|
{ "country_id": "128", "sortname": "MO", "country_name": "Macau" },
|
||||||
|
{ "country_id": "129", "sortname": "MK", "country_name": "Macedonia" },
|
||||||
|
{ "country_id": "130", "sortname": "MG", "country_name": "Madagascar" },
|
||||||
|
{ "country_id": "131", "sortname": "MW", "country_name": "Malawi" },
|
||||||
|
{ "country_id": "132", "sortname": "MY", "country_name": "Malaysia" },
|
||||||
|
{ "country_id": "133", "sortname": "MV", "country_name": "Maldives" },
|
||||||
|
{ "country_id": "134", "sortname": "ML", "country_name": "Mali" },
|
||||||
|
{ "country_id": "135", "sortname": "MT", "country_name": "Malta" },
|
||||||
|
{ "country_id": "136", "sortname": "XM", "country_name": "Isle of Man" },
|
||||||
|
{
|
||||||
|
"country_id": "137",
|
||||||
|
"sortname": "MH",
|
||||||
|
"country_name": "Marshall Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "138", "sortname": "MQ", "country_name": "Martinique" },
|
||||||
|
{ "country_id": "139", "sortname": "MR", "country_name": "Mauritania" },
|
||||||
|
{ "country_id": "140", "sortname": "MU", "country_name": "Mauritius" },
|
||||||
|
{ "country_id": "141", "sortname": "YT", "country_name": "Mayotte" },
|
||||||
|
{ "country_id": "142", "sortname": "MX", "country_name": "Mexico" },
|
||||||
|
{ "country_id": "143", "sortname": "FM", "country_name": "Micronesia" },
|
||||||
|
{ "country_id": "144", "sortname": "MD", "country_name": "Moldova" },
|
||||||
|
{ "country_id": "145", "sortname": "MC", "country_name": "Monaco" },
|
||||||
|
{ "country_id": "146", "sortname": "MN", "country_name": "Mongolia" },
|
||||||
|
{ "country_id": "147", "sortname": "MS", "country_name": "Montserrat" },
|
||||||
|
{ "country_id": "148", "sortname": "MA", "country_name": "Morocco" },
|
||||||
|
{ "country_id": "149", "sortname": "MZ", "country_name": "Mozambique" },
|
||||||
|
{ "country_id": "150", "sortname": "MM", "country_name": "Myanmar" },
|
||||||
|
{ "country_id": "151", "sortname": "NA", "country_name": "Namibia" },
|
||||||
|
{ "country_id": "152", "sortname": "NR", "country_name": "Nauru" },
|
||||||
|
{ "country_id": "153", "sortname": "NP", "country_name": "Nepal" },
|
||||||
|
{
|
||||||
|
"country_id": "154",
|
||||||
|
"sortname": "AN",
|
||||||
|
"country_name": "Netherlands Antilles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "155",
|
||||||
|
"sortname": "NL",
|
||||||
|
"country_name": "Netherlands The"
|
||||||
|
},
|
||||||
|
{ "country_id": "156", "sortname": "NC", "country_name": "New Caledonia" },
|
||||||
|
{ "country_id": "157", "sortname": "NZ", "country_name": "New Zealand" },
|
||||||
|
{ "country_id": "158", "sortname": "NI", "country_name": "Nicaragua" },
|
||||||
|
{ "country_id": "159", "sortname": "NE", "country_name": "Niger" },
|
||||||
|
{ "country_id": "160", "sortname": "NG", "country_name": "Nigeria" },
|
||||||
|
{ "country_id": "161", "sortname": "NU", "country_name": "Niue" },
|
||||||
|
{ "country_id": "162", "sortname": "NF", "country_name": "Norfolk Island" },
|
||||||
|
{
|
||||||
|
"country_id": "163",
|
||||||
|
"sortname": "MP",
|
||||||
|
"country_name": "Northern Mariana Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "164", "sortname": "NO", "country_name": "Norway" },
|
||||||
|
{ "country_id": "165", "sortname": "OM", "country_name": "Oman" },
|
||||||
|
{ "country_id": "166", "sortname": "PK", "country_name": "Pakistan" },
|
||||||
|
{ "country_id": "167", "sortname": "PW", "country_name": "Palau" },
|
||||||
|
{
|
||||||
|
"country_id": "168",
|
||||||
|
"sortname": "PS",
|
||||||
|
"country_name": "Palestinian Territory Occupied"
|
||||||
|
},
|
||||||
|
{ "country_id": "169", "sortname": "PA", "country_name": "Panama" },
|
||||||
|
{
|
||||||
|
"country_id": "170",
|
||||||
|
"sortname": "PG",
|
||||||
|
"country_name": "Papua new Guinea"
|
||||||
|
},
|
||||||
|
{ "country_id": "171", "sortname": "PY", "country_name": "Paraguay" },
|
||||||
|
{ "country_id": "172", "sortname": "PE", "country_name": "Peru" },
|
||||||
|
{ "country_id": "173", "sortname": "PH", "country_name": "Philippines" },
|
||||||
|
{
|
||||||
|
"country_id": "174",
|
||||||
|
"sortname": "PN",
|
||||||
|
"country_name": "Pitcairn Island"
|
||||||
|
},
|
||||||
|
{ "country_id": "175", "sortname": "PL", "country_name": "Poland" },
|
||||||
|
{ "country_id": "176", "sortname": "PT", "country_name": "Portugal" },
|
||||||
|
{ "country_id": "177", "sortname": "PR", "country_name": "Puerto Rico" },
|
||||||
|
{ "country_id": "178", "sortname": "QA", "country_name": "Qatar" },
|
||||||
|
{ "country_id": "179", "sortname": "RE", "country_name": "Reunion" },
|
||||||
|
{ "country_id": "180", "sortname": "RO", "country_name": "Romania" },
|
||||||
|
{ "country_id": "181", "sortname": "RU", "country_name": "Russia" },
|
||||||
|
{ "country_id": "182", "sortname": "RW", "country_name": "Rwanda" },
|
||||||
|
{ "country_id": "183", "sortname": "SH", "country_name": "Saint Helena" },
|
||||||
|
{
|
||||||
|
"country_id": "184",
|
||||||
|
"sortname": "KN",
|
||||||
|
"country_name": "Saint Kitts And Nevis"
|
||||||
|
},
|
||||||
|
{ "country_id": "185", "sortname": "LC", "country_name": "Saint Lucia" },
|
||||||
|
{
|
||||||
|
"country_id": "186",
|
||||||
|
"sortname": "PM",
|
||||||
|
"country_name": "Saint Pierre and Miquelon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "187",
|
||||||
|
"sortname": "VC",
|
||||||
|
"country_name": "Saint Vincent and The Grenadines"
|
||||||
|
},
|
||||||
|
{ "country_id": "188", "sortname": "WS", "country_name": "Samoa" },
|
||||||
|
{ "country_id": "189", "sortname": "SM", "country_name": "San Marino" },
|
||||||
|
{
|
||||||
|
"country_id": "190",
|
||||||
|
"sortname": "ST",
|
||||||
|
"country_name": "Sao Tome and Principe"
|
||||||
|
},
|
||||||
|
{ "country_id": "191", "sortname": "SA", "country_name": "Saudi Arabia" },
|
||||||
|
{ "country_id": "192", "sortname": "SN", "country_name": "Senegal" },
|
||||||
|
{ "country_id": "193", "sortname": "RS", "country_name": "Serbia" },
|
||||||
|
{ "country_id": "194", "sortname": "SC", "country_name": "Seychelles" },
|
||||||
|
{ "country_id": "195", "sortname": "SL", "country_name": "Sierra Leone" },
|
||||||
|
{ "country_id": "196", "sortname": "SG", "country_name": "Singapore" },
|
||||||
|
{ "country_id": "197", "sortname": "SK", "country_name": "Slovakia" },
|
||||||
|
{ "country_id": "198", "sortname": "SI", "country_name": "Slovenia" },
|
||||||
|
{
|
||||||
|
"country_id": "199",
|
||||||
|
"sortname": "XG",
|
||||||
|
"country_name": "Smaller Territories of the UK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "200",
|
||||||
|
"sortname": "SB",
|
||||||
|
"country_name": "Solomon Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "201", "sortname": "SO", "country_name": "Somalia" },
|
||||||
|
{ "country_id": "202", "sortname": "ZA", "country_name": "South Africa" },
|
||||||
|
{ "country_id": "203", "sortname": "GS", "country_name": "South Georgia" },
|
||||||
|
{ "country_id": "204", "sortname": "SS", "country_name": "South Sudan" },
|
||||||
|
{ "country_id": "205", "sortname": "ES", "country_name": "Spain" },
|
||||||
|
{ "country_id": "206", "sortname": "LK", "country_name": "Sri Lanka" },
|
||||||
|
{ "country_id": "207", "sortname": "SD", "country_name": "Sudan" },
|
||||||
|
{ "country_id": "208", "sortname": "SR", "country_name": "Suriname" },
|
||||||
|
{
|
||||||
|
"country_id": "209",
|
||||||
|
"sortname": "SJ",
|
||||||
|
"country_name": "Svalbard And Jan Mayen Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "210", "sortname": "SZ", "country_name": "Swaziland" },
|
||||||
|
{ "country_id": "211", "sortname": "SE", "country_name": "Sweden" },
|
||||||
|
{ "country_id": "212", "sortname": "CH", "country_name": "Switzerland" },
|
||||||
|
{ "country_id": "213", "sortname": "SY", "country_name": "Syria" },
|
||||||
|
{ "country_id": "214", "sortname": "TW", "country_name": "Taiwan" },
|
||||||
|
{ "country_id": "215", "sortname": "TJ", "country_name": "Tajikistan" },
|
||||||
|
{ "country_id": "216", "sortname": "TZ", "country_name": "Tanzania" },
|
||||||
|
{ "country_id": "217", "sortname": "TH", "country_name": "Thailand" },
|
||||||
|
{ "country_id": "218", "sortname": "TG", "country_name": "Togo" },
|
||||||
|
{ "country_id": "219", "sortname": "TK", "country_name": "Tokelau" },
|
||||||
|
{ "country_id": "220", "sortname": "TO", "country_name": "Tonga" },
|
||||||
|
{
|
||||||
|
"country_id": "221",
|
||||||
|
"sortname": "TT",
|
||||||
|
"country_name": "Trinidad And Tobago"
|
||||||
|
},
|
||||||
|
{ "country_id": "222", "sortname": "TN", "country_name": "Tunisia" },
|
||||||
|
{ "country_id": "223", "sortname": "TR", "country_name": "Turkey" },
|
||||||
|
{ "country_id": "224", "sortname": "TM", "country_name": "Turkmenistan" },
|
||||||
|
{
|
||||||
|
"country_id": "225",
|
||||||
|
"sortname": "TC",
|
||||||
|
"country_name": "Turks And Caicos Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "226", "sortname": "TV", "country_name": "Tuvalu" },
|
||||||
|
{ "country_id": "227", "sortname": "UG", "country_name": "Uganda" },
|
||||||
|
{ "country_id": "228", "sortname": "UA", "country_name": "Ukraine" },
|
||||||
|
{
|
||||||
|
"country_id": "229",
|
||||||
|
"sortname": "AE",
|
||||||
|
"country_name": "United Arab Emirates"
|
||||||
|
},
|
||||||
|
{ "country_id": "230", "sortname": "GB", "country_name": "United Kingdom" },
|
||||||
|
{ "country_id": "231", "sortname": "US", "country_name": "United States" },
|
||||||
|
{
|
||||||
|
"country_id": "232",
|
||||||
|
"sortname": "UM",
|
||||||
|
"country_name": "United States Minor Outlying Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "233", "sortname": "UY", "country_name": "Uruguay" },
|
||||||
|
{ "country_id": "234", "sortname": "UZ", "country_name": "Uzbekistan" },
|
||||||
|
{ "country_id": "235", "sortname": "VU", "country_name": "Vanuatu" },
|
||||||
|
{
|
||||||
|
"country_id": "236",
|
||||||
|
"sortname": "VA",
|
||||||
|
"country_name": "Vatican City State (Holy See)"
|
||||||
|
},
|
||||||
|
{ "country_id": "237", "sortname": "VE", "country_name": "Venezuela" },
|
||||||
|
{ "country_id": "238", "sortname": "VN", "country_name": "Vietnam" },
|
||||||
|
{
|
||||||
|
"country_id": "239",
|
||||||
|
"sortname": "VG",
|
||||||
|
"country_name": "Virgin Islands (British)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "240",
|
||||||
|
"sortname": "VI",
|
||||||
|
"country_name": "Virgin Islands (US)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "241",
|
||||||
|
"sortname": "WF",
|
||||||
|
"country_name": "Wallis And Futuna Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "242", "sortname": "EH", "country_name": "Western Sahara" },
|
||||||
|
{ "country_id": "243", "sortname": "YE", "country_name": "Yemen" },
|
||||||
|
{ "country_id": "244", "sortname": "YU", "country_name": "Yugoslavia" },
|
||||||
|
{ "country_id": "245", "sortname": "ZM", "country_name": "Zambia" },
|
||||||
|
{ "country_id": "246", "sortname": "ZW", "country_name": "Zimbabwe" }
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "QuestionsQuestion" ADD COLUMN "numEncounters" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsQuestion_numEncounters_id_idx" ON "QuestionsQuestion"("numEncounters", "id");
|
@ -0,0 +1,44 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Country" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Country_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "State" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"countryId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "State_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "City" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"stateId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "City_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Country_name_key" ON "Country"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Country_code_key" ON "Country"("code");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "State_name_countryId_key" ON "State"("name", "countryId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "City_name_stateId_key" ON "City"("name", "stateId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "State" ADD CONSTRAINT "State_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "City" ADD CONSTRAINT "City_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "State"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `location` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "location",
|
||||||
|
ADD COLUMN "cityId" TEXT,
|
||||||
|
ADD COLUMN "countryId" TEXT,
|
||||||
|
ADD COLUMN "stateId" TEXT,
|
||||||
|
ALTER COLUMN "companyId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswer_updatedAt_id_idx" ON "QuestionsAnswer"("updatedAt", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswer_upvotes_id_idx" ON "QuestionsAnswer"("upvotes", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswerComment_updatedAt_id_idx" ON "QuestionsAnswerComment"("updatedAt", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsAnswerComment_upvotes_id_idx" ON "QuestionsAnswerComment"("upvotes", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsQuestionComment_updatedAt_id_idx" ON "QuestionsQuestionComment"("updatedAt", "id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "QuestionsQuestionComment_upvotes_id_idx" ON "QuestionsQuestionComment"("upvotes", "id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "State"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "QuestionsQuestionEncounter" ADD CONSTRAINT "QuestionsQuestionEncounter_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "QuestionsQuestionType" ADD VALUE 'THEORY';
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `companyPercentile` on the `OffersAnalysis` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `noOfSimilarCompanyOffers` on the `OffersAnalysis` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `noOfSimilarOffers` on the `OffersAnalysis` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `overallPercentile` on the `OffersAnalysis` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `userId` on the `OffersProfile` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the `_TopCompanyOffers` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `_TopOverallOffers` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- Added the required column `overallAnalysisUnitId` to the `OffersAnalysis` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `updatedAt` to the `OffersAnalysis` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "OffersProfile" DROP CONSTRAINT "OffersProfile_userId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_TopCompanyOffers" DROP CONSTRAINT "_TopCompanyOffers_A_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_TopCompanyOffers" DROP CONSTRAINT "_TopCompanyOffers_B_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_TopOverallOffers" DROP CONSTRAINT "_TopOverallOffers_A_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_TopOverallOffers" DROP CONSTRAINT "_TopOverallOffers_B_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "OffersAnalysis" DROP COLUMN "companyPercentile",
|
||||||
|
DROP COLUMN "noOfSimilarCompanyOffers",
|
||||||
|
DROP COLUMN "noOfSimilarOffers",
|
||||||
|
DROP COLUMN "overallPercentile",
|
||||||
|
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ADD COLUMN "overallAnalysisUnitId" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "OffersProfile" DROP COLUMN "userId";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "_TopCompanyOffers";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "_TopOverallOffers";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "OffersAnalysisUnit" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"companyName" TEXT NOT NULL,
|
||||||
|
"percentile" DOUBLE PRECISION NOT NULL,
|
||||||
|
"noOfSimilarOffers" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "OffersAnalysisUnit_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_OffersProfileToUser" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_CompanyAnalysis" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_OffersAnalysisUnitToOffersOffer" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_OffersProfileToUser_AB_unique" ON "_OffersProfileToUser"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_OffersProfileToUser_B_index" ON "_OffersProfileToUser"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_CompanyAnalysis_AB_unique" ON "_CompanyAnalysis"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_CompanyAnalysis_B_index" ON "_CompanyAnalysis"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_OffersAnalysisUnitToOffersOffer_AB_unique" ON "_OffersAnalysisUnitToOffersOffer"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_OffersAnalysisUnitToOffersOffer_B_index" ON "_OffersAnalysisUnitToOffersOffer"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_overallAnalysisUnitId_fkey" FOREIGN KEY ("overallAnalysisUnitId") REFERENCES "OffersAnalysisUnit"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_OffersProfileToUser" ADD CONSTRAINT "_OffersProfileToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_OffersProfileToUser" ADD CONSTRAINT "_OffersProfileToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_CompanyAnalysis" ADD CONSTRAINT "_CompanyAnalysis_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_CompanyAnalysis" ADD CONSTRAINT "_CompanyAnalysis_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersAnalysisUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_OffersAnalysisUnitToOffersOffer" ADD CONSTRAINT "_OffersAnalysisUnitToOffersOffer_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysisUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_OffersAnalysisUnitToOffersOffer" ADD CONSTRAINT "_OffersAnalysisUnitToOffersOffer_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
After Width: | Height: | Size: 351 KiB |
After Width: | Height: | Size: 10 KiB |
@ -1,17 +0,0 @@
|
|||||||
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
|
|
||||||
|
|
||||||
const navigation: ProductNavigationItems = [
|
|
||||||
{ href: '/offers/browse', name: 'Browse all offers' },
|
|
||||||
{ href: '/offers/submit', name: 'Analyze your offers' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
// TODO: Change this to your own GA4 measurement ID.
|
|
||||||
googleAnalyticsMeasurementID: 'G-DBLZDQ2ZZN',
|
|
||||||
navigation,
|
|
||||||
showGlobalNav: false,
|
|
||||||
title: 'Tech Offers Repo',
|
|
||||||
titleHref: '/offers',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
@ -0,0 +1,30 @@
|
|||||||
|
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
|
||||||
|
|
||||||
|
const navigation: ProductNavigationItems = [
|
||||||
|
{ href: '/offers/submit', name: 'Analyze your offers' },
|
||||||
|
{ href: '/offers/features', name: 'Features' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const navigationAuthenticated: ProductNavigationItems = [
|
||||||
|
{ href: '/offers/submit', name: 'Analyze your offers' },
|
||||||
|
{ href: '/offers/dashboard', name: 'Your dashboard' },
|
||||||
|
{ href: '/offers/features', name: 'Features' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
googleAnalyticsMeasurementID: 'G-34XRGLEVCF',
|
||||||
|
logo: (
|
||||||
|
<img alt="Tech Offers Repo" className="h-8 w-auto" src="/offers-logo.svg" />
|
||||||
|
),
|
||||||
|
navigation,
|
||||||
|
showGlobalNav: false,
|
||||||
|
title: 'Tech Offers Repo',
|
||||||
|
titleHref: '/offers',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OffersNavigationAuthenticated = {
|
||||||
|
...config,
|
||||||
|
navigation: navigationAuthenticated,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
@ -0,0 +1,55 @@
|
|||||||
|
import { JobType } from '@prisma/client';
|
||||||
|
import { HorizontalDivider } from '@tih/ui';
|
||||||
|
|
||||||
|
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
|
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||||
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
|
||||||
|
import type { UserProfileOffer } from '~/types/offers';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disableTopDivider?: boolean;
|
||||||
|
offer: UserProfileOffer;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function DashboardProfileCard({
|
||||||
|
disableTopDivider,
|
||||||
|
offer: {
|
||||||
|
company,
|
||||||
|
income,
|
||||||
|
jobType,
|
||||||
|
level,
|
||||||
|
location,
|
||||||
|
monthYearReceived,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!disableTopDivider && <HorizontalDivider />}
|
||||||
|
<div className="flex items-end justify-between">
|
||||||
|
<div className="col-span-1 row-span-3">
|
||||||
|
<p className="font-bold">
|
||||||
|
{getLabelForJobTitleType(title as JobTitleType)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{location
|
||||||
|
? `Company: ${company.name}, ${location}`
|
||||||
|
: `Company: ${company.name}`}
|
||||||
|
</p>
|
||||||
|
{level && <p>Level: {level}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 row-span-3">
|
||||||
|
<p className="text-end">{formatDate(monthYearReceived)}</p>
|
||||||
|
<p className="text-end text-xl">
|
||||||
|
{jobType === JobType.FULLTIME
|
||||||
|
? `${convertMoneyToString(income)} / year`
|
||||||
|
: `${convertMoneyToString(income)} / month`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { ArrowRightIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Button, useToast } from '@tih/ui';
|
||||||
|
|
||||||
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
|
import DashboardOfferCard from '~/components/offers/dashboard/DashboardOfferCard';
|
||||||
|
|
||||||
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import ProfilePhotoHolder from '../profile/ProfilePhotoHolder';
|
||||||
|
|
||||||
|
import type { UserProfile, UserProfileOffer } from '~/types/offers';
|
||||||
|
type Props = Readonly<{
|
||||||
|
profile: UserProfile;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function DashboardProfileCard({
|
||||||
|
profile: { createdAt, id, offers, profileName, token },
|
||||||
|
}: Props) {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const trpcContext = trpc.useContext();
|
||||||
|
const PROFILE_URL = `/offers/profile/${id}?token=${token}`;
|
||||||
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
|
const removeSavedProfileMutation = trpc.useMutation(
|
||||||
|
'offers.user.profile.removeFromUserProfile',
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
title: `Server error.`,
|
||||||
|
variant: 'failure',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
trpcContext.invalidateQueries(['offers.user.profile.getUserProfiles']);
|
||||||
|
showToast({
|
||||||
|
title: `Profile removed from your dashboard successfully!`,
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleRemoveProfile() {
|
||||||
|
removeSavedProfileMutation.mutate({ profileId: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 bg-white px-4 pt-5 sm:px-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="-ml-4 -mt-2 flex flex-wrap items-center justify-between border-b border-gray-300 pb-4 sm:flex-nowrap">
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div>
|
||||||
|
<ProfilePhotoHolder size="sm" />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-10">
|
||||||
|
<p className="text-xl font-bold">{profileName}</p>
|
||||||
|
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span>Created at {formatDate(createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex self-start">
|
||||||
|
<Button
|
||||||
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
|
icon={XMarkIcon}
|
||||||
|
isLabelHidden={true}
|
||||||
|
label="Remove Profile"
|
||||||
|
size="md"
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={handleRemoveProfile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Offers */}
|
||||||
|
<div>
|
||||||
|
{offers.map((offer: UserProfileOffer, index) =>
|
||||||
|
index === 0 ? (
|
||||||
|
<DashboardOfferCard
|
||||||
|
key={offer.id}
|
||||||
|
disableTopDivider={true}
|
||||||
|
offer={offer}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DashboardOfferCard key={offer.id} offer={offer} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end pt-1">
|
||||||
|
<Button
|
||||||
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
|
icon={ArrowRightIcon}
|
||||||
|
isLabelHidden={false}
|
||||||
|
label="Read full profile"
|
||||||
|
size="md"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
gaEvent({
|
||||||
|
action: 'offers.view_profile_from_dashboard',
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'View profile from dashboard',
|
||||||
|
});
|
||||||
|
router.push(PROFILE_URL);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 994 KiB |
After Width: | Height: | Size: 923 KiB |
@ -0,0 +1,42 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||||
|
import type { Placement } from '@popperjs/core';
|
||||||
|
|
||||||
|
type TooltipProps = Readonly<{
|
||||||
|
children: ReactNode;
|
||||||
|
placement?: Placement;
|
||||||
|
tooltipContent: ReactNode;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function Tooltip({
|
||||||
|
children,
|
||||||
|
tooltipContent,
|
||||||
|
placement = 'bottom-start',
|
||||||
|
}: TooltipProps) {
|
||||||
|
const {
|
||||||
|
getTooltipProps,
|
||||||
|
getArrowProps,
|
||||||
|
setTooltipRef,
|
||||||
|
setTriggerRef,
|
||||||
|
visible,
|
||||||
|
} = usePopperTooltip({
|
||||||
|
interactive: true,
|
||||||
|
placement,
|
||||||
|
trigger: ['focus', 'hover'],
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={setTriggerRef}>{children}</div>
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
ref={setTooltipRef}
|
||||||
|
{...getTooltipProps({
|
||||||
|
className: 'tooltip-container ',
|
||||||
|
})}>
|
||||||
|
{tooltipContent}
|
||||||
|
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import type { UseInfiniteQueryResult } from 'react-query';
|
||||||
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
|
export type PaginationLoadMoreButtonProps = {
|
||||||
|
query: UseInfiniteQueryResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PaginationLoadMoreButton(
|
||||||
|
props: PaginationLoadMoreButtonProps,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
query: { hasNextPage, isFetchingNextPage, fetchNextPage },
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
label={hasNextPage ? 'Load more' : 'Nothing more to load'}
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={() => {
|
||||||
|
fetchNextPage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import { Select } from '~/../../../packages/ui/dist';
|
||||||
|
import { SORT_ORDERS, SORT_TYPES } from '~/utils/questions/constants';
|
||||||
|
|
||||||
|
import type { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
|
export type SortOption<Value> = {
|
||||||
|
label: string;
|
||||||
|
value: Value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortTypeOptions = SORT_TYPES;
|
||||||
|
const sortOrderOptions = SORT_ORDERS;
|
||||||
|
|
||||||
|
type SortOrderProps<Order> = {
|
||||||
|
onSortOrderChange?: (sortValue: Order) => void;
|
||||||
|
sortOrderValue: Order;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortTypeProps<Type> = {
|
||||||
|
onSortTypeChange?: (sortType: Type) => void;
|
||||||
|
sortTypeValue: Type;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortOptionsSelectProps = SortOrderProps<SortOrder> &
|
||||||
|
SortTypeProps<SortType>;
|
||||||
|
|
||||||
|
export default function SortOptionsSelect({
|
||||||
|
onSortOrderChange,
|
||||||
|
sortOrderValue,
|
||||||
|
onSortTypeChange,
|
||||||
|
sortTypeValue,
|
||||||
|
}: SortOptionsSelectProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-end justify-end gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
display="inline"
|
||||||
|
label="Sort by"
|
||||||
|
options={sortTypeOptions}
|
||||||
|
value={sortTypeValue}
|
||||||
|
onChange={(value) => {
|
||||||
|
const chosenOption = sortTypeOptions.find(
|
||||||
|
(option) => String(option.value) === value,
|
||||||
|
);
|
||||||
|
if (chosenOption) {
|
||||||
|
onSortTypeChange?.(chosenOption.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
display="inline"
|
||||||
|
label="Order by"
|
||||||
|
options={sortOrderOptions}
|
||||||
|
value={sortOrderValue}
|
||||||
|
onChange={(value) => {
|
||||||
|
const chosenOption = sortOrderOptions.find(
|
||||||
|
(option) => String(option.value) === value,
|
||||||
|
);
|
||||||
|
if (chosenOption) {
|
||||||
|
onSortOrderChange?.(chosenOption.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,21 +1,71 @@
|
|||||||
import { LOCATIONS } from '~/utils/questions/constants';
|
import { useMemo, useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
||||||
import ExpandedTypeahead from './ExpandedTypeahead';
|
import ExpandedTypeahead from './ExpandedTypeahead';
|
||||||
|
|
||||||
|
import type { Location } from '~/types/questions';
|
||||||
|
|
||||||
export type LocationTypeaheadProps = Omit<
|
export type LocationTypeaheadProps = Omit<
|
||||||
ExpandedTypeaheadProps,
|
ExpandedTypeaheadProps,
|
||||||
'label' | 'onQueryChange' | 'options'
|
'label' | 'onQueryChange' | 'onSelect' | 'onSuggestionClick' | 'options'
|
||||||
>;
|
> & {
|
||||||
|
onSelect: (option: Location & TypeaheadOption) => void;
|
||||||
|
onSuggestionClick?: (option: Location) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LocationTypeahead({
|
||||||
|
onSelect,
|
||||||
|
onSuggestionClick,
|
||||||
|
...restProps
|
||||||
|
}: LocationTypeaheadProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const { data: locations } = trpc.useQuery([
|
||||||
|
'locations.cities.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const locationOptions = useMemo(() => {
|
||||||
|
return (
|
||||||
|
locations?.map(({ id, name, state }) => ({
|
||||||
|
cityId: id,
|
||||||
|
countryId: state.country.id,
|
||||||
|
id,
|
||||||
|
label: `${name}, ${state.name}, ${state.country.name}`,
|
||||||
|
stateId: state.id,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}, [locations]);
|
||||||
|
|
||||||
export default function LocationTypeahead(props: LocationTypeaheadProps) {
|
|
||||||
return (
|
return (
|
||||||
<ExpandedTypeahead
|
<ExpandedTypeahead
|
||||||
{...(props as ExpandedTypeaheadProps)}
|
{...({
|
||||||
|
onSuggestionClick: onSuggestionClick
|
||||||
|
? (option: TypeaheadOption) => {
|
||||||
|
const location = locationOptions.find(
|
||||||
|
(locationOption) => locationOption.id === option.id,
|
||||||
|
)!;
|
||||||
|
onSuggestionClick({
|
||||||
|
...location,
|
||||||
|
...option,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
...restProps,
|
||||||
|
} as ExpandedTypeaheadProps)}
|
||||||
label="Location"
|
label="Location"
|
||||||
options={LOCATIONS}
|
options={locationOptions}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
onQueryChange={setQuery}
|
||||||
onQueryChange={() => {}}
|
onSelect={({ id }: TypeaheadOption) => {
|
||||||
|
const location = locationOptions.find((option) => option.id === id)!;
|
||||||
|
onSelect(location);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,34 @@
|
|||||||
import { ROLES } from '~/utils/questions/constants';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
||||||
import ExpandedTypeahead from './ExpandedTypeahead';
|
import ExpandedTypeahead from './ExpandedTypeahead';
|
||||||
|
import type { FilterChoices } from '../filter/FilterSection';
|
||||||
|
|
||||||
export type RoleTypeaheadProps = Omit<
|
export type RoleTypeaheadProps = Omit<
|
||||||
ExpandedTypeaheadProps,
|
ExpandedTypeaheadProps,
|
||||||
'label' | 'onQueryChange' | 'options'
|
'label' | 'onQueryChange' | 'options'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
|
||||||
|
([slug, label]) => ({
|
||||||
|
id: slug,
|
||||||
|
label,
|
||||||
|
value: slug,
|
||||||
|
}),
|
||||||
|
);
|
||||||
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandedTypeahead
|
<ExpandedTypeahead
|
||||||
{...(props as ExpandedTypeaheadProps)}
|
{...(props as ExpandedTypeaheadProps)}
|
||||||
label="Role"
|
label="Role"
|
||||||
options={ROLES}
|
options={ROLES.filter((option) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
option.label.toLowerCase().includes(query.toLowerCase()),
|
||||||
onQueryChange={() => {}}
|
)}
|
||||||
|
onQueryChange={setQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
import { Typeahead } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disabled?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
isLabelHidden?: boolean;
|
||||||
|
label?: string;
|
||||||
|
onSelect: (option: TypeaheadOption | null) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
required?: boolean;
|
||||||
|
value?: TypeaheadOption | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function CitiesTypeahead({
|
||||||
|
disabled,
|
||||||
|
label = 'City',
|
||||||
|
onSelect,
|
||||||
|
isLabelHidden,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
value,
|
||||||
|
}: Props) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const cities = trpc.useQuery([
|
||||||
|
'locations.cities.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data } = cities;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typeahead
|
||||||
|
disabled={disabled}
|
||||||
|
isLabelHidden={isLabelHidden}
|
||||||
|
label={label}
|
||||||
|
noResultsMessage="No cities found"
|
||||||
|
nullable={true}
|
||||||
|
options={
|
||||||
|
data?.map(({ id, name, state }) => ({
|
||||||
|
id,
|
||||||
|
label: `${name}, ${state?.name}, ${state?.country?.name}`,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
textSize="inherit"
|
||||||
|
value={value}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
import { Typeahead } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disabled?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
isLabelHidden?: boolean;
|
||||||
|
onSelect: (option: TypeaheadOption | null) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
required?: boolean;
|
||||||
|
value?: TypeaheadOption | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function CountriesTypeahead({
|
||||||
|
disabled,
|
||||||
|
onSelect,
|
||||||
|
isLabelHidden,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
value,
|
||||||
|
}: Props) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const countries = trpc.useQuery([
|
||||||
|
'locations.countries.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data } = countries;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typeahead
|
||||||
|
disabled={disabled}
|
||||||
|
isLabelHidden={isLabelHidden}
|
||||||
|
label="Country"
|
||||||
|
noResultsMessage="No countries found"
|
||||||
|
nullable={true}
|
||||||
|
options={
|
||||||
|
data?.map(({ id, name }) => ({
|
||||||
|
id,
|
||||||
|
label: name,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
textSize="inherit"
|
||||||
|
value={value}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Banner } from '@tih/ui';
|
|
||||||
|
|
||||||
import OffersTable from '~/components/offers/table/OffersTable';
|
|
||||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
|
||||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
|
||||||
|
|
||||||
export default function OffersHomePage() {
|
|
||||||
const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
|
|
||||||
const [companyFilter, setCompanyFilter] = useState('');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex-1 overflow-y-auto">
|
|
||||||
<Banner size="sm">
|
|
||||||
⭐ Check if your offer is competitive by submitting it{' '}
|
|
||||||
<Link className="underline" href="/offers/submit">
|
|
||||||
here
|
|
||||||
</Link>
|
|
||||||
. ⭐
|
|
||||||
</Banner>
|
|
||||||
<div className="bg-slate-100 py-16 px-4">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-primary-600 text-center text-4xl font-bold sm:text-5xl">
|
|
||||||
Tech Offers Repo
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 text-center text-lg text-slate-600 sm:text-2xl">
|
|
||||||
Find out how good your offer is. Discover how others got their
|
|
||||||
offers.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flex flex-col items-center justify-center space-y-2 text-base text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg">
|
|
||||||
<span>Viewing offers for</span>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<JobTitlesTypeahead
|
|
||||||
isLabelHidden={true}
|
|
||||||
placeHolder="Software Engineer"
|
|
||||||
onSelect={({ value }) => setjobTitleFilter(value)}
|
|
||||||
/>
|
|
||||||
<span>in</span>
|
|
||||||
<CompaniesTypeahead
|
|
||||||
isLabelHidden={true}
|
|
||||||
placeHolder="All Companies"
|
|
||||||
onSelect={({ value }) => setCompanyFilter(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center bg-white pb-20 pt-10">
|
|
||||||
<OffersTable
|
|
||||||
companyFilter={companyFilter}
|
|
||||||
jobTitleFilter={jobTitleFilter}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,95 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { signIn, useSession } from 'next-auth/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, Spinner } from '@tih/ui';
|
||||||
|
|
||||||
|
import DashboardOfferCard from '~/components/offers/dashboard/DashboardProfileCard';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import type { UserProfile } from '~/types/offers';
|
||||||
|
|
||||||
|
export default function ProfilesDashboard() {
|
||||||
|
const { status } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);
|
||||||
|
|
||||||
|
const userProfilesQuery = trpc.useQuery(
|
||||||
|
['offers.user.profile.getUserProfiles'],
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
if (error.data?.code === 'UNAUTHORIZED') {
|
||||||
|
signIn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (response: Array<UserProfile>) => {
|
||||||
|
setUserProfiles(response);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 'loading' || userProfilesQuery.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'unauthenticated') {
|
||||||
|
signIn();
|
||||||
|
}
|
||||||
|
if (userProfiles.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center text-xl">
|
||||||
|
<div className="mb-8 flex w-full flex-row justify-center">
|
||||||
|
<h2>You have not saved any offer profiles yet.</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-center">
|
||||||
|
<Button
|
||||||
|
label="Submit your offers now!"
|
||||||
|
size="lg"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => router.push('/offers/submit')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{userProfilesQuery.isLoading && (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!userProfilesQuery.isLoading && (
|
||||||
|
<div className="mt-8 overflow-y-auto">
|
||||||
|
<h1 className="mx-auto mb-4 w-3/4 text-start text-4xl font-bold text-slate-900">
|
||||||
|
Your dashboard
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto w-3/4 text-start text-xl text-slate-900">
|
||||||
|
Save your offer profiles to dashboard to easily access and edit them
|
||||||
|
later.
|
||||||
|
</p>
|
||||||
|
<div className="justfy-center mt-8 flex w-screen">
|
||||||
|
<ul className="mx-auto w-3/4 space-y-3" role="list">
|
||||||
|
{userProfiles?.map((profile) => (
|
||||||
|
<li
|
||||||
|
key={profile.id}
|
||||||
|
className="overflow-hidden bg-white px-4 py-4 shadow sm:rounded-md sm:px-6">
|
||||||
|
<DashboardOfferCard key={profile.id} profile={profile} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,251 @@
|
|||||||
|
import type { SVGProps } from 'react';
|
||||||
|
import {
|
||||||
|
BookmarkSquareIcon,
|
||||||
|
ChartBarSquareIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
ShareIcon,
|
||||||
|
TableCellsIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
import offersAnalysis from '~/components/offers/features/images/offers-analysis.png';
|
||||||
|
import offersBrowse from '~/components/offers/features/images/offers-browse.png';
|
||||||
|
import offersProfile from '~/components/offers/features/images/offers-profile.png';
|
||||||
|
import LeftTextCard from '~/components/offers/features/LeftTextCard';
|
||||||
|
import RightTextCard from '~/components/offers/features/RightTextCard';
|
||||||
|
import { HOME_URL } from '~/components/offers/types';
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'Profile names are randomly generated to keep your offers strictly anonymous.',
|
||||||
|
icon: UsersIcon,
|
||||||
|
name: 'Anonymized Profile Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'Only users with the edit link can edit that profile. Share profiles to others using a public link without giving edit permission.',
|
||||||
|
icon: ShareIcon,
|
||||||
|
name: 'Edit Link v.s. Public Link',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Offer profiles will not be automatically saved under creators' account in our database unless explicit permission is given.",
|
||||||
|
icon: BookmarkSquareIcon,
|
||||||
|
name: 'Save with Permission',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const footerNavigation = {
|
||||||
|
social: [
|
||||||
|
// {
|
||||||
|
// href: '#',
|
||||||
|
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||||
|
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||||
|
// <path
|
||||||
|
// clipRule="evenodd"
|
||||||
|
// d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
|
||||||
|
// fillRule="evenodd"
|
||||||
|
// />
|
||||||
|
// </svg>
|
||||||
|
// ),
|
||||||
|
// name: 'Facebook',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// href: '#',
|
||||||
|
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||||
|
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||||
|
// <path
|
||||||
|
// clipRule="evenodd"
|
||||||
|
// d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
|
||||||
|
// fillRule="evenodd"
|
||||||
|
// />
|
||||||
|
// </svg>
|
||||||
|
// ),
|
||||||
|
// name: 'Instagram',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
href: 'https://github.com/yangshun/tech-interview-handbook',
|
||||||
|
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: 'GitHub',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full overflow-y-auto bg-white">
|
||||||
|
<main>
|
||||||
|
{/* Hero section */}
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
|
||||||
|
<img
|
||||||
|
alt="Tech Offers Repo"
|
||||||
|
className="mx-auto mb-8 w-auto"
|
||||||
|
src="/offers-logo.svg"
|
||||||
|
/>
|
||||||
|
<h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
|
||||||
|
<span>Choosing offers </span>
|
||||||
|
<span className="from-primary-600 -mb-1 mr-2 bg-gradient-to-r to-purple-500 bg-clip-text pb-1 pr-4 italic text-transparent">
|
||||||
|
made easier
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto mt-6 max-w-lg text-center text-xl sm:max-w-3xl">
|
||||||
|
Analyze your offers using profiles from fellow software engineers.
|
||||||
|
</p>
|
||||||
|
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
|
||||||
|
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-1 sm:gap-5 sm:space-y-0">
|
||||||
|
<a
|
||||||
|
className="border-grey-600 flex items-center justify-center rounded-md border bg-white px-4 py-3 text-base font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 sm:px-8"
|
||||||
|
href={HOME_URL}>
|
||||||
|
Get started
|
||||||
|
</a>
|
||||||
|
{/* <a
|
||||||
|
className="bg-primary-500 flex items-center justify-center rounded-md border border-transparent px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-opacity-70 sm:px-8"
|
||||||
|
href="#">
|
||||||
|
Live demo
|
||||||
|
</a> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Alternating Feature Sections */}
|
||||||
|
<div className="relative overflow-hidden pt-16 pb-32">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 top-0 h-48 bg-gradient-to-b from-gray-100"
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<LeftTextCard
|
||||||
|
description="Filter relevant offers by job title, company, submission date, salary and more."
|
||||||
|
icon={
|
||||||
|
<TableCellsIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
imageAlt="Offer table page"
|
||||||
|
imageSrc={offersBrowse}
|
||||||
|
title="Stay informed of recent offers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-36">
|
||||||
|
<RightTextCard
|
||||||
|
description="With our offer engine analysis, you can compare your offers with other offers on the market and make an informed decision."
|
||||||
|
icon={
|
||||||
|
<ChartBarSquareIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
imageAlt="Customer profile user interface"
|
||||||
|
imageSrc={offersAnalysis}
|
||||||
|
title="Better understand your offers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-36">
|
||||||
|
<LeftTextCard
|
||||||
|
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
|
||||||
|
icon={
|
||||||
|
<InformationCircleIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
imageAlt="Offer table page"
|
||||||
|
imageSrc={offersProfile}
|
||||||
|
title="Choosing an offer needs context"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient Feature Section */}
|
||||||
|
<div className="to-primary-600 bg-gradient-to-r from-purple-800">
|
||||||
|
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 sm:pt-20 sm:pb-24 lg:max-w-7xl lg:px-8 lg:pt-24">
|
||||||
|
<h2 className="flex justify-center text-4xl font-bold tracking-tight text-white">
|
||||||
|
Your privacy is our priority.
|
||||||
|
</h2>
|
||||||
|
<p className="text-primary-100 mt-4 flex flex-row justify-center text-lg">
|
||||||
|
All offer profiles are anonymized and we do not store information
|
||||||
|
about your personal identity.
|
||||||
|
</p>
|
||||||
|
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div key={feature.name}>
|
||||||
|
<div>
|
||||||
|
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-white bg-opacity-10">
|
||||||
|
<feature.icon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-lg font-medium text-white">
|
||||||
|
{feature.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-primary-100 mt-2 text-base">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<div className="bg-white">
|
||||||
|
<div className="mx-auto max-w-4xl py-16 px-4 sm:px-6 sm:py-24 lg:flex lg:max-w-7xl lg:items-center lg:justify-between lg:px-8">
|
||||||
|
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||||
|
<span className="block">Ready to get started?</span>
|
||||||
|
<span className="to-primary-600 -mb-1 block bg-gradient-to-r from-purple-600 bg-clip-text pb-1 text-transparent">
|
||||||
|
Create your own offer profile today.
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div className="mt-6 space-y-4 sm:flex sm:space-y-0 sm:space-x-5">
|
||||||
|
<a
|
||||||
|
className="to-primary-500 flex items-center justify-center rounded-md border border-transparent bg-gradient-to-r from-purple-600 bg-origin-border px-4 py-3 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
|
||||||
|
href={HOME_URL}>
|
||||||
|
Get Started
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer aria-labelledby="footer-heading" className="bg-gray-50">
|
||||||
|
<h2 className="sr-only" id="footer-heading">
|
||||||
|
Footer
|
||||||
|
</h2>
|
||||||
|
<div className="mx-auto max-w-7xl px-4 pt-0 pb-8 sm:px-6 lg:px-8">
|
||||||
|
<div className="mt-12 border-t border-gray-200 pt-8 md:flex md:items-center md:justify-between lg:mt-16">
|
||||||
|
<div className="flex space-x-6 md:order-2">
|
||||||
|
{footerNavigation.social.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
className="text-gray-400 hover:text-gray-500"
|
||||||
|
href={item.href}>
|
||||||
|
<span className="sr-only">{item.name}</span>
|
||||||
|
<item.icon aria-hidden="true" className="h-6 w-6" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-8 text-base text-gray-400 md:order-1 md:mt-0">
|
||||||
|
© 2022 Tech Offers Repo. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,244 +1,79 @@
|
|||||||
import type { SVGProps } from 'react';
|
import Link from 'next/link';
|
||||||
import {
|
import { useState } from 'react';
|
||||||
BookmarkSquareIcon,
|
import { Banner } from '@tih/ui';
|
||||||
ChartBarSquareIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
ShareIcon,
|
|
||||||
TableCellsIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from '@heroicons/react/24/outline';
|
|
||||||
|
|
||||||
import LeftTextCard from '~/components/offers/landing/LeftTextCard';
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
import RightTextCard from '~/components/offers/landing/RightTextCard';
|
import OffersTable from '~/components/offers/table/OffersTable';
|
||||||
import { HOME_URL } from '~/components/offers/types';
|
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||||
|
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||||
|
|
||||||
const features = [
|
export default function OffersHomePage() {
|
||||||
{
|
const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
|
||||||
description:
|
const [companyFilter, setCompanyFilter] = useState('');
|
||||||
'Profile names are randomly generated to keep your offers strictly anonymous.',
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
icon: UsersIcon,
|
|
||||||
name: 'Anonymized Profile Name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
'Only users with the edit link can edit that profile. Share profiles to others using a public link without giving edit permission.',
|
|
||||||
icon: ShareIcon,
|
|
||||||
name: 'Edit Link v.s. Public Link',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"Offer profiles will not be automatically saved under creators' account in our database unless explicit permission is given.",
|
|
||||||
icon: BookmarkSquareIcon,
|
|
||||||
name: 'Save with Permission',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const footerNavigation = {
|
|
||||||
social: [
|
|
||||||
{
|
|
||||||
href: '#',
|
|
||||||
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
name: 'Facebook',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '#',
|
|
||||||
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
name: 'Instagram',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'https://github.com/yangshun/tech-interview-handbook',
|
|
||||||
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
name: 'GitHub',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LandingPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full overflow-y-auto bg-white">
|
<main className="flex-1 overflow-y-auto">
|
||||||
<main>
|
<Banner size="sm">
|
||||||
{/* Hero section */}
|
⭐ Check if your offer is competitive by submitting it{' '}
|
||||||
<div className="relative h-full">
|
<Link className="underline" href="/offers/submit">
|
||||||
<div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
|
here
|
||||||
<h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
|
</Link>
|
||||||
<span>Choosing offers </span>
|
. ⭐
|
||||||
<span className="from-primary-600 -mb-1 mr-2 bg-gradient-to-r to-purple-500 bg-clip-text pb-1 pr-4 italic text-transparent">
|
</Banner>
|
||||||
made easier
|
<div className="bg-slate-100 py-16 px-4">
|
||||||
</span>
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-primary-600 text-center text-4xl font-bold sm:text-5xl">
|
||||||
|
Tech Offers Repo
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto mt-6 max-w-lg text-center text-xl sm:max-w-3xl">
|
|
||||||
Analyze your offers using profiles from fellow software engineers.
|
|
||||||
</p>
|
|
||||||
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
|
|
||||||
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0">
|
|
||||||
<a
|
|
||||||
className="border-grey-600 flex items-center justify-center rounded-md border bg-white px-4 py-3 text-base font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 sm:px-8"
|
|
||||||
href={HOME_URL}>
|
|
||||||
Get started
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="bg-primary-500 flex items-center justify-center rounded-md border border-transparent px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-opacity-70 sm:px-8"
|
|
||||||
href="#">
|
|
||||||
Live demo
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-4 text-center text-lg text-slate-600 sm:text-2xl">
|
||||||
|
Find out how good your offer is. Discover how others got their
|
||||||
{/* Alternating Feature Sections */}
|
offers.
|
||||||
<div className="relative overflow-hidden pt-16 pb-32">
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-x-0 top-0 h-48 bg-gradient-to-b from-gray-100"
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
|
||||||
<LeftTextCard
|
|
||||||
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
|
|
||||||
icon={
|
|
||||||
<InformationCircleIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-6 w-6 text-white"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
imageAlt="Offer table page"
|
|
||||||
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
|
|
||||||
title="Choosing an offer needs context"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-36">
|
</div>
|
||||||
<RightTextCard
|
<div className="mt-6 flex flex-col items-center justify-center space-y-2 text-base text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg">
|
||||||
description="With our offer engine analysis, you can compare your offers with other offers on the market and make an informed decision."
|
<span>Viewing offers for</span>
|
||||||
icon={
|
<div className="flex items-center space-x-4">
|
||||||
<ChartBarSquareIcon
|
<JobTitlesTypeahead
|
||||||
aria-hidden="true"
|
isLabelHidden={true}
|
||||||
className="h-6 w-6 text-white"
|
placeholder="Software Engineer"
|
||||||
/>
|
onSelect={(option) => {
|
||||||
}
|
if (option) {
|
||||||
imageAlt="Customer profile user interface"
|
setjobTitleFilter(option.value);
|
||||||
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-2.jpg"
|
gaEvent({
|
||||||
title="Better understand your offers"
|
action: `offers.table_filter_job_title_${option.value}`,
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Filter by job title',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
<span>in</span>
|
||||||
<div className="mt-36">
|
<CompaniesTypeahead
|
||||||
<LeftTextCard
|
isLabelHidden={true}
|
||||||
description="Filter relevant offers by job title, company, submission date, salary and more."
|
placeholder="All Companies"
|
||||||
icon={
|
onSelect={(option) => {
|
||||||
<TableCellsIcon
|
if (option) {
|
||||||
aria-hidden="true"
|
setCompanyFilter(option.value);
|
||||||
className="h-6 w-6 text-white"
|
gaEvent({
|
||||||
/>
|
action: 'offers.table_filter_company',
|
||||||
}
|
category: 'engagement',
|
||||||
imageAlt="Offer table page"
|
label: 'Filter by company',
|
||||||
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
|
});
|
||||||
title="Stay informed of recent offers"
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Gradient Feature Section */}
|
<div className="flex justify-center bg-white pb-20 pt-10">
|
||||||
<div className="to-primary-600 bg-gradient-to-r from-purple-800">
|
<OffersTable
|
||||||
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 sm:pt-20 sm:pb-24 lg:max-w-7xl lg:px-8 lg:pt-24">
|
companyFilter={companyFilter}
|
||||||
<h2 className="flex justify-center text-4xl font-bold tracking-tight text-white">
|
jobTitleFilter={jobTitleFilter}
|
||||||
Your privacy is our priority.
|
/>
|
||||||
</h2>
|
</div>
|
||||||
<p className="text-primary-100 mt-4 flex flex-row justify-center text-lg">
|
</main>
|
||||||
All offer profiles are anonymized and we do not store information
|
|
||||||
about your personal identity.
|
|
||||||
</p>
|
|
||||||
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<div key={feature.name}>
|
|
||||||
<div>
|
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-white bg-opacity-10">
|
|
||||||
<feature.icon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-6 w-6 text-white"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-lg font-medium text-white">
|
|
||||||
{feature.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-primary-100 mt-2 text-base">
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<div className="bg-white">
|
|
||||||
<div className="mx-auto max-w-4xl py-16 px-4 sm:px-6 sm:py-24 lg:flex lg:max-w-7xl lg:items-center lg:justify-between lg:px-8">
|
|
||||||
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
|
||||||
<span className="block">Ready to get started?</span>
|
|
||||||
<span className="to-primary-600 -mb-1 block bg-gradient-to-r from-purple-600 bg-clip-text pb-1 text-transparent">
|
|
||||||
Create your own offer profile today.
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div className="mt-6 space-y-4 sm:flex sm:space-y-0 sm:space-x-5">
|
|
||||||
<a
|
|
||||||
className="to-primary-500 flex items-center justify-center rounded-md border border-transparent bg-gradient-to-r from-purple-600 bg-origin-border px-4 py-3 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
|
|
||||||
href={HOME_URL}>
|
|
||||||
Get Started
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer aria-labelledby="footer-heading" className="bg-gray-50">
|
|
||||||
<h2 className="sr-only" id="footer-heading">
|
|
||||||
Footer
|
|
||||||
</h2>
|
|
||||||
<div className="mx-auto max-w-7xl px-4 pt-0 pb-8 sm:px-6 lg:px-8">
|
|
||||||
<div className="mt-12 border-t border-gray-200 pt-8 md:flex md:items-center md:justify-between lg:mt-16">
|
|
||||||
<div className="flex space-x-6 md:order-2">
|
|
||||||
{footerNavigation.social.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
className="text-gray-400 hover:text-gray-500"
|
|
||||||
href={item.href}>
|
|
||||||
<span className="sr-only">{item.name}</span>
|
|
||||||
<item.icon aria-hidden="true" className="h-6 w-6" />
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="mt-8 text-base text-gray-400 md:order-1 md:mt-0">
|
|
||||||
© 2022 Tech Interview Handbook Offer Profile Repository. All
|
|
||||||
rights reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createRouter } from './context';
|
||||||
|
|
||||||
|
export const locationsRouter = createRouter()
|
||||||
|
.query('cities.list', {
|
||||||
|
input: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
return await ctx.prisma.city.findMany({
|
||||||
|
orderBy: {
|
||||||
|
name: 'asc',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
state: {
|
||||||
|
select: {
|
||||||
|
country: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
contains: input.name,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.query('countries.list', {
|
||||||
|
input: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
return await ctx.prisma.country.findMany({
|
||||||
|
orderBy: {
|
||||||
|
name: 'asc',
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
contains: input.name,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -1,275 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { TRPCError } from '@trpc/server';
|
|
||||||
|
|
||||||
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
|
|
||||||
|
|
||||||
import { createProtectedRouter } from './context';
|
|
||||||
|
|
||||||
export const questionsListRouter = createProtectedRouter()
|
|
||||||
.query('getListsByUser', {
|
|
||||||
async resolve({ ctx }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
// TODO: Optimize by not returning question entries
|
|
||||||
const questionsLists = await ctx.prisma.questionsList.findMany({
|
|
||||||
include: {
|
|
||||||
questionEntries: {
|
|
||||||
include: {
|
|
||||||
question: {
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
answers: true,
|
|
||||||
comments: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
encounters: {
|
|
||||||
select: {
|
|
||||||
company: true,
|
|
||||||
location: true,
|
|
||||||
role: true,
|
|
||||||
seenAt: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'asc',
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const lists = questionsLists.map((list) => ({
|
|
||||||
...list,
|
|
||||||
questionEntries: list.questionEntries.map((entry) => ({
|
|
||||||
...entry,
|
|
||||||
question: createQuestionWithAggregateData(entry.question),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return lists;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.query('getListById', {
|
|
||||||
input: z.object({
|
|
||||||
listId: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { listId } = input;
|
|
||||||
|
|
||||||
const questionList = await ctx.prisma.questionsList.findFirst({
|
|
||||||
include: {
|
|
||||||
questionEntries: {
|
|
||||||
include: {
|
|
||||||
question: {
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
answers: true,
|
|
||||||
comments: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
encounters: {
|
|
||||||
select: {
|
|
||||||
company: true,
|
|
||||||
location: true,
|
|
||||||
role: true,
|
|
||||||
seenAt: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'asc',
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: listId,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!questionList) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'NOT_FOUND',
|
|
||||||
message: 'Question list not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...questionList,
|
|
||||||
questionEntries: questionList.questionEntries.map((questionEntry) => ({
|
|
||||||
...questionEntry,
|
|
||||||
question: createQuestionWithAggregateData(questionEntry.question),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('create', {
|
|
||||||
input: z.object({
|
|
||||||
name: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const { name } = input;
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsList.create({
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('update', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { name, id } = input;
|
|
||||||
|
|
||||||
const listToUpdate = await ctx.prisma.questionsList.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (listToUpdate?.id !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsList.update({
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('delete', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const listToDelete = await ctx.prisma.questionsList.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (listToDelete?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsList.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('createQuestionEntry', {
|
|
||||||
input: z.object({
|
|
||||||
listId: z.string(),
|
|
||||||
questionId: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const listToAugment = await ctx.prisma.questionsList.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.listId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (listToAugment?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { questionId, listId } = input;
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsListQuestionEntry.create({
|
|
||||||
data: {
|
|
||||||
listId,
|
|
||||||
questionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('deleteQuestionEntry', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const entryToDelete =
|
|
||||||
await ctx.prisma.questionsListQuestionEntry.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (entryToDelete === null) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'NOT_FOUND',
|
|
||||||
message: 'Entry not found.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const listToAugment = await ctx.prisma.questionsList.findUnique({
|
|
||||||
where: {
|
|
||||||
id: entryToDelete.listId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (listToAugment?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsListQuestionEntry.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,437 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { QuestionsQuestionType, Vote } from '@prisma/client';
|
|
||||||
import { TRPCError } from '@trpc/server';
|
|
||||||
|
|
||||||
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
|
|
||||||
|
|
||||||
import { createProtectedRouter } from './context';
|
|
||||||
|
|
||||||
import { SortOrder, SortType } from '~/types/questions.d';
|
|
||||||
|
|
||||||
export const questionsQuestionRouter = createProtectedRouter()
|
|
||||||
.query('getQuestionsByFilter', {
|
|
||||||
input: z.object({
|
|
||||||
companyNames: z.string().array(),
|
|
||||||
cursor: z
|
|
||||||
.object({
|
|
||||||
idCursor: z.string().optional(),
|
|
||||||
lastSeenCursor: z.date().nullish().optional(),
|
|
||||||
upvoteCursor: z.number().optional(),
|
|
||||||
})
|
|
||||||
.nullish(),
|
|
||||||
endDate: z.date().default(new Date()),
|
|
||||||
limit: z.number().min(1).default(50),
|
|
||||||
locations: z.string().array(),
|
|
||||||
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
|
|
||||||
roles: z.string().array(),
|
|
||||||
sortOrder: z.nativeEnum(SortOrder),
|
|
||||||
sortType: z.nativeEnum(SortType),
|
|
||||||
startDate: z.date().optional(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const { cursor } = input;
|
|
||||||
|
|
||||||
const sortCondition =
|
|
||||||
input.sortType === SortType.TOP
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
upvotes: input.sortOrder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: input.sortOrder,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
lastSeenAt: input.sortOrder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: input.sortOrder,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const questionsData = await ctx.prisma.questionsQuestion.findMany({
|
|
||||||
cursor:
|
|
||||||
cursor !== undefined
|
|
||||||
? {
|
|
||||||
id: cursor ? cursor!.idCursor : undefined,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
answers: true,
|
|
||||||
comments: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
encounters: {
|
|
||||||
select: {
|
|
||||||
company: true,
|
|
||||||
location: true,
|
|
||||||
role: true,
|
|
||||||
seenAt: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
orderBy: sortCondition,
|
|
||||||
take: input.limit + 1,
|
|
||||||
where: {
|
|
||||||
...(input.questionTypes.length > 0
|
|
||||||
? {
|
|
||||||
questionType: {
|
|
||||||
in: input.questionTypes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
encounters: {
|
|
||||||
some: {
|
|
||||||
seenAt: {
|
|
||||||
gte: input.startDate,
|
|
||||||
lte: input.endDate,
|
|
||||||
},
|
|
||||||
...(input.companyNames.length > 0
|
|
||||||
? {
|
|
||||||
company: {
|
|
||||||
name: {
|
|
||||||
in: input.companyNames,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(input.locations.length > 0
|
|
||||||
? {
|
|
||||||
location: {
|
|
||||||
in: input.locations,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(input.roles.length > 0
|
|
||||||
? {
|
|
||||||
role: {
|
|
||||||
in: input.roles,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const processedQuestionsData = questionsData.map(
|
|
||||||
createQuestionWithAggregateData,
|
|
||||||
);
|
|
||||||
|
|
||||||
let nextCursor: typeof cursor | undefined = undefined;
|
|
||||||
|
|
||||||
if (questionsData.length > input.limit) {
|
|
||||||
const nextItem = questionsData.pop()!;
|
|
||||||
processedQuestionsData.pop();
|
|
||||||
|
|
||||||
const nextIdCursor: string | undefined = nextItem.id;
|
|
||||||
const nextLastSeenCursor =
|
|
||||||
input.sortType === SortType.NEW ? nextItem.lastSeenAt : undefined;
|
|
||||||
const nextUpvoteCursor =
|
|
||||||
input.sortType === SortType.TOP ? nextItem.upvotes : undefined;
|
|
||||||
|
|
||||||
nextCursor = {
|
|
||||||
idCursor: nextIdCursor,
|
|
||||||
lastSeenCursor: nextLastSeenCursor,
|
|
||||||
upvoteCursor: nextUpvoteCursor,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: processedQuestionsData,
|
|
||||||
nextCursor,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.query('getQuestionById', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const questionData = await ctx.prisma.questionsQuestion.findUnique({
|
|
||||||
include: {
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
answers: true,
|
|
||||||
comments: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
encounters: {
|
|
||||||
select: {
|
|
||||||
company: true,
|
|
||||||
location: true,
|
|
||||||
role: true,
|
|
||||||
seenAt: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!questionData) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'NOT_FOUND',
|
|
||||||
message: 'Question not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return createQuestionWithAggregateData(questionData);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('create', {
|
|
||||||
input: z.object({
|
|
||||||
companyId: z.string(),
|
|
||||||
content: z.string(),
|
|
||||||
location: z.string(),
|
|
||||||
questionType: z.nativeEnum(QuestionsQuestionType),
|
|
||||||
role: z.string(),
|
|
||||||
seenAt: z.date(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsQuestion.create({
|
|
||||||
data: {
|
|
||||||
content: input.content,
|
|
||||||
encounters: {
|
|
||||||
create: {
|
|
||||||
company: {
|
|
||||||
connect: {
|
|
||||||
id: input.companyId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
location: input.location,
|
|
||||||
role: input.role,
|
|
||||||
seenAt: input.seenAt,
|
|
||||||
user: {
|
|
||||||
connect: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lastSeenAt: input.seenAt,
|
|
||||||
questionType: input.questionType,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('update', {
|
|
||||||
input: z.object({
|
|
||||||
content: z.string().optional(),
|
|
||||||
id: z.string(),
|
|
||||||
questionType: z.nativeEnum(QuestionsQuestionType).optional(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const questionToUpdate = await ctx.prisma.questionsQuestion.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (questionToUpdate?.id !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
// Optional: pass the original error to retain stack trace
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { content, questionType } = input;
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsQuestion.update({
|
|
||||||
data: {
|
|
||||||
content,
|
|
||||||
questionType,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('delete', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const questionToDelete = await ctx.prisma.questionsQuestion.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (questionToDelete?.id !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
// Optional: pass the original error to retain stack trace
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsQuestion.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.query('getVote', {
|
|
||||||
input: z.object({
|
|
||||||
questionId: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { questionId } = input;
|
|
||||||
|
|
||||||
return await ctx.prisma.questionsQuestionVote.findUnique({
|
|
||||||
where: {
|
|
||||||
questionId_userId: { questionId, userId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('createVote', {
|
|
||||||
input: z.object({
|
|
||||||
questionId: z.string(),
|
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { questionId, vote } = input;
|
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
|
|
||||||
|
|
||||||
const [questionVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsQuestionVote.create({
|
|
||||||
data: {
|
|
||||||
questionId,
|
|
||||||
userId,
|
|
||||||
vote,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsQuestion.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: questionId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return questionVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('updateVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
vote: z.nativeEnum(Vote),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
const { id, vote } = input;
|
|
||||||
|
|
||||||
const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (voteToUpdate?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
|
|
||||||
|
|
||||||
const [questionVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsQuestionVote.update({
|
|
||||||
data: {
|
|
||||||
vote,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsQuestion.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToUpdate.questionId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return questionVote;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.mutation('deleteVote', {
|
|
||||||
input: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
async resolve({ ctx, input }) {
|
|
||||||
const userId = ctx.session?.user?.id;
|
|
||||||
|
|
||||||
const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (voteToDelete?.userId !== userId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'User have no authorization to record.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
|
|
||||||
|
|
||||||
const [questionVote] = await ctx.prisma.$transaction([
|
|
||||||
ctx.prisma.questionsQuestionVote.delete({
|
|
||||||
where: {
|
|
||||||
id: input.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ctx.prisma.questionsQuestion.update({
|
|
||||||
data: {
|
|
||||||
upvotes: {
|
|
||||||
increment: incrementValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: voteToDelete.questionId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return questionVote;
|
|
||||||
},
|
|
||||||
});
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue