commit
5adc9d4a15
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,410 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "country_id": "1", "sortname": "AF", "country_name": "Afghanistan" },
|
||||||
|
{ "country_id": "2", "sortname": "AL", "country_name": "Albania" },
|
||||||
|
{ "country_id": "3", "sortname": "DZ", "country_name": "Algeria" },
|
||||||
|
{ "country_id": "4", "sortname": "AS", "country_name": "American Samoa" },
|
||||||
|
{ "country_id": "5", "sortname": "AD", "country_name": "Andorra" },
|
||||||
|
{ "country_id": "6", "sortname": "AO", "country_name": "Angola" },
|
||||||
|
{ "country_id": "7", "sortname": "AI", "country_name": "Anguilla" },
|
||||||
|
{ "country_id": "8", "sortname": "AQ", "country_name": "Antarctica" },
|
||||||
|
{
|
||||||
|
"country_id": "9",
|
||||||
|
"sortname": "AG",
|
||||||
|
"country_name": "Antigua And Barbuda"
|
||||||
|
},
|
||||||
|
{ "country_id": "10", "sortname": "AR", "country_name": "Argentina" },
|
||||||
|
{ "country_id": "11", "sortname": "AM", "country_name": "Armenia" },
|
||||||
|
{ "country_id": "12", "sortname": "AW", "country_name": "Aruba" },
|
||||||
|
{ "country_id": "13", "sortname": "AU", "country_name": "Australia" },
|
||||||
|
{ "country_id": "14", "sortname": "AT", "country_name": "Austria" },
|
||||||
|
{ "country_id": "15", "sortname": "AZ", "country_name": "Azerbaijan" },
|
||||||
|
{ "country_id": "16", "sortname": "BS", "country_name": "The Bahamas" },
|
||||||
|
{ "country_id": "17", "sortname": "BH", "country_name": "Bahrain" },
|
||||||
|
{ "country_id": "18", "sortname": "BD", "country_name": "Bangladesh" },
|
||||||
|
{ "country_id": "19", "sortname": "BB", "country_name": "Barbados" },
|
||||||
|
{ "country_id": "20", "sortname": "BY", "country_name": "Belarus" },
|
||||||
|
{ "country_id": "21", "sortname": "BE", "country_name": "Belgium" },
|
||||||
|
{ "country_id": "22", "sortname": "BZ", "country_name": "Belize" },
|
||||||
|
{ "country_id": "23", "sortname": "BJ", "country_name": "Benin" },
|
||||||
|
{ "country_id": "24", "sortname": "BM", "country_name": "Bermuda" },
|
||||||
|
{ "country_id": "25", "sortname": "BT", "country_name": "Bhutan" },
|
||||||
|
{ "country_id": "26", "sortname": "BO", "country_name": "Bolivia" },
|
||||||
|
{
|
||||||
|
"country_id": "27",
|
||||||
|
"sortname": "BA",
|
||||||
|
"country_name": "Bosnia and Herzegovina"
|
||||||
|
},
|
||||||
|
{ "country_id": "28", "sortname": "BW", "country_name": "Botswana" },
|
||||||
|
{ "country_id": "29", "sortname": "BV", "country_name": "Bouvet Island" },
|
||||||
|
{ "country_id": "30", "sortname": "BR", "country_name": "Brazil" },
|
||||||
|
{
|
||||||
|
"country_id": "31",
|
||||||
|
"sortname": "IO",
|
||||||
|
"country_name": "British Indian Ocean Territory"
|
||||||
|
},
|
||||||
|
{ "country_id": "32", "sortname": "BN", "country_name": "Brunei" },
|
||||||
|
{ "country_id": "33", "sortname": "BG", "country_name": "Bulgaria" },
|
||||||
|
{ "country_id": "34", "sortname": "BF", "country_name": "Burkina Faso" },
|
||||||
|
{ "country_id": "35", "sortname": "BI", "country_name": "Burundi" },
|
||||||
|
{ "country_id": "36", "sortname": "KH", "country_name": "Cambodia" },
|
||||||
|
{ "country_id": "37", "sortname": "CM", "country_name": "Cameroon" },
|
||||||
|
{ "country_id": "38", "sortname": "CA", "country_name": "Canada" },
|
||||||
|
{ "country_id": "39", "sortname": "CV", "country_name": "Cape Verde" },
|
||||||
|
{ "country_id": "40", "sortname": "KY", "country_name": "Cayman Islands" },
|
||||||
|
{
|
||||||
|
"country_id": "41",
|
||||||
|
"sortname": "CF",
|
||||||
|
"country_name": "Central African Republic"
|
||||||
|
},
|
||||||
|
{ "country_id": "42", "sortname": "TD", "country_name": "Chad" },
|
||||||
|
{ "country_id": "43", "sortname": "CL", "country_name": "Chile" },
|
||||||
|
{ "country_id": "44", "sortname": "CN", "country_name": "China" },
|
||||||
|
{
|
||||||
|
"country_id": "45",
|
||||||
|
"sortname": "CX",
|
||||||
|
"country_name": "Christmas Island"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "46",
|
||||||
|
"sortname": "CC",
|
||||||
|
"country_name": "Cocos (Keeling) Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "47", "sortname": "CO", "country_name": "Colombia" },
|
||||||
|
{ "country_id": "48", "sortname": "KM", "country_name": "Comoros" },
|
||||||
|
{ "country_id": "49", "sortname": "CG", "country_name": "Congo" },
|
||||||
|
{
|
||||||
|
"country_id": "50",
|
||||||
|
"sortname": "CD",
|
||||||
|
"country_name": "Democratic Republic of The Congo"
|
||||||
|
},
|
||||||
|
{ "country_id": "51", "sortname": "CK", "country_name": "Cook Islands" },
|
||||||
|
{ "country_id": "52", "sortname": "CR", "country_name": "Costa Rica" },
|
||||||
|
{
|
||||||
|
"country_id": "53",
|
||||||
|
"sortname": "CI",
|
||||||
|
"country_name": "Cote D'Ivoire (Ivory Coast)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "54",
|
||||||
|
"sortname": "HR",
|
||||||
|
"country_name": "Croatia (Hrvatska)"
|
||||||
|
},
|
||||||
|
{ "country_id": "55", "sortname": "CU", "country_name": "Cuba" },
|
||||||
|
{ "country_id": "56", "sortname": "CY", "country_name": "Cyprus" },
|
||||||
|
{ "country_id": "57", "sortname": "CZ", "country_name": "Czech Republic" },
|
||||||
|
{ "country_id": "58", "sortname": "DK", "country_name": "Denmark" },
|
||||||
|
{ "country_id": "59", "sortname": "DJ", "country_name": "Djibouti" },
|
||||||
|
{ "country_id": "60", "sortname": "DM", "country_name": "Dominica" },
|
||||||
|
{
|
||||||
|
"country_id": "61",
|
||||||
|
"sortname": "DO",
|
||||||
|
"country_name": "Dominican Republic"
|
||||||
|
},
|
||||||
|
{ "country_id": "62", "sortname": "TP", "country_name": "East Timor" },
|
||||||
|
{ "country_id": "63", "sortname": "EC", "country_name": "Ecuador" },
|
||||||
|
{ "country_id": "64", "sortname": "EG", "country_name": "Egypt" },
|
||||||
|
{ "country_id": "65", "sortname": "SV", "country_name": "El Salvador" },
|
||||||
|
{
|
||||||
|
"country_id": "66",
|
||||||
|
"sortname": "GQ",
|
||||||
|
"country_name": "Equatorial Guinea"
|
||||||
|
},
|
||||||
|
{ "country_id": "67", "sortname": "ER", "country_name": "Eritrea" },
|
||||||
|
{ "country_id": "68", "sortname": "EE", "country_name": "Estonia" },
|
||||||
|
{ "country_id": "69", "sortname": "ET", "country_name": "Ethiopia" },
|
||||||
|
{
|
||||||
|
"country_id": "70",
|
||||||
|
"sortname": "XA",
|
||||||
|
"country_name": "External Territories of Australia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "71",
|
||||||
|
"sortname": "FK",
|
||||||
|
"country_name": "Falkland Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "72", "sortname": "FO", "country_name": "Faroe Islands" },
|
||||||
|
{ "country_id": "73", "sortname": "FJ", "country_name": "Fiji Islands" },
|
||||||
|
{ "country_id": "74", "sortname": "FI", "country_name": "Finland" },
|
||||||
|
{ "country_id": "75", "sortname": "FR", "country_name": "France" },
|
||||||
|
{ "country_id": "76", "sortname": "GF", "country_name": "French Guiana" },
|
||||||
|
{
|
||||||
|
"country_id": "77",
|
||||||
|
"sortname": "PF",
|
||||||
|
"country_name": "French Polynesia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "78",
|
||||||
|
"sortname": "TF",
|
||||||
|
"country_name": "French Southern Territories"
|
||||||
|
},
|
||||||
|
{ "country_id": "79", "sortname": "GA", "country_name": "Gabon" },
|
||||||
|
{ "country_id": "80", "sortname": "GM", "country_name": "The Gambia" },
|
||||||
|
{ "country_id": "81", "sortname": "GE", "country_name": "Georgia" },
|
||||||
|
{ "country_id": "82", "sortname": "DE", "country_name": "Germany" },
|
||||||
|
{ "country_id": "83", "sortname": "GH", "country_name": "Ghana" },
|
||||||
|
{ "country_id": "84", "sortname": "GI", "country_name": "Gibraltar" },
|
||||||
|
{ "country_id": "85", "sortname": "GR", "country_name": "Greece" },
|
||||||
|
{ "country_id": "86", "sortname": "GL", "country_name": "Greenland" },
|
||||||
|
{ "country_id": "87", "sortname": "GD", "country_name": "Grenada" },
|
||||||
|
{ "country_id": "88", "sortname": "GP", "country_name": "Guadeloupe" },
|
||||||
|
{ "country_id": "89", "sortname": "GU", "country_name": "Guam" },
|
||||||
|
{ "country_id": "90", "sortname": "GT", "country_name": "Guatemala" },
|
||||||
|
{
|
||||||
|
"country_id": "91",
|
||||||
|
"sortname": "XU",
|
||||||
|
"country_name": "Guernsey and Alderney"
|
||||||
|
},
|
||||||
|
{ "country_id": "92", "sortname": "GN", "country_name": "Guinea" },
|
||||||
|
{ "country_id": "93", "sortname": "GW", "country_name": "Guinea-Bissau" },
|
||||||
|
{ "country_id": "94", "sortname": "GY", "country_name": "Guyana" },
|
||||||
|
{ "country_id": "95", "sortname": "HT", "country_name": "Haiti" },
|
||||||
|
{
|
||||||
|
"country_id": "96",
|
||||||
|
"sortname": "HM",
|
||||||
|
"country_name": "Heard and McDonald Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "97", "sortname": "HN", "country_name": "Honduras" },
|
||||||
|
{
|
||||||
|
"country_id": "98",
|
||||||
|
"sortname": "HK",
|
||||||
|
"country_name": "Hong Kong"
|
||||||
|
},
|
||||||
|
{ "country_id": "99", "sortname": "HU", "country_name": "Hungary" },
|
||||||
|
{ "country_id": "100", "sortname": "IS", "country_name": "Iceland" },
|
||||||
|
{ "country_id": "101", "sortname": "IN", "country_name": "India" },
|
||||||
|
{ "country_id": "102", "sortname": "ID", "country_name": "Indonesia" },
|
||||||
|
{ "country_id": "103", "sortname": "IR", "country_name": "Iran" },
|
||||||
|
{ "country_id": "104", "sortname": "IQ", "country_name": "Iraq" },
|
||||||
|
{ "country_id": "105", "sortname": "IE", "country_name": "Ireland" },
|
||||||
|
{ "country_id": "106", "sortname": "IL", "country_name": "Israel" },
|
||||||
|
{ "country_id": "107", "sortname": "IT", "country_name": "Italy" },
|
||||||
|
{ "country_id": "108", "sortname": "JM", "country_name": "Jamaica" },
|
||||||
|
{ "country_id": "109", "sortname": "JP", "country_name": "Japan" },
|
||||||
|
{ "country_id": "110", "sortname": "XJ", "country_name": "Jersey" },
|
||||||
|
{ "country_id": "111", "sortname": "JO", "country_name": "Jordan" },
|
||||||
|
{ "country_id": "112", "sortname": "KZ", "country_name": "Kazakhstan" },
|
||||||
|
{ "country_id": "113", "sortname": "KE", "country_name": "Kenya" },
|
||||||
|
{ "country_id": "114", "sortname": "KI", "country_name": "Kiribati" },
|
||||||
|
{ "country_id": "115", "sortname": "KP", "country_name": "North Korea" },
|
||||||
|
{ "country_id": "116", "sortname": "KR", "country_name": "South Korea" },
|
||||||
|
{ "country_id": "117", "sortname": "KW", "country_name": "Kuwait" },
|
||||||
|
{ "country_id": "118", "sortname": "KG", "country_name": "Kyrgyzstan" },
|
||||||
|
{ "country_id": "119", "sortname": "LA", "country_name": "Laos" },
|
||||||
|
{ "country_id": "120", "sortname": "LV", "country_name": "Latvia" },
|
||||||
|
{ "country_id": "121", "sortname": "LB", "country_name": "Lebanon" },
|
||||||
|
{ "country_id": "122", "sortname": "LS", "country_name": "Lesotho" },
|
||||||
|
{ "country_id": "123", "sortname": "LR", "country_name": "Liberia" },
|
||||||
|
{ "country_id": "124", "sortname": "LY", "country_name": "Libya" },
|
||||||
|
{ "country_id": "125", "sortname": "LI", "country_name": "Liechtenstein" },
|
||||||
|
{ "country_id": "126", "sortname": "LT", "country_name": "Lithuania" },
|
||||||
|
{ "country_id": "127", "sortname": "LU", "country_name": "Luxembourg" },
|
||||||
|
{ "country_id": "128", "sortname": "MO", "country_name": "Macau" },
|
||||||
|
{ "country_id": "129", "sortname": "MK", "country_name": "Macedonia" },
|
||||||
|
{ "country_id": "130", "sortname": "MG", "country_name": "Madagascar" },
|
||||||
|
{ "country_id": "131", "sortname": "MW", "country_name": "Malawi" },
|
||||||
|
{ "country_id": "132", "sortname": "MY", "country_name": "Malaysia" },
|
||||||
|
{ "country_id": "133", "sortname": "MV", "country_name": "Maldives" },
|
||||||
|
{ "country_id": "134", "sortname": "ML", "country_name": "Mali" },
|
||||||
|
{ "country_id": "135", "sortname": "MT", "country_name": "Malta" },
|
||||||
|
{ "country_id": "136", "sortname": "XM", "country_name": "Isle of Man" },
|
||||||
|
{
|
||||||
|
"country_id": "137",
|
||||||
|
"sortname": "MH",
|
||||||
|
"country_name": "Marshall Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "138", "sortname": "MQ", "country_name": "Martinique" },
|
||||||
|
{ "country_id": "139", "sortname": "MR", "country_name": "Mauritania" },
|
||||||
|
{ "country_id": "140", "sortname": "MU", "country_name": "Mauritius" },
|
||||||
|
{ "country_id": "141", "sortname": "YT", "country_name": "Mayotte" },
|
||||||
|
{ "country_id": "142", "sortname": "MX", "country_name": "Mexico" },
|
||||||
|
{ "country_id": "143", "sortname": "FM", "country_name": "Micronesia" },
|
||||||
|
{ "country_id": "144", "sortname": "MD", "country_name": "Moldova" },
|
||||||
|
{ "country_id": "145", "sortname": "MC", "country_name": "Monaco" },
|
||||||
|
{ "country_id": "146", "sortname": "MN", "country_name": "Mongolia" },
|
||||||
|
{ "country_id": "147", "sortname": "MS", "country_name": "Montserrat" },
|
||||||
|
{ "country_id": "148", "sortname": "MA", "country_name": "Morocco" },
|
||||||
|
{ "country_id": "149", "sortname": "MZ", "country_name": "Mozambique" },
|
||||||
|
{ "country_id": "150", "sortname": "MM", "country_name": "Myanmar" },
|
||||||
|
{ "country_id": "151", "sortname": "NA", "country_name": "Namibia" },
|
||||||
|
{ "country_id": "152", "sortname": "NR", "country_name": "Nauru" },
|
||||||
|
{ "country_id": "153", "sortname": "NP", "country_name": "Nepal" },
|
||||||
|
{
|
||||||
|
"country_id": "154",
|
||||||
|
"sortname": "AN",
|
||||||
|
"country_name": "Netherlands Antilles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "155",
|
||||||
|
"sortname": "NL",
|
||||||
|
"country_name": "Netherlands The"
|
||||||
|
},
|
||||||
|
{ "country_id": "156", "sortname": "NC", "country_name": "New Caledonia" },
|
||||||
|
{ "country_id": "157", "sortname": "NZ", "country_name": "New Zealand" },
|
||||||
|
{ "country_id": "158", "sortname": "NI", "country_name": "Nicaragua" },
|
||||||
|
{ "country_id": "159", "sortname": "NE", "country_name": "Niger" },
|
||||||
|
{ "country_id": "160", "sortname": "NG", "country_name": "Nigeria" },
|
||||||
|
{ "country_id": "161", "sortname": "NU", "country_name": "Niue" },
|
||||||
|
{ "country_id": "162", "sortname": "NF", "country_name": "Norfolk Island" },
|
||||||
|
{
|
||||||
|
"country_id": "163",
|
||||||
|
"sortname": "MP",
|
||||||
|
"country_name": "Northern Mariana Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "164", "sortname": "NO", "country_name": "Norway" },
|
||||||
|
{ "country_id": "165", "sortname": "OM", "country_name": "Oman" },
|
||||||
|
{ "country_id": "166", "sortname": "PK", "country_name": "Pakistan" },
|
||||||
|
{ "country_id": "167", "sortname": "PW", "country_name": "Palau" },
|
||||||
|
{
|
||||||
|
"country_id": "168",
|
||||||
|
"sortname": "PS",
|
||||||
|
"country_name": "Palestinian Territory Occupied"
|
||||||
|
},
|
||||||
|
{ "country_id": "169", "sortname": "PA", "country_name": "Panama" },
|
||||||
|
{
|
||||||
|
"country_id": "170",
|
||||||
|
"sortname": "PG",
|
||||||
|
"country_name": "Papua new Guinea"
|
||||||
|
},
|
||||||
|
{ "country_id": "171", "sortname": "PY", "country_name": "Paraguay" },
|
||||||
|
{ "country_id": "172", "sortname": "PE", "country_name": "Peru" },
|
||||||
|
{ "country_id": "173", "sortname": "PH", "country_name": "Philippines" },
|
||||||
|
{
|
||||||
|
"country_id": "174",
|
||||||
|
"sortname": "PN",
|
||||||
|
"country_name": "Pitcairn Island"
|
||||||
|
},
|
||||||
|
{ "country_id": "175", "sortname": "PL", "country_name": "Poland" },
|
||||||
|
{ "country_id": "176", "sortname": "PT", "country_name": "Portugal" },
|
||||||
|
{ "country_id": "177", "sortname": "PR", "country_name": "Puerto Rico" },
|
||||||
|
{ "country_id": "178", "sortname": "QA", "country_name": "Qatar" },
|
||||||
|
{ "country_id": "179", "sortname": "RE", "country_name": "Reunion" },
|
||||||
|
{ "country_id": "180", "sortname": "RO", "country_name": "Romania" },
|
||||||
|
{ "country_id": "181", "sortname": "RU", "country_name": "Russia" },
|
||||||
|
{ "country_id": "182", "sortname": "RW", "country_name": "Rwanda" },
|
||||||
|
{ "country_id": "183", "sortname": "SH", "country_name": "Saint Helena" },
|
||||||
|
{
|
||||||
|
"country_id": "184",
|
||||||
|
"sortname": "KN",
|
||||||
|
"country_name": "Saint Kitts And Nevis"
|
||||||
|
},
|
||||||
|
{ "country_id": "185", "sortname": "LC", "country_name": "Saint Lucia" },
|
||||||
|
{
|
||||||
|
"country_id": "186",
|
||||||
|
"sortname": "PM",
|
||||||
|
"country_name": "Saint Pierre and Miquelon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "187",
|
||||||
|
"sortname": "VC",
|
||||||
|
"country_name": "Saint Vincent and The Grenadines"
|
||||||
|
},
|
||||||
|
{ "country_id": "188", "sortname": "WS", "country_name": "Samoa" },
|
||||||
|
{ "country_id": "189", "sortname": "SM", "country_name": "San Marino" },
|
||||||
|
{
|
||||||
|
"country_id": "190",
|
||||||
|
"sortname": "ST",
|
||||||
|
"country_name": "Sao Tome and Principe"
|
||||||
|
},
|
||||||
|
{ "country_id": "191", "sortname": "SA", "country_name": "Saudi Arabia" },
|
||||||
|
{ "country_id": "192", "sortname": "SN", "country_name": "Senegal" },
|
||||||
|
{ "country_id": "193", "sortname": "RS", "country_name": "Serbia" },
|
||||||
|
{ "country_id": "194", "sortname": "SC", "country_name": "Seychelles" },
|
||||||
|
{ "country_id": "195", "sortname": "SL", "country_name": "Sierra Leone" },
|
||||||
|
{ "country_id": "196", "sortname": "SG", "country_name": "Singapore" },
|
||||||
|
{ "country_id": "197", "sortname": "SK", "country_name": "Slovakia" },
|
||||||
|
{ "country_id": "198", "sortname": "SI", "country_name": "Slovenia" },
|
||||||
|
{
|
||||||
|
"country_id": "199",
|
||||||
|
"sortname": "XG",
|
||||||
|
"country_name": "Smaller Territories of the UK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "200",
|
||||||
|
"sortname": "SB",
|
||||||
|
"country_name": "Solomon Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "201", "sortname": "SO", "country_name": "Somalia" },
|
||||||
|
{ "country_id": "202", "sortname": "ZA", "country_name": "South Africa" },
|
||||||
|
{ "country_id": "203", "sortname": "GS", "country_name": "South Georgia" },
|
||||||
|
{ "country_id": "204", "sortname": "SS", "country_name": "South Sudan" },
|
||||||
|
{ "country_id": "205", "sortname": "ES", "country_name": "Spain" },
|
||||||
|
{ "country_id": "206", "sortname": "LK", "country_name": "Sri Lanka" },
|
||||||
|
{ "country_id": "207", "sortname": "SD", "country_name": "Sudan" },
|
||||||
|
{ "country_id": "208", "sortname": "SR", "country_name": "Suriname" },
|
||||||
|
{
|
||||||
|
"country_id": "209",
|
||||||
|
"sortname": "SJ",
|
||||||
|
"country_name": "Svalbard And Jan Mayen Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "210", "sortname": "SZ", "country_name": "Swaziland" },
|
||||||
|
{ "country_id": "211", "sortname": "SE", "country_name": "Sweden" },
|
||||||
|
{ "country_id": "212", "sortname": "CH", "country_name": "Switzerland" },
|
||||||
|
{ "country_id": "213", "sortname": "SY", "country_name": "Syria" },
|
||||||
|
{ "country_id": "214", "sortname": "TW", "country_name": "Taiwan" },
|
||||||
|
{ "country_id": "215", "sortname": "TJ", "country_name": "Tajikistan" },
|
||||||
|
{ "country_id": "216", "sortname": "TZ", "country_name": "Tanzania" },
|
||||||
|
{ "country_id": "217", "sortname": "TH", "country_name": "Thailand" },
|
||||||
|
{ "country_id": "218", "sortname": "TG", "country_name": "Togo" },
|
||||||
|
{ "country_id": "219", "sortname": "TK", "country_name": "Tokelau" },
|
||||||
|
{ "country_id": "220", "sortname": "TO", "country_name": "Tonga" },
|
||||||
|
{
|
||||||
|
"country_id": "221",
|
||||||
|
"sortname": "TT",
|
||||||
|
"country_name": "Trinidad And Tobago"
|
||||||
|
},
|
||||||
|
{ "country_id": "222", "sortname": "TN", "country_name": "Tunisia" },
|
||||||
|
{ "country_id": "223", "sortname": "TR", "country_name": "Turkey" },
|
||||||
|
{ "country_id": "224", "sortname": "TM", "country_name": "Turkmenistan" },
|
||||||
|
{
|
||||||
|
"country_id": "225",
|
||||||
|
"sortname": "TC",
|
||||||
|
"country_name": "Turks And Caicos Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "226", "sortname": "TV", "country_name": "Tuvalu" },
|
||||||
|
{ "country_id": "227", "sortname": "UG", "country_name": "Uganda" },
|
||||||
|
{ "country_id": "228", "sortname": "UA", "country_name": "Ukraine" },
|
||||||
|
{
|
||||||
|
"country_id": "229",
|
||||||
|
"sortname": "AE",
|
||||||
|
"country_name": "United Arab Emirates"
|
||||||
|
},
|
||||||
|
{ "country_id": "230", "sortname": "GB", "country_name": "United Kingdom" },
|
||||||
|
{ "country_id": "231", "sortname": "US", "country_name": "United States" },
|
||||||
|
{
|
||||||
|
"country_id": "232",
|
||||||
|
"sortname": "UM",
|
||||||
|
"country_name": "United States Minor Outlying Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "233", "sortname": "UY", "country_name": "Uruguay" },
|
||||||
|
{ "country_id": "234", "sortname": "UZ", "country_name": "Uzbekistan" },
|
||||||
|
{ "country_id": "235", "sortname": "VU", "country_name": "Vanuatu" },
|
||||||
|
{
|
||||||
|
"country_id": "236",
|
||||||
|
"sortname": "VA",
|
||||||
|
"country_name": "Vatican City State (Holy See)"
|
||||||
|
},
|
||||||
|
{ "country_id": "237", "sortname": "VE", "country_name": "Venezuela" },
|
||||||
|
{ "country_id": "238", "sortname": "VN", "country_name": "Vietnam" },
|
||||||
|
{
|
||||||
|
"country_id": "239",
|
||||||
|
"sortname": "VG",
|
||||||
|
"country_name": "Virgin Islands (British)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "240",
|
||||||
|
"sortname": "VI",
|
||||||
|
"country_name": "Virgin Islands (US)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_id": "241",
|
||||||
|
"sortname": "WF",
|
||||||
|
"country_name": "Wallis And Futuna Islands"
|
||||||
|
},
|
||||||
|
{ "country_id": "242", "sortname": "EH", "country_name": "Western Sahara" },
|
||||||
|
{ "country_id": "243", "sortname": "YE", "country_name": "Yemen" },
|
||||||
|
{ "country_id": "244", "sortname": "YU", "country_name": "Yugoslavia" },
|
||||||
|
{ "country_id": "245", "sortname": "ZM", "country_name": "Zambia" },
|
||||||
|
{ "country_id": "246", "sortname": "ZW", "country_name": "Zimbabwe" }
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "QuestionsQuestion" ADD COLUMN "contentSearch" TSVECTOR
|
|
||||||
GENERATED ALWAYS AS
|
|
||||||
(to_tsvector('english', coalesce(content, '')))
|
|
||||||
STORED;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "QuestionsQuestion_contentSearch_idx" ON "QuestionsQuestion" USING GIN("contentSearch");
|
|
@ -1,8 +0,0 @@
|
|||||||
-- DropIndex
|
|
||||||
DROP INDEX "QuestionsQuestion_contentSearch_idx";
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "QuestionsQuestion" ALTER COLUMN "contentSearch" DROP DEFAULT;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "QuestionsQuestion_contentSearch_idx" ON "QuestionsQuestion"("contentSearch");
|
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ResumesResume" ADD COLUMN "isResolved" BOOLEAN NOT NULL DEFAULT false;
|
@ -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';
|
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,103 @@
|
|||||||
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { createContext, useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
type Context = Readonly<{
|
||||||
|
event: (payload: GoogleAnalyticsEventPayload) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const GoogleAnalyticsContext = createContext<Context>({
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
|
||||||
|
function pageview(measurementID: string, url: string) {
|
||||||
|
// Don't log analytics during development.
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gtag('config', measurementID, {
|
||||||
|
page_path: url,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.gtag('event', url, {
|
||||||
|
event_category: 'pageview',
|
||||||
|
event_label: document.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoogleAnalyticsEventPayload = Readonly<{
|
||||||
|
action: string;
|
||||||
|
category: string;
|
||||||
|
label: string;
|
||||||
|
value?: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
|
||||||
|
export function event({
|
||||||
|
action,
|
||||||
|
category,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: GoogleAnalyticsEventPayload) {
|
||||||
|
// Don't log analytics during development.
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gtag('event', action, {
|
||||||
|
event_category: category,
|
||||||
|
event_label: label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
measurementID: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function useGoogleAnalytics() {
|
||||||
|
return useContext(GoogleAnalyticsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GoogleAnalytics({ children, measurementID }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
useEffect(() => {
|
||||||
|
function handleRouteChange(url: string) {
|
||||||
|
pageview(measurementID, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.events.on('routeChangeComplete', handleRouteChange);
|
||||||
|
return () => {
|
||||||
|
router.events.off('routeChangeComplete', handleRouteChange);
|
||||||
|
};
|
||||||
|
}, [router.events, measurementID]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GoogleAnalyticsContext.Provider value={{ event }}>
|
||||||
|
{children}
|
||||||
|
<Head>
|
||||||
|
{/* TODO(yangshun): Change back to next/script in future. */}
|
||||||
|
{/* Global Site Tag (gtag.js) - Google Analytics */}
|
||||||
|
<script
|
||||||
|
async={true}
|
||||||
|
src={`https://www.googletagmanager.com/gtag/js?id=${measurementID}`}
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.gtag = function(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', '${measurementID}', {
|
||||||
|
page_path: window.location.pathname,
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
</GoogleAnalyticsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
import type { ProductNavigationItems } from '~/components/global/ProductNavigation';
|
|
||||||
|
|
||||||
const navigation: ProductNavigationItems = [
|
|
||||||
{ href: '/offers/browse', name: 'Browse' },
|
|
||||||
{ href: '/offers/submit', name: 'Analyse your offers' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
navigation,
|
|
||||||
showGlobalNav: false,
|
|
||||||
title: 'Offer Profile Repository',
|
|
||||||
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 repository' },
|
||||||
|
{ 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;
|
@ -1,18 +0,0 @@
|
|||||||
export default function OffersTitle() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-end justify-center">
|
|
||||||
<h1 className="text-primary-600 mt-16 text-center text-4xl font-bold">
|
|
||||||
Offer Profile Repository
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="text-primary-500 mt-2 text-center text-2xl font-normal">
|
|
||||||
Reveal profile stories behind offers
|
|
||||||
</div>
|
|
||||||
<div className="items-top flex justify-center text-xl font-normal">
|
|
||||||
Click into offers to view profiles, benchmark your offers and profiles,
|
|
||||||
and discuss with the community
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,55 @@
|
|||||||
|
import { JobType } from '@prisma/client';
|
||||||
|
import { HorizontalDivider } from '@tih/ui';
|
||||||
|
|
||||||
|
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
|
import { convertMoneyToString } from '~/utils/offers/currency';
|
||||||
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
|
||||||
|
import type { UserProfileOffer } from '~/types/offers';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disableTopDivider?: boolean;
|
||||||
|
offer: UserProfileOffer;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function DashboardProfileCard({
|
||||||
|
disableTopDivider,
|
||||||
|
offer: {
|
||||||
|
company,
|
||||||
|
income,
|
||||||
|
jobType,
|
||||||
|
level,
|
||||||
|
location,
|
||||||
|
monthYearReceived,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!disableTopDivider && <HorizontalDivider />}
|
||||||
|
<div className="flex items-end justify-between">
|
||||||
|
<div className="col-span-1 row-span-3">
|
||||||
|
<p className="font-bold">
|
||||||
|
{getLabelForJobTitleType(title as JobTitleType)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{location
|
||||||
|
? `Company: ${company.name}, ${location}`
|
||||||
|
: `Company: ${company.name}`}
|
||||||
|
</p>
|
||||||
|
{level && <p>Level: {level}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 row-span-3">
|
||||||
|
<p className="text-end">{formatDate(monthYearReceived)}</p>
|
||||||
|
<p className="text-end text-xl">
|
||||||
|
{jobType === JobType.FULLTIME
|
||||||
|
? `${convertMoneyToString(income)} / year`
|
||||||
|
: `${convertMoneyToString(income)} / month`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { ArrowRightIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Button, useToast } from '@tih/ui';
|
||||||
|
|
||||||
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
|
import DashboardOfferCard from '~/components/offers/dashboard/DashboardOfferCard';
|
||||||
|
|
||||||
|
import { formatDate } from '~/utils/offers/time';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import ProfilePhotoHolder from '../profile/ProfilePhotoHolder';
|
||||||
|
|
||||||
|
import type { UserProfile, UserProfileOffer } from '~/types/offers';
|
||||||
|
type Props = Readonly<{
|
||||||
|
profile: UserProfile;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function DashboardProfileCard({
|
||||||
|
profile: { createdAt, id, offers, profileName, token },
|
||||||
|
}: Props) {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const trpcContext = trpc.useContext();
|
||||||
|
const PROFILE_URL = `/offers/profile/${id}?token=${token}`;
|
||||||
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
|
const removeSavedProfileMutation = trpc.useMutation(
|
||||||
|
'offers.user.profile.removeFromUserProfile',
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
title: `Server error.`,
|
||||||
|
variant: 'failure',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
trpcContext.invalidateQueries(['offers.user.profile.getUserProfiles']);
|
||||||
|
showToast({
|
||||||
|
title: `Profile removed from your dashboard successfully!`,
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleRemoveProfile() {
|
||||||
|
removeSavedProfileMutation.mutate({ profileId: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 bg-white px-4 pt-5 sm:px-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="-ml-4 -mt-2 flex flex-wrap items-center justify-between border-b border-gray-300 pb-4 sm:flex-nowrap">
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div>
|
||||||
|
<ProfilePhotoHolder size="sm" />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-10">
|
||||||
|
<p className="text-xl font-bold">{profileName}</p>
|
||||||
|
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span>Created at {formatDate(createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex self-start">
|
||||||
|
<Button
|
||||||
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
|
icon={XMarkIcon}
|
||||||
|
isLabelHidden={true}
|
||||||
|
label="Remove Profile"
|
||||||
|
size="md"
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={handleRemoveProfile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Offers */}
|
||||||
|
<div>
|
||||||
|
{offers.map((offer: UserProfileOffer, index) =>
|
||||||
|
index === 0 ? (
|
||||||
|
<DashboardOfferCard
|
||||||
|
key={offer.id}
|
||||||
|
disableTopDivider={true}
|
||||||
|
offer={offer}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DashboardOfferCard key={offer.id} offer={offer} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end pt-1">
|
||||||
|
<Button
|
||||||
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
|
icon={ArrowRightIcon}
|
||||||
|
isLabelHidden={false}
|
||||||
|
label="Read full profile"
|
||||||
|
size="md"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
gaEvent({
|
||||||
|
action: 'offers.view_profile_from_dashboard',
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'View profile from dashboard',
|
||||||
|
});
|
||||||
|
router.push(PROFILE_URL);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 994 KiB |
After Width: | Height: | Size: 923 KiB |
@ -0,0 +1,42 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||||
|
import type { Placement } from '@popperjs/core';
|
||||||
|
|
||||||
|
type TooltipProps = Readonly<{
|
||||||
|
children: ReactNode;
|
||||||
|
placement?: Placement;
|
||||||
|
tooltipContent: ReactNode;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function Tooltip({
|
||||||
|
children,
|
||||||
|
tooltipContent,
|
||||||
|
placement = 'bottom-start',
|
||||||
|
}: TooltipProps) {
|
||||||
|
const {
|
||||||
|
getTooltipProps,
|
||||||
|
getArrowProps,
|
||||||
|
setTooltipRef,
|
||||||
|
setTriggerRef,
|
||||||
|
visible,
|
||||||
|
} = usePopperTooltip({
|
||||||
|
interactive: true,
|
||||||
|
placement,
|
||||||
|
trigger: ['focus', 'hover'],
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={setTriggerRef}>{children}</div>
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
ref={setTooltipRef}
|
||||||
|
{...getTooltipProps({
|
||||||
|
className: 'tooltip-container ',
|
||||||
|
})}>
|
||||||
|
{tooltipContent}
|
||||||
|
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import type { UseInfiniteQueryResult } from 'react-query';
|
||||||
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
|
export type PaginationLoadMoreButtonProps = {
|
||||||
|
query: UseInfiniteQueryResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PaginationLoadMoreButton(
|
||||||
|
props: PaginationLoadMoreButtonProps,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
query: { hasNextPage, isFetchingNextPage, fetchNextPage },
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
label={hasNextPage ? 'Load more' : 'Nothing more to load'}
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={() => {
|
||||||
|
fetchNextPage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import { Select } from '~/../../../packages/ui/dist';
|
||||||
|
import { SORT_ORDERS, SORT_TYPES } from '~/utils/questions/constants';
|
||||||
|
|
||||||
|
import type { SortOrder, SortType } from '~/types/questions.d';
|
||||||
|
|
||||||
|
export type SortOption<Value> = {
|
||||||
|
label: string;
|
||||||
|
value: Value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortTypeOptions = SORT_TYPES;
|
||||||
|
const sortOrderOptions = SORT_ORDERS;
|
||||||
|
|
||||||
|
type SortOrderProps<Order> = {
|
||||||
|
onSortOrderChange?: (sortValue: Order) => void;
|
||||||
|
sortOrderValue: Order;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortTypeProps<Type> = {
|
||||||
|
onSortTypeChange?: (sortType: Type) => void;
|
||||||
|
sortTypeValue: Type;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortOptionsSelectProps = SortOrderProps<SortOrder> &
|
||||||
|
SortTypeProps<SortType>;
|
||||||
|
|
||||||
|
export default function SortOptionsSelect({
|
||||||
|
onSortOrderChange,
|
||||||
|
sortOrderValue,
|
||||||
|
onSortTypeChange,
|
||||||
|
sortTypeValue,
|
||||||
|
}: SortOptionsSelectProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-end justify-end gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
display="inline"
|
||||||
|
label="Sort by"
|
||||||
|
options={sortTypeOptions}
|
||||||
|
value={sortTypeValue}
|
||||||
|
onChange={(value) => {
|
||||||
|
const chosenOption = sortTypeOptions.find(
|
||||||
|
(option) => String(option.value) === value,
|
||||||
|
);
|
||||||
|
if (chosenOption) {
|
||||||
|
onSortTypeChange?.(chosenOption.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
display="inline"
|
||||||
|
label="Order by"
|
||||||
|
options={sortOrderOptions}
|
||||||
|
value={sortOrderValue}
|
||||||
|
onChange={(value) => {
|
||||||
|
const chosenOption = sortOrderOptions.find(
|
||||||
|
(option) => String(option.value) === value,
|
||||||
|
);
|
||||||
|
if (chosenOption) {
|
||||||
|
onSortOrderChange?.(chosenOption.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,21 +1,71 @@
|
|||||||
import { LOCATIONS } from '~/utils/questions/constants';
|
import { useMemo, useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
||||||
import ExpandedTypeahead from './ExpandedTypeahead';
|
import ExpandedTypeahead from './ExpandedTypeahead';
|
||||||
|
|
||||||
|
import type { Location } from '~/types/questions';
|
||||||
|
|
||||||
export type LocationTypeaheadProps = Omit<
|
export type LocationTypeaheadProps = Omit<
|
||||||
ExpandedTypeaheadProps,
|
ExpandedTypeaheadProps,
|
||||||
'label' | 'onQueryChange' | 'options'
|
'label' | 'onQueryChange' | 'onSelect' | 'onSuggestionClick' | 'options'
|
||||||
>;
|
> & {
|
||||||
|
onSelect: (option: Location & TypeaheadOption) => void;
|
||||||
|
onSuggestionClick?: (option: Location) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LocationTypeahead({
|
||||||
|
onSelect,
|
||||||
|
onSuggestionClick,
|
||||||
|
...restProps
|
||||||
|
}: LocationTypeaheadProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const { data: locations } = trpc.useQuery([
|
||||||
|
'locations.cities.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const locationOptions = useMemo(() => {
|
||||||
|
return (
|
||||||
|
locations?.map(({ id, name, state }) => ({
|
||||||
|
cityId: id,
|
||||||
|
countryId: state.country.id,
|
||||||
|
id,
|
||||||
|
label: `${name}, ${state.name}, ${state.country.name}`,
|
||||||
|
stateId: state.id,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}, [locations]);
|
||||||
|
|
||||||
export default function LocationTypeahead(props: LocationTypeaheadProps) {
|
|
||||||
return (
|
return (
|
||||||
<ExpandedTypeahead
|
<ExpandedTypeahead
|
||||||
{...(props as ExpandedTypeaheadProps)}
|
{...({
|
||||||
|
onSuggestionClick: onSuggestionClick
|
||||||
|
? (option: TypeaheadOption) => {
|
||||||
|
const location = locationOptions.find(
|
||||||
|
(locationOption) => locationOption.id === option.id,
|
||||||
|
)!;
|
||||||
|
onSuggestionClick({
|
||||||
|
...location,
|
||||||
|
...option,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
...restProps,
|
||||||
|
} as ExpandedTypeaheadProps)}
|
||||||
label="Location"
|
label="Location"
|
||||||
options={LOCATIONS}
|
options={locationOptions}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
onQueryChange={setQuery}
|
||||||
onQueryChange={() => {}}
|
onSelect={({ id }: TypeaheadOption) => {
|
||||||
|
const location = locationOptions.find((option) => option.id === id)!;
|
||||||
|
onSelect(location);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,34 @@
|
|||||||
import { ROLES } from '~/utils/questions/constants';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||||
|
|
||||||
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
|
||||||
import ExpandedTypeahead from './ExpandedTypeahead';
|
import ExpandedTypeahead from './ExpandedTypeahead';
|
||||||
|
import type { FilterChoices } from '../filter/FilterSection';
|
||||||
|
|
||||||
export type RoleTypeaheadProps = Omit<
|
export type RoleTypeaheadProps = Omit<
|
||||||
ExpandedTypeaheadProps,
|
ExpandedTypeaheadProps,
|
||||||
'label' | 'onQueryChange' | 'options'
|
'label' | 'onQueryChange' | 'options'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
|
||||||
|
([slug, label]) => ({
|
||||||
|
id: slug,
|
||||||
|
label,
|
||||||
|
value: slug,
|
||||||
|
}),
|
||||||
|
);
|
||||||
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
export default function RoleTypeahead(props: RoleTypeaheadProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandedTypeahead
|
<ExpandedTypeahead
|
||||||
{...(props as ExpandedTypeaheadProps)}
|
{...(props as ExpandedTypeaheadProps)}
|
||||||
label="Role"
|
label="Role"
|
||||||
options={ROLES}
|
options={ROLES.filter((option) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
option.label.toLowerCase().includes(query.toLowerCase()),
|
||||||
onQueryChange={() => {}}
|
)}
|
||||||
|
onQueryChange={setQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
import { Typeahead } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disabled?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
isLabelHidden?: boolean;
|
||||||
|
label?: string;
|
||||||
|
onSelect: (option: TypeaheadOption | null) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
required?: boolean;
|
||||||
|
value?: TypeaheadOption | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function CitiesTypeahead({
|
||||||
|
disabled,
|
||||||
|
label = 'City',
|
||||||
|
onSelect,
|
||||||
|
isLabelHidden,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
value,
|
||||||
|
}: Props) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const cities = trpc.useQuery([
|
||||||
|
'locations.cities.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data } = cities;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typeahead
|
||||||
|
disabled={disabled}
|
||||||
|
isLabelHidden={isLabelHidden}
|
||||||
|
label={label}
|
||||||
|
noResultsMessage="No cities found"
|
||||||
|
nullable={true}
|
||||||
|
options={
|
||||||
|
data?.map(({ id, name, state }) => ({
|
||||||
|
id,
|
||||||
|
label: `${name}, ${state?.name}, ${state?.country?.name}`,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
textSize="inherit"
|
||||||
|
value={value}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
import { Typeahead } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disabled?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
isLabelHidden?: boolean;
|
||||||
|
onSelect: (option: TypeaheadOption | null) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
required?: boolean;
|
||||||
|
value?: TypeaheadOption | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function CountriesTypeahead({
|
||||||
|
disabled,
|
||||||
|
onSelect,
|
||||||
|
isLabelHidden,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
value,
|
||||||
|
}: Props) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const countries = trpc.useQuery([
|
||||||
|
'locations.countries.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data } = countries;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typeahead
|
||||||
|
disabled={disabled}
|
||||||
|
isLabelHidden={isLabelHidden}
|
||||||
|
label="Country"
|
||||||
|
noResultsMessage="No countries found"
|
||||||
|
nullable={true}
|
||||||
|
options={
|
||||||
|
data?.map(({ id, name }) => ({
|
||||||
|
id,
|
||||||
|
label: name,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
textSize="inherit"
|
||||||
|
value={value}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import OffersTitle from '~/components/offers/OffersTitle';
|
|
||||||
import OffersTable from '~/components/offers/table/OffersTable';
|
|
||||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
|
||||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
|
||||||
|
|
||||||
export default function OffersHomePage() {
|
|
||||||
const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
|
|
||||||
const [companyFilter, setCompanyFilter] = useState('');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex-1 overflow-y-auto">
|
|
||||||
<div className="grid-rows grid h-1/2 bg-slate-100">
|
|
||||||
<OffersTitle />
|
|
||||||
<div className="flex items-start justify-center">
|
|
||||||
<div className="mt-4 flex items-center">
|
|
||||||
Viewing offers for
|
|
||||||
<div className="mx-4">
|
|
||||||
<JobTitlesTypeahead
|
|
||||||
isLabelHidden={true}
|
|
||||||
placeHolder="Software Engineer"
|
|
||||||
onSelect={({ value }) => setjobTitleFilter(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
in
|
|
||||||
<div className="ml-4">
|
|
||||||
<CompaniesTypeahead
|
|
||||||
isLabelHidden={true}
|
|
||||||
placeHolder="All Companies"
|
|
||||||
onSelect={({ value }) => setCompanyFilter(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center bg-white pb-20 pt-10">
|
|
||||||
<OffersTable
|
|
||||||
companyFilter={companyFilter}
|
|
||||||
jobTitleFilter={jobTitleFilter}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,95 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { signIn, useSession } from 'next-auth/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, Spinner } from '@tih/ui';
|
||||||
|
|
||||||
|
import DashboardOfferCard from '~/components/offers/dashboard/DashboardProfileCard';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import type { UserProfile } from '~/types/offers';
|
||||||
|
|
||||||
|
export default function ProfilesDashboard() {
|
||||||
|
const { status } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);
|
||||||
|
|
||||||
|
const userProfilesQuery = trpc.useQuery(
|
||||||
|
['offers.user.profile.getUserProfiles'],
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
if (error.data?.code === 'UNAUTHORIZED') {
|
||||||
|
signIn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (response: Array<UserProfile>) => {
|
||||||
|
setUserProfiles(response);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 'loading' || userProfilesQuery.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === 'unauthenticated') {
|
||||||
|
signIn();
|
||||||
|
}
|
||||||
|
if (userProfiles.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center text-xl">
|
||||||
|
<div className="mb-8 flex w-full flex-row justify-center">
|
||||||
|
<h2>You have not saved any offer profiles yet.</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-center">
|
||||||
|
<Button
|
||||||
|
label="Submit your offers now!"
|
||||||
|
size="lg"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => router.push('/offers/submit')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{userProfilesQuery.isLoading && (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!userProfilesQuery.isLoading && (
|
||||||
|
<div className="mt-8 overflow-y-auto">
|
||||||
|
<h1 className="mx-auto mb-4 w-3/4 text-start text-4xl font-bold text-slate-900">
|
||||||
|
Your repository
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto w-3/4 text-start text-xl text-slate-900">
|
||||||
|
Save your offer profiles to respository to easily access and edit
|
||||||
|
them later.
|
||||||
|
</p>
|
||||||
|
<div className="justfy-center mt-8 flex w-screen">
|
||||||
|
<ul className="mx-auto w-3/4 space-y-3" role="list">
|
||||||
|
{userProfiles?.map((profile) => (
|
||||||
|
<li
|
||||||
|
key={profile.id}
|
||||||
|
className="overflow-hidden bg-white px-4 py-4 shadow sm:rounded-md sm:px-6">
|
||||||
|
<DashboardOfferCard key={profile.id} profile={profile} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,251 @@
|
|||||||
|
import type { SVGProps } from 'react';
|
||||||
|
import {
|
||||||
|
BookmarkSquareIcon,
|
||||||
|
ChartBarSquareIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
ShareIcon,
|
||||||
|
TableCellsIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
import offersAnalysis from '~/components/offers/features/images/offers-analysis.png';
|
||||||
|
import offersBrowse from '~/components/offers/features/images/offers-browse.png';
|
||||||
|
import offersProfile from '~/components/offers/features/images/offers-profile.png';
|
||||||
|
import LeftTextCard from '~/components/offers/features/LeftTextCard';
|
||||||
|
import RightTextCard from '~/components/offers/features/RightTextCard';
|
||||||
|
import { HOME_URL } from '~/components/offers/types';
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'Profile names are randomly generated to keep your offers strictly anonymous.',
|
||||||
|
icon: UsersIcon,
|
||||||
|
name: 'Anonymized Profile Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'Only users with the edit link can edit that profile. Share profiles to others using a public link without giving edit permission.',
|
||||||
|
icon: ShareIcon,
|
||||||
|
name: 'Edit Link v.s. Public Link',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Offer profiles will not be automatically saved under creators' account in our database unless explicit permission is given.",
|
||||||
|
icon: BookmarkSquareIcon,
|
||||||
|
name: 'Save with Permission',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const footerNavigation = {
|
||||||
|
social: [
|
||||||
|
// {
|
||||||
|
// href: '#',
|
||||||
|
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||||
|
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||||
|
// <path
|
||||||
|
// clipRule="evenodd"
|
||||||
|
// d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
|
||||||
|
// fillRule="evenodd"
|
||||||
|
// />
|
||||||
|
// </svg>
|
||||||
|
// ),
|
||||||
|
// name: 'Facebook',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// href: '#',
|
||||||
|
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||||
|
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||||
|
// <path
|
||||||
|
// clipRule="evenodd"
|
||||||
|
// d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
|
||||||
|
// fillRule="evenodd"
|
||||||
|
// />
|
||||||
|
// </svg>
|
||||||
|
// ),
|
||||||
|
// name: 'Instagram',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
href: 'https://github.com/yangshun/tech-interview-handbook',
|
||||||
|
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: 'GitHub',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full overflow-y-auto bg-white">
|
||||||
|
<main>
|
||||||
|
{/* Hero section */}
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
|
||||||
|
<img
|
||||||
|
alt="Tech Offers Repo"
|
||||||
|
className="mx-auto mb-8 w-auto"
|
||||||
|
src="/offers-logo.svg"
|
||||||
|
/>
|
||||||
|
<h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
|
||||||
|
<span>Choosing offers </span>
|
||||||
|
<span className="from-primary-600 -mb-1 mr-2 bg-gradient-to-r to-purple-500 bg-clip-text pb-1 pr-4 italic text-transparent">
|
||||||
|
made easier
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto mt-6 max-w-lg text-center text-xl sm:max-w-3xl">
|
||||||
|
Analyze your offers using profiles from fellow software engineers.
|
||||||
|
</p>
|
||||||
|
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
|
||||||
|
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-1 sm:gap-5 sm:space-y-0">
|
||||||
|
<a
|
||||||
|
className="border-grey-600 flex items-center justify-center rounded-md border bg-white px-4 py-3 text-base font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 sm:px-8"
|
||||||
|
href={HOME_URL}>
|
||||||
|
Get started
|
||||||
|
</a>
|
||||||
|
{/* <a
|
||||||
|
className="bg-primary-500 flex items-center justify-center rounded-md border border-transparent px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-opacity-70 sm:px-8"
|
||||||
|
href="#">
|
||||||
|
Live demo
|
||||||
|
</a> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Alternating Feature Sections */}
|
||||||
|
<div className="relative overflow-hidden pt-16 pb-32">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 top-0 h-48 bg-gradient-to-b from-gray-100"
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<LeftTextCard
|
||||||
|
description="Filter relevant offers by job title, company, submission date, salary and more."
|
||||||
|
icon={
|
||||||
|
<TableCellsIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
imageAlt="Offer table page"
|
||||||
|
imageSrc={offersBrowse}
|
||||||
|
title="Stay informed of recent offers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-36">
|
||||||
|
<RightTextCard
|
||||||
|
description="With our offer engine analysis, you can compare your offers with other offers on the market and make an informed decision."
|
||||||
|
icon={
|
||||||
|
<ChartBarSquareIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
imageAlt="Customer profile user interface"
|
||||||
|
imageSrc={offersAnalysis}
|
||||||
|
title="Better understand your offers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-36">
|
||||||
|
<LeftTextCard
|
||||||
|
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
|
||||||
|
icon={
|
||||||
|
<InformationCircleIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
imageAlt="Offer table page"
|
||||||
|
imageSrc={offersProfile}
|
||||||
|
title="Choosing an offer needs context"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient Feature Section */}
|
||||||
|
<div className="to-primary-600 bg-gradient-to-r from-purple-800">
|
||||||
|
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 sm:pt-20 sm:pb-24 lg:max-w-7xl lg:px-8 lg:pt-24">
|
||||||
|
<h2 className="flex justify-center text-4xl font-bold tracking-tight text-white">
|
||||||
|
Your privacy is our priority.
|
||||||
|
</h2>
|
||||||
|
<p className="text-primary-100 mt-4 flex flex-row justify-center text-lg">
|
||||||
|
All offer profiles are anonymized and we do not store information
|
||||||
|
about your personal identity.
|
||||||
|
</p>
|
||||||
|
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div key={feature.name}>
|
||||||
|
<div>
|
||||||
|
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-white bg-opacity-10">
|
||||||
|
<feature.icon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 text-white"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-lg font-medium text-white">
|
||||||
|
{feature.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-primary-100 mt-2 text-base">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<div className="bg-white">
|
||||||
|
<div className="mx-auto max-w-4xl py-16 px-4 sm:px-6 sm:py-24 lg:flex lg:max-w-7xl lg:items-center lg:justify-between lg:px-8">
|
||||||
|
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||||
|
<span className="block">Ready to get started?</span>
|
||||||
|
<span className="to-primary-600 -mb-1 block bg-gradient-to-r from-purple-600 bg-clip-text pb-1 text-transparent">
|
||||||
|
Create your own offer profile today.
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div className="mt-6 space-y-4 sm:flex sm:space-y-0 sm:space-x-5">
|
||||||
|
<a
|
||||||
|
className="to-primary-500 flex items-center justify-center rounded-md border border-transparent bg-gradient-to-r from-purple-600 bg-origin-border px-4 py-3 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
|
||||||
|
href={HOME_URL}>
|
||||||
|
Get Started
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer aria-labelledby="footer-heading" className="bg-gray-50">
|
||||||
|
<h2 className="sr-only" id="footer-heading">
|
||||||
|
Footer
|
||||||
|
</h2>
|
||||||
|
<div className="mx-auto max-w-7xl px-4 pt-0 pb-8 sm:px-6 lg:px-8">
|
||||||
|
<div className="mt-12 border-t border-gray-200 pt-8 md:flex md:items-center md:justify-between lg:mt-16">
|
||||||
|
<div className="flex space-x-6 md:order-2">
|
||||||
|
{footerNavigation.social.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
className="text-gray-400 hover:text-gray-500"
|
||||||
|
href={item.href}>
|
||||||
|
<span className="sr-only">{item.name}</span>
|
||||||
|
<item.icon aria-hidden="true" className="h-6 w-6" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-8 text-base text-gray-400 md:order-1 md:mt-0">
|
||||||
|
© 2022 Tech Offers Repo. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,244 +1,79 @@
|
|||||||
import type { SVGProps } from 'react';
|
import Link from 'next/link';
|
||||||
import {
|
import { useState } from 'react';
|
||||||
BookmarkSquareIcon,
|
import { Banner } from '@tih/ui';
|
||||||
ChartBarSquareIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
ShareIcon,
|
|
||||||
TableCellsIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from '@heroicons/react/24/outline';
|
|
||||||
|
|
||||||
import LeftTextCard from '~/components/offers/landing/LeftTextCard';
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
import RightTextCard from '~/components/offers/landing/RightTextCard';
|
import OffersTable from '~/components/offers/table/OffersTable';
|
||||||
import { HOME_URL } from '~/components/offers/types';
|
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||||
|
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||||
|
|
||||||
const features = [
|
export default function OffersHomePage() {
|
||||||
{
|
const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
|
||||||
description:
|
const [companyFilter, setCompanyFilter] = useState('');
|
||||||
'Profile names are randomly generated to keep your offers strictly anonymous.',
|
const { event: gaEvent } = useGoogleAnalytics();
|
||||||
icon: UsersIcon,
|
|
||||||
name: 'Anonymized Profile Name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
'Only users with the edit link can edit that profile. Share profiles to others using a public link without giving edit permission.',
|
|
||||||
icon: ShareIcon,
|
|
||||||
name: 'Edit Link v.s. Public Link',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"Offer profiles will not be automatically saved under creators' account in our database unless explicit permission is given.",
|
|
||||||
icon: BookmarkSquareIcon,
|
|
||||||
name: 'Save with Permission',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const footerNavigation = {
|
|
||||||
social: [
|
|
||||||
{
|
|
||||||
href: '#',
|
|
||||||
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
name: 'Facebook',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '#',
|
|
||||||
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
name: 'Instagram',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'https://github.com/yangshun/tech-interview-handbook',
|
|
||||||
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
name: 'GitHub',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LandingPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full overflow-y-auto bg-white">
|
<main className="flex-1 overflow-y-auto">
|
||||||
<main>
|
<Banner size="sm">
|
||||||
{/* Hero section */}
|
⭐ Check if your offer is competitive by submitting it{' '}
|
||||||
<div className="relative h-full">
|
<Link className="underline" href="/offers/submit">
|
||||||
<div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
|
here
|
||||||
<h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
|
</Link>
|
||||||
<span>Choosing offers </span>
|
. ⭐
|
||||||
<span className="from-primary-600 -mb-1 mr-2 bg-gradient-to-r to-purple-500 bg-clip-text pb-1 pr-4 italic text-transparent">
|
</Banner>
|
||||||
made easier
|
<div className="bg-slate-100 py-16 px-4">
|
||||||
</span>
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-primary-600 text-center text-4xl font-bold sm:text-5xl">
|
||||||
|
Tech Offers Repo
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto mt-6 max-w-lg text-center text-xl sm:max-w-3xl">
|
|
||||||
Analyze your offers using profiles from fellow software engineers.
|
|
||||||
</p>
|
|
||||||
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
|
|
||||||
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0">
|
|
||||||
<a
|
|
||||||
className="border-grey-600 flex items-center justify-center rounded-md border bg-white bg-white px-4 py-3 text-base font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 sm:px-8"
|
|
||||||
href={HOME_URL}>
|
|
||||||
Get started
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="bg-primary-500 flex items-center justify-center rounded-md border border-transparent px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-opacity-70 sm:px-8"
|
|
||||||
href="#">
|
|
||||||
Live demo
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-4 text-center text-lg text-slate-600 sm:text-2xl">
|
||||||
|
Find out how good your offer is. Discover how others got their
|
||||||
{/* Alternating Feature Sections */}
|
offers.
|
||||||
<div className="relative overflow-hidden pt-16 pb-32">
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-x-0 top-0 h-48 bg-gradient-to-b from-gray-100"
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
|
||||||
<LeftTextCard
|
|
||||||
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
|
|
||||||
icon={
|
|
||||||
<InformationCircleIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-6 w-6 text-white"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
imageAlt="Offer table page"
|
|
||||||
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
|
|
||||||
title="Choosing an offer needs context"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-36">
|
</div>
|
||||||
<RightTextCard
|
<div className="mt-6 flex flex-col items-center justify-center space-y-2 text-base text-slate-700 sm:mt-10 sm:flex-row sm:space-y-0 sm:space-x-4 sm:text-lg">
|
||||||
description="With our offer engine analysis, you can compare your offers with other offers on the market and make an informed decision."
|
<span>Viewing offers for</span>
|
||||||
icon={
|
<div className="flex items-center space-x-4">
|
||||||
<ChartBarSquareIcon
|
<JobTitlesTypeahead
|
||||||
aria-hidden="true"
|
isLabelHidden={true}
|
||||||
className="h-6 w-6 text-white"
|
placeholder="Software Engineer"
|
||||||
/>
|
onSelect={(option) => {
|
||||||
}
|
if (option) {
|
||||||
imageAlt="Customer profile user interface"
|
setjobTitleFilter(option.value);
|
||||||
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-2.jpg"
|
gaEvent({
|
||||||
title="Better understand your offers"
|
action: `offers.table_filter_job_title_${option.value}`,
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'Filter by job title',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
<span>in</span>
|
||||||
<div className="mt-36">
|
<CompaniesTypeahead
|
||||||
<LeftTextCard
|
isLabelHidden={true}
|
||||||
description="Filter relevant offers by job title, company, submission date, salary and more."
|
placeholder="All Companies"
|
||||||
icon={
|
onSelect={(option) => {
|
||||||
<TableCellsIcon
|
if (option) {
|
||||||
aria-hidden="true"
|
setCompanyFilter(option.value);
|
||||||
className="h-6 w-6 text-white"
|
gaEvent({
|
||||||
/>
|
action: 'offers.table_filter_company',
|
||||||
}
|
category: 'engagement',
|
||||||
imageAlt="Offer table page"
|
label: 'Filter by company',
|
||||||
imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
|
});
|
||||||
title="Stay informed of recent offers"
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Gradient Feature Section */}
|
<div className="flex justify-center bg-white pb-20 pt-10">
|
||||||
<div className="to-primary-600 bg-gradient-to-r from-purple-800">
|
<OffersTable
|
||||||
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 sm:pt-20 sm:pb-24 lg:max-w-7xl lg:px-8 lg:pt-24">
|
companyFilter={companyFilter}
|
||||||
<h2 className="flex justify-center text-4xl font-bold tracking-tight text-white">
|
jobTitleFilter={jobTitleFilter}
|
||||||
Your privacy is our priority.
|
/>
|
||||||
</h2>
|
</div>
|
||||||
<p className="text-primary-100 mt-4 flex flex-row justify-center text-lg">
|
</main>
|
||||||
All offer profiles are anonymized and we do not store information
|
|
||||||
about your personal identity.
|
|
||||||
</p>
|
|
||||||
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<div key={feature.name}>
|
|
||||||
<div>
|
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-white bg-opacity-10">
|
|
||||||
<feature.icon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-6 w-6 text-white"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-lg font-medium text-white">
|
|
||||||
{feature.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-primary-100 mt-2 text-base">
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<div className="bg-white">
|
|
||||||
<div className="mx-auto max-w-4xl py-16 px-4 sm:px-6 sm:py-24 lg:flex lg:max-w-7xl lg:items-center lg:justify-between lg:px-8">
|
|
||||||
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
|
||||||
<span className="block">Ready to get started?</span>
|
|
||||||
<span className="to-primary-600 -mb-1 block bg-gradient-to-r from-purple-600 bg-clip-text pb-1 text-transparent">
|
|
||||||
Create your own offer profile today.
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div className="mt-6 space-y-4 sm:flex sm:space-y-0 sm:space-x-5">
|
|
||||||
<a
|
|
||||||
className="to-primary-500 flex items-center justify-center rounded-md border border-transparent bg-gradient-to-r from-purple-600 bg-origin-border px-4 py-3 text-base font-medium text-white shadow-sm hover:from-purple-700 hover:to-indigo-700"
|
|
||||||
href={HOME_URL}>
|
|
||||||
Get Started
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer aria-labelledby="footer-heading" className="bg-gray-50">
|
|
||||||
<h2 className="sr-only" id="footer-heading">
|
|
||||||
Footer
|
|
||||||
</h2>
|
|
||||||
<div className="mx-auto max-w-7xl px-4 pt-0 pb-8 sm:px-6 lg:px-8">
|
|
||||||
<div className="mt-12 border-t border-gray-200 pt-8 md:flex md:items-center md:justify-between lg:mt-16">
|
|
||||||
<div className="flex space-x-6 md:order-2">
|
|
||||||
{footerNavigation.social.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
className="text-gray-400 hover:text-gray-500"
|
|
||||||
href={item.href}>
|
|
||||||
<span className="sr-only">{item.name}</span>
|
|
||||||
<item.icon aria-hidden="true" className="h-6 w-6" />
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="mt-8 text-base text-gray-400 md:order-1 md:mt-0">
|
|
||||||
© 2022 Tech Interview Handbook Offer Profile Repository. All
|
|
||||||
rights reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
|
||||||
|
|
||||||
|
export default function OffersSubmissionPage() {
|
||||||
|
return <OffersSubmissionForm />;
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
|
||||||
|
import { EyeIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Button, Spinner } from '@tih/ui';
|
||||||
|
|
||||||
|
import type { BreadcrumbStep } from '~/components/offers/Breadcrumb';
|
||||||
|
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
|
||||||
|
import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave';
|
||||||
|
import OffersSubmissionAnalysis from '~/components/offers/offersSubmission/OffersSubmissionAnalysis';
|
||||||
|
|
||||||
|
import { getProfilePath } from '~/utils/offers/link';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
import type { ProfileAnalysis } from '~/types/offers';
|
||||||
|
|
||||||
|
export default function OffersSubmissionResult() {
|
||||||
|
const router = useRouter();
|
||||||
|
let { offerProfileId, token = '' } = router.query;
|
||||||
|
offerProfileId = offerProfileId as string;
|
||||||
|
token = token as string;
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [analysis, setAnalysis] = useState<ProfileAnalysis | null>(null);
|
||||||
|
|
||||||
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollToTop = () =>
|
||||||
|
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
|
||||||
|
|
||||||
|
// TODO: Check if the token is valid before showing this page
|
||||||
|
const getAnalysis = trpc.useQuery(
|
||||||
|
['offers.analysis.get', { profileId: offerProfileId }],
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
setAnalysis(data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
<OffersProfileSave key={0} profileId={offerProfileId} token={token} />,
|
||||||
|
<OffersSubmissionAnalysis
|
||||||
|
key={1}
|
||||||
|
analysis={analysis}
|
||||||
|
isError={getAnalysis.isError}
|
||||||
|
isLoading={getAnalysis.isLoading}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
|
||||||
|
const breadcrumbSteps: Array<BreadcrumbStep> = [
|
||||||
|
{
|
||||||
|
label: 'Offers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Background',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Save profile',
|
||||||
|
step: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Analysis',
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToTop();
|
||||||
|
}, [step]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{getAnalysis.isLoading && (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<div className="m-auto mx-auto w-screen justify-center">
|
||||||
|
<Spinner display="block" size="lg" />
|
||||||
|
<div className="text-center">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!getAnalysis.isLoading && (
|
||||||
|
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
|
||||||
|
<div className="mb-20 flex justify-center">
|
||||||
|
<div className="my-5 block w-full max-w-screen-md rounded-lg bg-white py-10 px-10 shadow-lg">
|
||||||
|
<div className="mb-4 flex justify-end">
|
||||||
|
<Breadcrumbs
|
||||||
|
currentStep={step}
|
||||||
|
setStep={setStep}
|
||||||
|
steps={breadcrumbSteps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{steps[step]}
|
||||||
|
{step === 0 && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
disabled={false}
|
||||||
|
icon={ArrowRightIcon}
|
||||||
|
label="Next"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setStep(step + 1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
addonPosition="start"
|
||||||
|
icon={ArrowLeftIcon}
|
||||||
|
label="Previous"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setStep(step - 1)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
href={getProfilePath(
|
||||||
|
offerProfileId as string,
|
||||||
|
token as string,
|
||||||
|
)}
|
||||||
|
icon={EyeIcon}
|
||||||
|
label="View your profile"
|
||||||
|
variant="primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,644 +0,0 @@
|
|||||||
import Head from 'next/head';
|
|
||||||
import Router, { useRouter } from 'next/router';
|
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { Dialog, Disclosure, Transition } from '@headlessui/react';
|
|
||||||
import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
|
|
||||||
import {
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
NewspaperIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from '@heroicons/react/24/outline';
|
|
||||||
import {
|
|
||||||
CheckboxInput,
|
|
||||||
CheckboxList,
|
|
||||||
DropdownMenu,
|
|
||||||
Pagination,
|
|
||||||
Spinner,
|
|
||||||
Tabs,
|
|
||||||
TextInput,
|
|
||||||
} from '@tih/ui';
|
|
||||||
|
|
||||||
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
|
|
||||||
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
|
|
||||||
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
|
|
||||||
|
|
||||||
import type { Filter, FilterId, Shortcut } from '~/utils/resumes/resumeFilters';
|
|
||||||
import {
|
|
||||||
BROWSE_TABS_VALUES,
|
|
||||||
EXPERIENCES,
|
|
||||||
INITIAL_FILTER_STATE,
|
|
||||||
isInitialFilterState,
|
|
||||||
LOCATIONS,
|
|
||||||
ROLES,
|
|
||||||
SHORTCUTS,
|
|
||||||
SORT_OPTIONS,
|
|
||||||
} from '~/utils/resumes/resumeFilters';
|
|
||||||
import useDebounceValue from '~/utils/resumes/useDebounceValue';
|
|
||||||
import useSearchParams from '~/utils/resumes/useSearchParams';
|
|
||||||
import { trpc } from '~/utils/trpc';
|
|
||||||
|
|
||||||
import type { FilterState, SortOrder } from '../../utils/resumes/resumeFilters';
|
|
||||||
|
|
||||||
const STALE_TIME = 5 * 60 * 1000;
|
|
||||||
const DEBOUNCE_DELAY = 800;
|
|
||||||
const PAGE_LIMIT = 10;
|
|
||||||
const filters: Array<Filter> = [
|
|
||||||
{
|
|
||||||
id: 'role',
|
|
||||||
label: 'Role',
|
|
||||||
options: ROLES,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'experience',
|
|
||||||
label: 'Experience',
|
|
||||||
options: EXPERIENCES,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'location',
|
|
||||||
label: 'Location',
|
|
||||||
options: LOCATIONS,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getLoggedOutText = (tabsValue: string) => {
|
|
||||||
switch (tabsValue) {
|
|
||||||
case BROWSE_TABS_VALUES.STARRED:
|
|
||||||
return 'to view starred resumes!';
|
|
||||||
case BROWSE_TABS_VALUES.MY:
|
|
||||||
return 'to view your submitted resumes!';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEmptyDataText = (
|
|
||||||
tabsValue: string,
|
|
||||||
searchValue: string,
|
|
||||||
userFilters: FilterState,
|
|
||||||
) => {
|
|
||||||
if (searchValue.length > 0) {
|
|
||||||
return 'Try tweaking your search text to see more resumes.';
|
|
||||||
}
|
|
||||||
if (!isInitialFilterState(userFilters)) {
|
|
||||||
return 'Try tweaking your filters to see more resumes.';
|
|
||||||
}
|
|
||||||
switch (tabsValue) {
|
|
||||||
case BROWSE_TABS_VALUES.ALL:
|
|
||||||
return 'Looks like SWEs are feeling lucky!';
|
|
||||||
case BROWSE_TABS_VALUES.STARRED:
|
|
||||||
return 'You have not starred any resumes. Star one to see it here!';
|
|
||||||
case BROWSE_TABS_VALUES.MY:
|
|
||||||
return 'Upload a resume to see it here!';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ResumeHomePage() {
|
|
||||||
const { data: sessionData } = useSession();
|
|
||||||
const router = useRouter();
|
|
||||||
const [tabsValue, setTabsValue, isTabsValueInit] = useSearchParams(
|
|
||||||
'tabsValue',
|
|
||||||
BROWSE_TABS_VALUES.ALL,
|
|
||||||
);
|
|
||||||
const [sortOrder, setSortOrder, isSortOrderInit] = useSearchParams<SortOrder>(
|
|
||||||
'sortOrder',
|
|
||||||
'latest',
|
|
||||||
);
|
|
||||||
const [searchValue, setSearchValue, isSearchValueInit] = useSearchParams(
|
|
||||||
'searchValue',
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
const [shortcutSelected, setShortcutSelected, isShortcutInit] =
|
|
||||||
useSearchParams('shortcutSelected', 'All');
|
|
||||||
const [currentPage, setCurrentPage, isCurrentPageInit] = useSearchParams(
|
|
||||||
'currentPage',
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
const [userFilters, setUserFilters, isUserFiltersInit] = useSearchParams(
|
|
||||||
'userFilters',
|
|
||||||
INITIAL_FILTER_STATE,
|
|
||||||
);
|
|
||||||
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
|
||||||
|
|
||||||
const skip = (currentPage - 1) * PAGE_LIMIT;
|
|
||||||
const isSearchOptionsInit = useMemo(() => {
|
|
||||||
return (
|
|
||||||
isTabsValueInit &&
|
|
||||||
isSortOrderInit &&
|
|
||||||
isSearchValueInit &&
|
|
||||||
isShortcutInit &&
|
|
||||||
isCurrentPageInit &&
|
|
||||||
isUserFiltersInit
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
isTabsValueInit,
|
|
||||||
isSortOrderInit,
|
|
||||||
isSearchValueInit,
|
|
||||||
isShortcutInit,
|
|
||||||
isCurrentPageInit,
|
|
||||||
isUserFiltersInit,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPage(1);
|
|
||||||
}, [userFilters, sortOrder, setCurrentPage, searchValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Router.replace used instead of router.replace to avoid
|
|
||||||
// the page reloading itself since the router.replace
|
|
||||||
// callback changes on every page load
|
|
||||||
if (!isSearchOptionsInit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Router.replace({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: {
|
|
||||||
currentPage: JSON.stringify(currentPage),
|
|
||||||
searchValue: JSON.stringify(searchValue),
|
|
||||||
shortcutSelected: JSON.stringify(shortcutSelected),
|
|
||||||
sortOrder: JSON.stringify(sortOrder),
|
|
||||||
tabsValue: JSON.stringify(tabsValue),
|
|
||||||
userFilters: JSON.stringify(userFilters),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
tabsValue,
|
|
||||||
sortOrder,
|
|
||||||
searchValue,
|
|
||||||
userFilters,
|
|
||||||
shortcutSelected,
|
|
||||||
currentPage,
|
|
||||||
router.pathname,
|
|
||||||
isSearchOptionsInit,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const allResumesQuery = trpc.useQuery(
|
|
||||||
[
|
|
||||||
'resumes.resume.findAll',
|
|
||||||
{
|
|
||||||
experienceFilters: userFilters.experience,
|
|
||||||
locationFilters: userFilters.location,
|
|
||||||
numComments: userFilters.numComments,
|
|
||||||
roleFilters: userFilters.role,
|
|
||||||
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
|
|
||||||
skip,
|
|
||||||
sortOrder,
|
|
||||||
take: PAGE_LIMIT,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
|
|
||||||
staleTime: STALE_TIME,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const starredResumesQuery = trpc.useQuery(
|
|
||||||
[
|
|
||||||
'resumes.resume.user.findUserStarred',
|
|
||||||
{
|
|
||||||
experienceFilters: userFilters.experience,
|
|
||||||
locationFilters: userFilters.location,
|
|
||||||
numComments: userFilters.numComments,
|
|
||||||
roleFilters: userFilters.role,
|
|
||||||
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
|
|
||||||
skip,
|
|
||||||
sortOrder,
|
|
||||||
take: PAGE_LIMIT,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
enabled: tabsValue === BROWSE_TABS_VALUES.STARRED,
|
|
||||||
retry: false,
|
|
||||||
staleTime: STALE_TIME,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const myResumesQuery = trpc.useQuery(
|
|
||||||
[
|
|
||||||
'resumes.resume.user.findUserCreated',
|
|
||||||
{
|
|
||||||
experienceFilters: userFilters.experience,
|
|
||||||
locationFilters: userFilters.location,
|
|
||||||
numComments: userFilters.numComments,
|
|
||||||
roleFilters: userFilters.role,
|
|
||||||
searchValue: useDebounceValue(searchValue, DEBOUNCE_DELAY),
|
|
||||||
skip,
|
|
||||||
sortOrder,
|
|
||||||
take: PAGE_LIMIT,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
enabled: tabsValue === BROWSE_TABS_VALUES.MY,
|
|
||||||
retry: false,
|
|
||||||
staleTime: STALE_TIME,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmitResume = () => {
|
|
||||||
if (sessionData === null) {
|
|
||||||
router.push('/api/auth/signin');
|
|
||||||
} else {
|
|
||||||
router.push('/resumes/submit');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFilterCheckboxChange = (
|
|
||||||
isChecked: boolean,
|
|
||||||
filterSection: FilterId,
|
|
||||||
filterValue: string,
|
|
||||||
) => {
|
|
||||||
if (isChecked) {
|
|
||||||
setUserFilters({
|
|
||||||
...userFilters,
|
|
||||||
[filterSection]: [...userFilters[filterSection], filterValue],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setUserFilters({
|
|
||||||
...userFilters,
|
|
||||||
[filterSection]: userFilters[filterSection].filter(
|
|
||||||
(value) => value !== filterValue,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onShortcutChange = ({
|
|
||||||
sortOrder: shortcutSortOrder,
|
|
||||||
filters: shortcutFilters,
|
|
||||||
name: shortcutName,
|
|
||||||
}: Shortcut) => {
|
|
||||||
setShortcutSelected(shortcutName);
|
|
||||||
setSortOrder(shortcutSortOrder);
|
|
||||||
setUserFilters(shortcutFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTabChange = (tab: string) => {
|
|
||||||
setTabsValue(tab);
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabQueryData = () => {
|
|
||||||
switch (tabsValue) {
|
|
||||||
case BROWSE_TABS_VALUES.ALL:
|
|
||||||
return allResumesQuery.data;
|
|
||||||
case BROWSE_TABS_VALUES.STARRED:
|
|
||||||
return starredResumesQuery.data;
|
|
||||||
case BROWSE_TABS_VALUES.MY:
|
|
||||||
return myResumesQuery.data;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabResumes = () => {
|
|
||||||
return getTabQueryData()?.mappedResumeData ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabTotalPages = () => {
|
|
||||||
const numRecords = getTabQueryData()?.totalRecords ?? 0;
|
|
||||||
return numRecords % PAGE_LIMIT === 0
|
|
||||||
? numRecords / PAGE_LIMIT
|
|
||||||
: Math.floor(numRecords / PAGE_LIMIT) + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFetchingResumes =
|
|
||||||
allResumesQuery.isFetching ||
|
|
||||||
starredResumesQuery.isFetching ||
|
|
||||||
myResumesQuery.isFetching;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Resume Review Portal</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
{/* Mobile Filters */}
|
|
||||||
<div>
|
|
||||||
<Transition.Root as={Fragment} show={mobileFiltersOpen}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
className="relative z-40 lg:hidden"
|
|
||||||
onClose={setMobileFiltersOpen}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition-opacity ease-linear duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="transition-opacity ease-linear duration-300"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0">
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-40 flex">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-in-out duration-300 transform"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transition ease-in-out duration-300 transform"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full">
|
|
||||||
<Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-scroll bg-white py-4 pb-12 shadow-xl">
|
|
||||||
<div className="flex items-center justify-between px-4">
|
|
||||||
<h2 className="text-lg font-medium text-slate-900">
|
|
||||||
Shortcuts
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-slate-400"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileFiltersOpen(false)}>
|
|
||||||
<span className="sr-only">Close menu</span>
|
|
||||||
<XMarkIcon aria-hidden="true" className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form className="mt-4 border-t border-slate-200">
|
|
||||||
<ul
|
|
||||||
className="flex flex-wrap justify-start gap-4 px-4 py-3 font-medium text-slate-900"
|
|
||||||
role="list">
|
|
||||||
{SHORTCUTS.map((shortcut) => (
|
|
||||||
<li key={shortcut.name}>
|
|
||||||
<ResumeFilterPill
|
|
||||||
isSelected={shortcutSelected === shortcut.name}
|
|
||||||
title={shortcut.name}
|
|
||||||
onClick={() => onShortcutChange(shortcut)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{filters.map((filter) => (
|
|
||||||
<Disclosure
|
|
||||||
key={filter.id}
|
|
||||||
as="div"
|
|
||||||
className="border-t border-slate-200 px-4 py-6">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<h3 className="-mx-2 -my-3 flow-root">
|
|
||||||
<Disclosure.Button className="flex w-full items-center justify-between bg-white px-2 py-3 text-slate-400 hover:text-slate-500">
|
|
||||||
<span className="font-medium text-slate-900">
|
|
||||||
{filter.label}
|
|
||||||
</span>
|
|
||||||
<span className="ml-6 flex items-center">
|
|
||||||
{open ? (
|
|
||||||
<MinusIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-5 w-5"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PlusIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-5 w-5"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Disclosure.Button>
|
|
||||||
</h3>
|
|
||||||
<Disclosure.Panel className="pt-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{filter.options.map((option) => (
|
|
||||||
<div
|
|
||||||
key={option.value}
|
|
||||||
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 [&>div>div:nth-child(2)>label]:font-normal">
|
|
||||||
<CheckboxInput
|
|
||||||
label={option.label}
|
|
||||||
value={userFilters[filter.id].includes(
|
|
||||||
option.value,
|
|
||||||
)}
|
|
||||||
onChange={(isChecked) =>
|
|
||||||
onFilterCheckboxChange(
|
|
||||||
isChecked,
|
|
||||||
filter.id,
|
|
||||||
option.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
))}
|
|
||||||
</form>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main className="h-[calc(100vh-4rem)] flex-auto px-8 pb-4">
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<div className="fixed top-0 bottom-0 mt-24 hidden w-64 overflow-auto lg:block">
|
|
||||||
<h3 className="text-md font-medium tracking-tight text-gray-900">
|
|
||||||
Shortcuts
|
|
||||||
</h3>
|
|
||||||
<div className="w-100 pt-4 sm:pr-0 md:pr-4">
|
|
||||||
<form>
|
|
||||||
<ul
|
|
||||||
className="flex w-11/12 flex-wrap justify-start gap-2 pb-6 text-sm font-medium text-slate-900"
|
|
||||||
role="list">
|
|
||||||
{SHORTCUTS.map((shortcut) => (
|
|
||||||
<li key={shortcut.name}>
|
|
||||||
<ResumeFilterPill
|
|
||||||
isSelected={shortcutSelected === shortcut.name}
|
|
||||||
title={shortcut.name}
|
|
||||||
onClick={() => onShortcutChange(shortcut)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<h3 className="text-md font-medium tracking-tight text-slate-900">
|
|
||||||
Explore these filters
|
|
||||||
</h3>
|
|
||||||
{filters.map((filter) => (
|
|
||||||
<Disclosure
|
|
||||||
key={filter.id}
|
|
||||||
as="div"
|
|
||||||
className="border-b border-slate-200 py-6">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<h3 className="-my-3 flow-root">
|
|
||||||
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-slate-400 hover:text-slate-500">
|
|
||||||
<span className="font-medium text-slate-900">
|
|
||||||
{filter.label}
|
|
||||||
</span>
|
|
||||||
<span className="ml-6 flex items-center">
|
|
||||||
{open ? (
|
|
||||||
<MinusIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-5 w-5"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PlusIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
className="h-5 w-5"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Disclosure.Button>
|
|
||||||
</h3>
|
|
||||||
<Disclosure.Panel className="pt-4">
|
|
||||||
<CheckboxList
|
|
||||||
description=""
|
|
||||||
isLabelHidden={true}
|
|
||||||
label=""
|
|
||||||
orientation="vertical">
|
|
||||||
{filter.options.map((option) => (
|
|
||||||
<div
|
|
||||||
key={option.value}
|
|
||||||
className="[&>div>div:nth-child(1)>input]:text-primary-600 [&>div>div:nth-child(1)>input]:ring-primary-500 px-1 [&>div>div:nth-child(2)>label]:font-normal">
|
|
||||||
<CheckboxInput
|
|
||||||
label={option.label}
|
|
||||||
value={userFilters[filter.id].includes(
|
|
||||||
option.value,
|
|
||||||
)}
|
|
||||||
onChange={(isChecked) =>
|
|
||||||
onFilterCheckboxChange(
|
|
||||||
isChecked,
|
|
||||||
filter.id,
|
|
||||||
option.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</CheckboxList>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
))}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative lg:left-64 lg:w-[calc(100%-16rem)]">
|
|
||||||
<div className="lg:border-grey-200 sticky top-0 z-0 flex flex-wrap items-center justify-between pt-6 pb-2 lg:border-b">
|
|
||||||
<div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none lg:pb-0">
|
|
||||||
<div>
|
|
||||||
<Tabs
|
|
||||||
label="Resume Browse Tabs"
|
|
||||||
tabs={[
|
|
||||||
{
|
|
||||||
label: 'All Resumes',
|
|
||||||
value: BROWSE_TABS_VALUES.ALL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Starred Resumes',
|
|
||||||
value: BROWSE_TABS_VALUES.STARRED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'My Resumes',
|
|
||||||
value: BROWSE_TABS_VALUES.MY,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={tabsValue}
|
|
||||||
onChange={onTabChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="bg-primary-500 ml-4 rounded-md py-2 px-3 text-sm font-medium text-white lg:hidden"
|
|
||||||
type="button"
|
|
||||||
onClick={onSubmitResume}>
|
|
||||||
Submit Resume
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center justify-start gap-8">
|
|
||||||
<div className="w-64">
|
|
||||||
<TextInput
|
|
||||||
isLabelHidden={true}
|
|
||||||
label="search"
|
|
||||||
placeholder="Search Resumes"
|
|
||||||
startAddOn={MagnifyingGlassIcon}
|
|
||||||
startAddOnType="icon"
|
|
||||||
type="text"
|
|
||||||
value={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DropdownMenu
|
|
||||||
align="end"
|
|
||||||
label={
|
|
||||||
SORT_OPTIONS.find(({ value }) => value === sortOrder)
|
|
||||||
?.label
|
|
||||||
}>
|
|
||||||
{SORT_OPTIONS.map(({ label, value }) => (
|
|
||||||
<DropdownMenu.Item
|
|
||||||
key={value}
|
|
||||||
isSelected={sortOrder === value}
|
|
||||||
label={label}
|
|
||||||
onClick={() => setSortOrder(value)}></DropdownMenu.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="-m-2 text-slate-400 hover:text-slate-500 lg:hidden"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMobileFiltersOpen(true)}>
|
|
||||||
<span className="sr-only">Filters</span>
|
|
||||||
<FunnelIcon aria-hidden="true" className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="bg-primary-500 hidden w-36 rounded-md py-2 px-3 text-sm font-medium text-white lg:block"
|
|
||||||
type="button"
|
|
||||||
onClick={onSubmitResume}>
|
|
||||||
Submit Resume
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isFetchingResumes ? (
|
|
||||||
<div className="w-full pt-4">
|
|
||||||
{' '}
|
|
||||||
<Spinner display="block" size="lg" />{' '}
|
|
||||||
</div>
|
|
||||||
) : sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? (
|
|
||||||
<ResumeSignInButton
|
|
||||||
className="mt-8"
|
|
||||||
text={getLoggedOutText(tabsValue)}
|
|
||||||
/>
|
|
||||||
) : getTabResumes().length === 0 ? (
|
|
||||||
<div className="mt-24 flex flex-wrap justify-center">
|
|
||||||
<NewspaperIcon
|
|
||||||
className="mb-12 basis-full"
|
|
||||||
height={196}
|
|
||||||
width={196}
|
|
||||||
/>
|
|
||||||
{getEmptyDataText(tabsValue, searchValue, userFilters)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-[calc(100vh-9rem)] pb-10 lg:h-[calc(100vh-6rem)]">
|
|
||||||
<div className="h-[85%] overflow-y-auto">
|
|
||||||
<div>
|
|
||||||
<ResumeListItems resumes={getTabResumes()} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-[15%] items-center justify-center">
|
|
||||||
{getTabTotalPages() > 1 && (
|
|
||||||
<div>
|
|
||||||
<Pagination
|
|
||||||
current={currentPage}
|
|
||||||
end={getTabTotalPages()}
|
|
||||||
label="pagination"
|
|
||||||
start={1}
|
|
||||||
onSelect={(page) => setCurrentPage(page)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,21 @@
|
|||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
import { CallToAction } from '~/components/resumes/landing/CallToAction';
|
||||||
|
import { Hero } from '~/components/resumes/landing/Hero';
|
||||||
|
import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Resume Review Portal</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<main className="h-full w-full overflow-y-auto">
|
||||||
|
<Hero />
|
||||||
|
<PrimaryFeatures />
|
||||||
|
<CallToAction />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue