@ -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 { OVERALL_TAB } from '../constants';
|
||||||
|
|
||||||
import type { Analysis } from '~/types/offers';
|
import type { AnalysisUnit } from '~/types/offers';
|
||||||
|
|
||||||
type OfferPercentileAnalysisTextProps = Readonly<{
|
type OfferPercentileAnalysisTextProps = Readonly<{
|
||||||
companyName: string;
|
analysis: AnalysisUnit;
|
||||||
offerAnalysis: Analysis;
|
isSubmission: boolean;
|
||||||
tab: string;
|
tab: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function OfferPercentileAnalysisText({
|
export default function OfferPercentileAnalysisText({
|
||||||
tab,
|
tab,
|
||||||
companyName,
|
analysis: { noOfOffers, percentile, companyName },
|
||||||
offerAnalysis: { noOfOffers, percentile },
|
isSubmission,
|
||||||
}: OfferPercentileAnalysisTextProps) {
|
}: OfferPercentileAnalysisTextProps) {
|
||||||
return tab === OVERALL_TAB ? (
|
return tab === OVERALL_TAB ? (
|
||||||
<p>
|
<p>
|
||||||
Your highest offer is from <b>{companyName}</b>, which is{' '}
|
{isSubmission ? 'Your' : "This profile's"} highest offer is from{' '}
|
||||||
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
|
<b>{companyName}</b>, which is <b>{percentile.toFixed(1)}</b> percentile
|
||||||
offers received for the same job title and YOE(±1) in the last year.
|
out of <b>{noOfOffers}</b> offers received for the same job title and
|
||||||
|
YOE(±1) in the last year.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
Your offer from <b>{companyName}</b> is <b>{percentile.toFixed(1)}</b>{' '}
|
{isSubmission ? 'Your' : 'The'} offer from <b>{companyName}</b> is{' '}
|
||||||
percentile out of <b>{noOfOffers}</b> offers received in {companyName} for
|
<b>{percentile.toFixed(1)}</b> percentile out of <b>{noOfOffers}</b>{' '}
|
||||||
the same job title and YOE(±1) in the last year.
|
offers received in {companyName} for the same job title and YOE(±1) in the
|
||||||
|
last year.
|
||||||
</p>
|
</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 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
|
||||||
},
|
|
||||||
});
|
|