@ -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" }
|
||||
]
|
||||
}
|
@ -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/submit', name: 'Analyze your offers' },
|
||||
{ href: '/offers/features', name: 'Features' },
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 994 KiB After Width: | Height: | Size: 994 KiB |
Before Width: | Height: | Size: 923 KiB After Width: | Height: | Size: 923 KiB |
@ -1,29 +1,31 @@
|
||||
import { OVERALL_TAB } from '../constants';
|
||||
|
||||
import type { Analysis } from '~/types/offers';
|
||||
import type { AnalysisUnit } from '~/types/offers';
|
||||
|
||||
type OfferPercentileAnalysisTextProps = Readonly<{
|
||||
companyName: string;
|
||||
offerAnalysis: Analysis;
|
||||
analysis: AnalysisUnit;
|
||||
isSubmission: boolean;
|
||||
tab: string;
|
||||
}>;
|
||||
|
||||
export default function OfferPercentileAnalysisText({
|
||||
tab,
|
||||
companyName,
|
||||
offerAnalysis: { noOfOffers, percentile },
|
||||
analysis: { noOfOffers, percentile, companyName },
|
||||
isSubmission,
|
||||
}: OfferPercentileAnalysisTextProps) {
|
||||
return tab === OVERALL_TAB ? (
|
||||
<p>
|
||||
Your highest offer is from <b>{companyName}</b>, which is{' '}
|
||||
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
|
||||
offers received for the same job title and YOE(±1) in the last year.
|
||||
{isSubmission ? 'Your' : "This profile's"} highest offer is from{' '}
|
||||
<b>{companyName}</b>, which is <b>{percentile.toFixed(1)}</b> percentile
|
||||
out of <b>{noOfOffers}</b> offers received for the same job title and
|
||||
YOE(±1) in the last year.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Your offer from <b>{companyName}</b> is <b>{percentile.toFixed(1)}</b>{' '}
|
||||
percentile out of <b>{noOfOffers}</b> offers received in {companyName} for
|
||||
the same job title and YOE(±1) in the last year.
|
||||
{isSubmission ? 'Your' : 'The'} offer from <b>{companyName}</b> is{' '}
|
||||
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
|
||||
offers received in {companyName} for the same job title and YOE(±1) in the
|
||||
last year.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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 ExpandedTypeahead from './ExpandedTypeahead';
|
||||
|
||||
import type { Location } from '~/types/questions';
|
||||
|
||||
export type LocationTypeaheadProps = Omit<
|
||||
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 (
|
||||
<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"
|
||||
options={LOCATIONS}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onQueryChange={() => {}}
|
||||
options={locationOptions}
|
||||
onQueryChange={setQuery}
|
||||
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 ExpandedTypeahead from './ExpandedTypeahead';
|
||||
import type { FilterChoices } from '../filter/FilterSection';
|
||||
|
||||
export type RoleTypeaheadProps = Omit<
|
||||
ExpandedTypeaheadProps,
|
||||
'label' | 'onQueryChange' | 'options'
|
||||
>;
|
||||
|
||||
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
|
||||
([slug, label]) => ({
|
||||
id: slug,
|
||||
label,
|
||||
value: slug,
|
||||
}),
|
||||
);
|
||||
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
return (
|
||||
<ExpandedTypeahead
|
||||
{...(props as ExpandedTypeaheadProps)}
|
||||
label="Role"
|
||||
options={ROLES}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onQueryChange={() => {}}
|
||||
options={ROLES.filter((option) =>
|
||||
option.label.toLowerCase().includes(query.toLowerCase()),
|
||||
)}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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,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;
|
||||
},
|
||||
});
|