From d200793d20e95d5f9b97a31c645b703d1b781604 Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Fri, 21 Oct 2022 21:50:01 +0800 Subject: [PATCH 01/19] [offers][feat] Allowing showing income based on selected salary --- .../components/offers/table/OffersTable.tsx | 6 +- .../src/pages/offers/test/listOffers.tsx | 3 +- .../router/offers/offers-comments-router.ts | 57 +-- .../portal/src/server/router/offers/offers.ts | 52 ++- .../utils/offers/currency/CurrencyEnum.tsx | 329 +++++++++--------- .../offers/currency/currency-exchange.ts | 14 + yarn.lock | 74 ++-- 7 files changed, 295 insertions(+), 240 deletions(-) create mode 100644 apps/portal/src/utils/offers/currency/currency-exchange.ts diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index 9f46d4d4..df0622f6 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -15,6 +15,7 @@ import { trpc } from '~/utils/trpc'; import OffersRow from './OffersRow'; import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers'; +import { Currency } from '~/utils/offers/currency/CurrencyEnum'; const NUMBER_OF_OFFERS_IN_PAGE = 10; export type OffersTableProps = Readonly<{ @@ -25,7 +26,7 @@ export default function OffersTable({ companyFilter, jobTitleFilter, }: OffersTableProps) { - const [currency, setCurrency] = useState('SGD'); // TODO: Detect location + const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY); const [pagination, setPagination] = useState({ currentPage: 0, @@ -44,12 +45,13 @@ export default function OffersTable({ numOfPages: 0, totalItems: 0, }); - }, [selectedTab]); + }, [selectedTab, currency]); const offersQuery = trpc.useQuery( [ 'offers.list', { companyId: companyFilter, + currency, limit: NUMBER_OF_OFFERS_IN_PAGE, location: 'Singapore, Singapore', // TODO: Geolocation offset: pagination.currentPage, diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx index 143d1826..1e8545d2 100644 --- a/apps/portal/src/pages/offers/test/listOffers.tsx +++ b/apps/portal/src/pages/offers/test/listOffers.tsx @@ -6,11 +6,12 @@ function Test() { const data = trpc.useQuery([ 'offers.list', { + currency: "aed", limit: 100, location: 'Singapore, Singapore', offset: 0, sortBy: '+totalCompensation', - yoeCategory: 1, + yoeCategory: 2, }, ]); diff --git a/apps/portal/src/server/router/offers/offers-comments-router.ts b/apps/portal/src/server/router/offers/offers-comments-router.ts index 2e6b9e38..f2160243 100644 --- a/apps/portal/src/server/router/offers/offers-comments-router.ts +++ b/apps/portal/src/server/router/offers/offers-comments-router.ts @@ -26,26 +26,27 @@ export const offersCommentsRouter = createRouter() user: true, }, orderBy: { - createdAt: 'desc' - } + createdAt: 'desc', + }, }, replyingTo: true, user: true, }, orderBy: { - createdAt: 'desc' - } + createdAt: 'desc', + }, }, }, where: { id: input.profileId, - } + }, }); const discussions: OffersDiscussion = { - data: result?.discussion + data: + result?.discussion .filter((x) => { - return x.replyingToId === null + return x.replyingToId === null; }) .map((x) => { if (x.user == null) { @@ -81,18 +82,18 @@ export const offersCommentsRouter = createRouter() message: reply.message, replies: [], replyingToId: reply.replyingToId, - user: reply.user - } + user: reply.user, + }; }), replyingToId: x.replyingToId, - user: x.user - } + user: x.user, + }; - return replyType - }) ?? [] - } + return replyType; + }) ?? [], + }; - return discussions + return discussions; }, }) .mutation('create', { @@ -101,7 +102,7 @@ export const offersCommentsRouter = createRouter() profileId: z.string(), replyingToId: z.string().optional(), token: z.string().optional(), - userId: z.string().optional() + userId: z.string().optional(), }), async resolve({ ctx, input }) { const profile = await ctx.prisma.offersProfile.findFirst({ @@ -156,7 +157,7 @@ export const offersCommentsRouter = createRouter() const created = await ctx.prisma.offersReply.findFirst({ include: { - user: true + user: true, }, where: { id: createdReply.id, @@ -175,10 +176,10 @@ export const offersCommentsRouter = createRouter() id: '', image: '', name: profile?.profileName ?? '', - } - } + }, + }; - return result + return result; } throw new trpc.TRPCError({ @@ -223,10 +224,10 @@ export const offersCommentsRouter = createRouter() include: { replies: { include: { - user: true - } + user: true, + }, }, - user: true + user: true, }, where: { id: input.id, @@ -250,8 +251,8 @@ export const offersCommentsRouter = createRouter() id: '', image: '', name: profile?.profileName ?? '', - } - } + }, + }; }), replyingToId: updated!.replyingToId, user: updated!.user ?? { @@ -260,10 +261,10 @@ export const offersCommentsRouter = createRouter() id: '', image: '', name: profile?.profileName ?? '', - } - } + }, + }; - return result + return result; } throw new trpc.TRPCError({ diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 953e133a..f4ba3b4f 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -1,12 +1,9 @@ -import { z } from 'zod'; -import { TRPCError } from '@trpc/server'; - -import { - dashboardOfferDtoMapper, - getOffersResponseMapper, -} from '~/mappers/offers-mappers'; - -import { createRouter } from '../context'; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { dashboardOfferDtoMapper, getOffersResponseMapper } from "~/mappers/offers-mappers"; +import { convert } from "~/utils/offers/currency/currency-exchange"; +import { Currency } from "~/utils/offers/currency/CurrencyEnum"; +import { createRouter } from "../context"; const yoeCategoryMap: Record = { 0: 'Internship', @@ -38,6 +35,7 @@ const createSortByValidationRegex = () => { export const offersRouter = createRouter().query('list', { input: z.object({ companyId: z.string().nullish(), + currency: z.string().nullish(), dateEnd: z.date().nullish(), dateStart: z.date().nullish(), limit: z.number().positive(), @@ -80,6 +78,9 @@ export const offersRouter = createRouter().query('list', { }, }, }, + orderBy: { + monthYearReceived: 'desc', + }, where: { AND: [ { @@ -121,6 +122,9 @@ export const offersRouter = createRouter().query('list', { }, }, }, + orderBy: { + monthYearReceived: 'desc', + }, where: { AND: [ { @@ -150,6 +154,36 @@ export const offersRouter = createRouter().query('list', { }, }); + // CONVERTING + const currency = input.currency?.toUpperCase() + if (currency != null && currency in Currency) { + data = await Promise.all( + data.map(async (offer) => { + + if (offer.offersFullTime?.totalCompensation) { + offer.offersFullTime.totalCompensation.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.totalCompensation.currency = currency; + offer.offersFullTime.baseSalary.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.baseSalary.currency = currency; + offer.offersFullTime.stocks.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.stocks.currency = currency; + offer.offersFullTime.bonus.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.bonus.currency = currency; + } else if (offer.offersIntern?.monthlySalary) { + offer.offersIntern.monthlySalary.value = await convert(offer.offersIntern.monthlySalary.value, offer.offersIntern.monthlySalary.currency, currency); + offer.offersIntern.monthlySalary.currency = currency; + } else { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Total Compensation or Salary not found', + }); + } + + return offer; + }), + ); + } + // FILTERING data = data.filter((offer) => { let validRecord = true; diff --git a/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx b/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx index 7cfbc0cb..88efa1c1 100644 --- a/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx +++ b/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx @@ -1,167 +1,170 @@ // eslint-disable-next-line no-shadow -export enum Currency { - AED = 'AED', // United Arab Emirates Dirham - AFN = 'AFN', // Afghanistan Afghani - ALL = 'ALL', // Albania Lek - AMD = 'AMD', // Armenia Dram - ANG = 'ANG', // Netherlands Antilles Guilder - AOA = 'AOA', // Angola Kwanza - ARS = 'ARS', // Argentina Peso - AUD = 'AUD', // Australia Dollar - AWG = 'AWG', // Aruba Guilder - AZN = 'AZN', // Azerbaijan New Manat - BAM = 'BAM', // Bosnia and Herzegovina Convertible Marka - BBD = 'BBD', // Barbados Dollar - BDT = 'BDT', // Bangladesh Taka - BGN = 'BGN', // Bulgaria Lev - BHD = 'BHD', // Bahrain Dinar - BIF = 'BIF', // Burundi Franc - BMD = 'BMD', // Bermuda Dollar - BND = 'BND', // Brunei Darussalam Dollar - BOB = 'BOB', // Bolivia Bolíviano - BRL = 'BRL', // Brazil Real - BSD = 'BSD', // Bahamas Dollar - BTN = 'BTN', // Bhutan Ngultrum - BWP = 'BWP', // Botswana Pula - BYR = 'BYR', // Belarus Ruble - BZD = 'BZD', // Belize Dollar - CAD = 'CAD', // Canada Dollar - CDF = 'CDF', // Congo/Kinshasa Franc - CHF = 'CHF', // Switzerland Franc - CLP = 'CLP', // Chile Peso - CNY = 'CNY', // China Yuan Renminbi - COP = 'COP', // Colombia Peso - CRC = 'CRC', // Costa Rica Colon - CUC = 'CUC', // Cuba Convertible Peso - CUP = 'CUP', // Cuba Peso - CVE = 'CVE', // Cape Verde Escudo - CZK = 'CZK', // Czech Republic Koruna - DJF = 'DJF', // Djibouti Franc - DKK = 'DKK', // Denmark Krone - DOP = 'DOP', // Dominican Republic Peso - DZD = 'DZD', // Algeria Dinar - EGP = 'EGP', // Egypt Pound - ERN = 'ERN', // Eritrea Nakfa - ETB = 'ETB', // Ethiopia Birr - EUR = 'EUR', // Euro Member Countries - FJD = 'FJD', // Fiji Dollar - FKP = 'FKP', // Falkland Islands (Malvinas) Pound - GBP = 'GBP', // United Kingdom Pound - GEL = 'GEL', // Georgia Lari - GGP = 'GGP', // Guernsey Pound - GHS = 'GHS', // Ghana Cedi - GIP = 'GIP', // Gibraltar Pound - GMD = 'GMD', // Gambia Dalasi - GNF = 'GNF', // Guinea Franc - GTQ = 'GTQ', // Guatemala Quetzal - GYD = 'GYD', // Guyana Dollar - HKD = 'HKD', // Hong Kong Dollar - HNL = 'HNL', // Honduras Lempira - HRK = 'HRK', // Croatia Kuna - HTG = 'HTG', // Haiti Gourde - HUF = 'HUF', // Hungary Forint - IDR = 'IDR', // Indonesia Rupiah - ILS = 'ILS', // Israel Shekel - IMP = 'IMP', // Isle of Man Pound - INR = 'INR', // India Rupee - IQD = 'IQD', // Iraq Dinar - IRR = 'IRR', // Iran Rial - ISK = 'ISK', // Iceland Krona - JEP = 'JEP', // Jersey Pound - JMD = 'JMD', // Jamaica Dollar - JOD = 'JOD', // Jordan Dinar - JPY = 'JPY', // Japan Yen - KES = 'KES', // Kenya Shilling - KGS = 'KGS', // Kyrgyzstan Som - KHR = 'KHR', // Cambodia Riel - KMF = 'KMF', // Comoros Franc - KPW = 'KPW', // Korea (North) Won - KRW = 'KRW', // Korea (South) Won - KWD = 'KWD', // Kuwait Dinar - KYD = 'KYD', // Cayman Islands Dollar - KZT = 'KZT', // Kazakhstan Tenge - LAK = 'LAK', // Laos Kip - LBP = 'LBP', // Lebanon Pound - LKR = 'LKR', // Sri Lanka Rupee - LRD = 'LRD', // Liberia Dollar - LSL = 'LSL', // Lesotho Loti - LYD = 'LYD', // Libya Dinar - MAD = 'MAD', // Morocco Dirham - MDL = 'MDL', // Moldova Leu - MGA = 'MGA', // Madagascar Ariary - MKD = 'MKD', // Macedonia Denar - MMK = 'MMK', // Myanmar (Burma) Kyat - MNT = 'MNT', // Mongolia Tughrik - MOP = 'MOP', // Macau Pataca - MRO = 'MRO', // Mauritania Ouguiya - MUR = 'MUR', // Mauritius Rupee - MVR = 'MVR', // Maldives (Maldive Islands) Rufiyaa - MWK = 'MWK', // Malawi Kwacha - MXN = 'MXN', // Mexico Peso - MYR = 'MYR', // Malaysia Ringgit - MZN = 'MZN', // Mozambique Metical - NAD = 'NAD', // Namibia Dollar - NGN = 'NGN', // Nigeria Naira - NIO = 'NIO', // Nicaragua Cordoba - NOK = 'NOK', // Norway Krone - NPR = 'NPR', // Nepal Rupee - NZD = 'NZD', // New Zealand Dollar - OMR = 'OMR', // Oman Rial - PAB = 'PAB', // Panama Balboa - PEN = 'PEN', // Peru Sol - PGK = 'PGK', // Papua New Guinea Kina - PHP = 'PHP', // Philippines Peso - PKR = 'PKR', // Pakistan Rupee - PLN = 'PLN', // Poland Zloty - PYG = 'PYG', // Paraguay Guarani - QAR = 'QAR', // Qatar Riyal - RON = 'RON', // Romania New Leu - RSD = 'RSD', // Serbia Dinar - RUB = 'RUB', // Russia Ruble - RWF = 'RWF', // Rwanda Franc - SAR = 'SAR', // Saudi Arabia Riyal - SBD = 'SBD', // Solomon Islands Dollar - SCR = 'SCR', // Seychelles Rupee - SDG = 'SDG', // Sudan Pound - SEK = 'SEK', // Sweden Krona - SGD = 'SGD', // Singapore Dollar - SHP = 'SHP', // Saint Helena Pound - SLL = 'SLL', // Sierra Leone Leone - SOS = 'SOS', // Somalia Shilling - SPL = 'SPL', // Seborga Luigino - SRD = 'SRD', // Suriname Dollar - STD = 'STD', // São Tomé and Príncipe Dobra - SVC = 'SVC', // El Salvador Colon - SYP = 'SYP', // Syria Pound - SZL = 'SZL', // Swaziland Lilangeni - THB = 'THB', // Thailand Baht - TJS = 'TJS', // Tajikistan Somoni - TMT = 'TMT', // Turkmenistan Manat - TND = 'TND', // Tunisia Dinar - TOP = 'TOP', // Tonga Pa'anga - TRY = 'TRY', // Turkey Lira - TTD = 'TTD', // Trinidad and Tobago Dollar - TVD = 'TVD', // Tuvalu Dollar - TWD = 'TWD', // Taiwan New Dollar - TZS = 'TZS', // Tanzania Shilling - UAH = 'UAH', // Ukraine Hryvnia - UGX = 'UGX', // Uganda Shilling - USD = 'USD', // United States Dollar - UYU = 'UYU', // Uruguay Peso - UZS = 'UZS', // Uzbekistan Som - VEF = 'VEF', // Venezuela Bolivar - VND = 'VND', // Viet Nam Dong - VUV = 'VUV', // Vanuatu Vatu - WST = 'WST', // Samoa Tala - XAF = 'XAF', // Communauté Financière Africaine (BEAC) CFA Franc BEAC - XCD = 'XCD', // East Caribbean Dollar - XDR = 'XDR', // International Monetary Fund (IMF) Special Drawing Rights - XOF = 'XOF', // Communauté Financière Africaine (BCEAO) Franc - XPF = 'XPF', // Comptoirs Français du Pacifique (CFP) Franc - YER = 'YER', // Yemen Rial - ZAR = 'ZAR', // South Africa Rand - ZMW = 'ZMW', // Zambia Kwacha - ZWD = 'ZWD', // Zimbabwe Dollar +export enum Currency { + AED = "AED", // 'UNITED ARAB EMIRATES DIRHAM' + AFN = "AFN", // 'AFGHAN AFGHANI' + ALL = "ALL", // 'ALBANIAN LEK' + AMD = "AMD", // 'ARMENIAN DRAM' + ANG = "ANG", // 'NETHERLANDS ANTILLEAN GUILDER' + AOA = "AOA", // 'ANGOLAN KWANZA' + ARS = "ARS", // 'ARGENTINE PESO' + AUD = "AUD", // 'AUSTRALIAN DOLLAR' + AWG = "AWG", // 'ARUBAN FLORIN' + AZN = "AZN", // 'AZERBAIJANI MANAT' + BAM = "BAM", // 'BOSNIA-HERZEGOVINA CONVERTIBLE MARK' + BBD = "BBD", // 'BAJAN DOLLAR' + BDT = "BDT", // 'BANGLADESHI TAKA' + BGN = "BGN", // 'BULGARIAN LEV' + BHD = "BHD", // 'BAHRAINI DINAR' + BIF = "BIF", // 'BURUNDIAN FRANC' + BMD = "BMD", // 'BERMUDAN DOLLAR' + BND = "BND", // 'BRUNEI DOLLAR' + BOB = "BOB", // 'BOLIVIAN BOLIVIANO' + BRL = "BRL", // 'BRAZILIAN REAL' + BSD = "BSD", // 'BAHAMIAN DOLLAR' + BTN = "BTN", // 'BHUTAN CURRENCY' + BWP = "BWP", // 'BOTSWANAN PULA' + BYN = "BYN", // 'NEW BELARUSIAN RUBLE' + BYR = "BYR", // 'BELARUSIAN RUBLE' + BZD = "BZD", // 'BELIZE DOLLAR' + CAD = "CAD", // 'CANADIAN DOLLAR' + CDF = "CDF", // 'CONGOLESE FRANC' + CHF = "CHF", // 'SWISS FRANC' + CLF = "CLF", // 'CHILEAN UNIT OF ACCOUNT (UF)' + CLP = "CLP", // 'CHILEAN PESO' + CNY = "CNY", // 'CHINESE YUAN' + COP = "COP", // 'COLOMBIAN PESO' + CRC = "CRC", // 'COSTA RICAN COLÓN' + CUC = "CUC", // 'CUBAN CONVERTIBLE PESO' + CUP = "CUP", // 'CUBAN PESO' + CVE = "CVE", // 'CAPE VERDEAN ESCUDO' + CVX = "CVX", // 'CONVEX FINANCE' + CZK = "CZK", // 'CZECH KORUNA' + DJF = "DJF", // 'DJIBOUTIAN FRANC' + DKK = "DKK", // 'DANISH KRONE' + DOP = "DOP", // 'DOMINICAN PESO' + DZD = "DZD", // 'ALGERIAN DINAR' + EGP = "EGP", // 'EGYPTIAN POUND' + ERN = "ERN", // 'ERITREAN NAKFA' + ETB = "ETB", // 'ETHIOPIAN BIRR' + ETC = "ETC", // 'ETHEREUM CLASSIC' + EUR = "EUR", // 'EURO' + FEI = "FEI", // 'FEI USD' + FJD = "FJD", // 'FIJIAN DOLLAR' + FKP = "FKP", // 'FALKLAND ISLANDS POUND' + GBP = "GBP", // 'POUND STERLING' + GEL = "GEL", // 'GEORGIAN LARI' + GHS = "GHS", // 'GHANAIAN CEDI' + GIP = "GIP", // 'GIBRALTAR POUND' + GMD = "GMD", // 'GAMBIAN DALASI' + GNF = "GNF", // 'GUINEAN FRANC' + GTQ = "GTQ", // 'GUATEMALAN QUETZAL' + GYD = "GYD", // 'GUYANAESE DOLLAR' + HKD = "HKD", // 'HONG KONG DOLLAR' + HNL = "HNL", // 'HONDURAN LEMPIRA' + HRK = "HRK", // 'CROATIAN KUNA' + HTG = "HTG", // 'HAITIAN GOURDE' + HUF = "HUF", // 'HUNGARIAN FORINT' + ICP = "ICP", // 'INTERNET COMPUTER' + IDR = "IDR", // 'INDONESIAN RUPIAH' + ILS = "ILS", // 'ISRAELI NEW SHEKEL' + INR = "INR", // 'INDIAN RUPEE' + IQD = "IQD", // 'IRAQI DINAR' + IRR = "IRR", // 'IRANIAN RIAL' + ISK = "ISK", // 'ICELANDIC KRÓNA' + JEP = "JEP", // 'JERSEY POUND' + JMD = "JMD", // 'JAMAICAN DOLLAR' + JOD = "JOD", // 'JORDANIAN DINAR' + JPY = "JPY", // 'JAPANESE YEN' + KES = "KES", // 'KENYAN SHILLING' + KGS = "KGS", // 'KYRGYSTANI SOM' + KHR = "KHR", // 'CAMBODIAN RIEL' + KMF = "KMF", // 'COMORIAN FRANC' + KPW = "KPW", // 'NORTH KOREAN WON' + KRW = "KRW", // 'SOUTH KOREAN WON' + KWD = "KWD", // 'KUWAITI DINAR' + KYD = "KYD", // 'CAYMAN ISLANDS DOLLAR' + KZT = "KZT", // 'KAZAKHSTANI TENGE' + LAK = "LAK", // 'LAOTIAN KIP' + LBP = "LPB", // 'LEBANESE POUND' + LKR = "LKR", // 'SRI LANKAN RUPEE' + LRD = "LRD", // 'LIBERIAN DOLLAR' + LSL = "LSL", // 'LESOTHO LOTI' + LTL = "LTL", // 'LITHUANIAN LITAS' + LVL = "LVL", // 'LATVIAN LATS' + LYD = "LYD", // 'LIBYAN DINAR' + MAD = "MAD", // 'MOROCCAN DIRHAM' + MDL = "MDL", // 'MOLDOVAN LEU' + MGA = "MGA", // 'MALAGASY ARIARY' + MKD = "MKD", // 'MACEDONIAN DENAR' + MMK = "MMK", // 'MYANMAR KYAT' + MNT = "MNT", // 'MONGOLIAN TUGRIK' + MOP = "MOP", // 'MACANESE PATACA' + MRO = "MRO", // 'MAURITANIAN OUGUIYA' + MUR = "MUR", // 'MAURITIAN RUPEE' + MVR = "MVR", // 'MALDIVIAN RUFIYAA' + MWK = "MWK", // 'MALAWIAN KWACHA' + MXN = "MXN", // 'MEXICAN PESO' + MYR = "MYR", // 'MALAYSIAN RINGGIT' + MZN = "MZN", // 'MOZAMBICAN METICAL' + NAD = "NAD", // 'NAMIBIAN DOLLAR' + NGN = "NGN", // 'NIGERIAN NAIRA' + NIO = "NIO", // 'NICARAGUAN CÓRDOBA' + NOK = "NOK", // 'NORWEGIAN KRONE' + NPR = "NPR", // 'NEPALESE RUPEE' + NZD = "NZD", // 'NEW ZEALAND DOLLAR' + OMR = "OMR", // 'OMANI RIAL' + ONE = "ONE", // 'MENLO ONE' + PAB = "PAB", // 'PANAMANIAN BALBOA' + PGK = "PGK", // 'PAPUA NEW GUINEAN KINA' + PHP = "PHP", // 'PHILIPPINE PESO' + PKR = "PKR", // 'PAKISTANI RUPEE' + PLN = "PLN", // 'POLAND ZŁOTY' + PYG = "PYG", // 'PARAGUAYAN GUARANI' + QAR = "QAR", // 'QATARI RIAL' + RON = "RON", // 'ROMANIAN LEU' + RSD = "RSD", // 'SERBIAN DINAR' + RUB = "RUB", // 'RUSSIAN RUBLE' + RWF = "RWF", // 'RWANDAN FRANC' + SAR = "SAR", // 'SAUDI RIYAL' + SBD = "SBD", // 'SOLOMON ISLANDS DOLLAR' + SCR = "SCR", // 'SEYCHELLOIS RUPEE' + SDG = "SDG", // 'SUDANESE POUND' + SEK = "SEK", // 'SWEDISH KRONA' + SGD = "SGD", // 'SINGAPORE DOLLAR' + SHIB = "SHIB", // 'SHIBA INU' + SHP = "SHP", // 'SAINT HELENA POUND' + SLL = "SLL", // 'SIERRA LEONEAN LEONE' + SOS = "SOS", // 'SOMALI SHILLING' + SRD = "SRD", // 'SURINAMESE DOLLAR' + STD = "STD", // 'SÃO TOMÉ AND PRÍNCIPE DOBRA (PRE-2018)' + SVC = "SVC", // 'SALVADORAN COLÓN' + SYP = "SYP", // 'SYRIAN POUND' + SZL = "SZL", // 'SWAZI LILANGENI' + THB = "THB", // 'THAI BAHT' + TJS = "TJS", // 'TAJIKISTANI SOMONI' + TMT = "TMT", // 'TURKMENISTANI MANAT' + TND = "TND", // 'TUNISIAN DINAR' + TOP = "TOP", // "TONGAN PA'ANGA" + TRY = "TRY", // 'TURKISH LIRA' + TTD = "TTD", // 'TRINIDAD & TOBAGO DOLLAR' + TWD = "TWD", // 'NEW TAIWAN DOLLAR' + TZS = "TZS", // 'TANZANIAN SHILLING' + UAH = "UAH", // 'UKRAINIAN HRYVNIA' + UGX = "UGX", // 'UGANDAN SHILLING' + USD = "USD", // 'UNITED STATES DOLLAR' + UYU = "UYU", // 'URUGUAYAN PESO' + UZS = "UZS", // 'UZBEKISTANI SOM' + VND = "VND", // 'VIETNAMESE DONG' + VUV = "VUV", // 'VANUATU VATU' + WST = "WST", // 'SAMOAN TALA' + XAF = "XAF", // 'CENTRAL AFRICAN CFA FRANC' + XCD = "XCD", // 'EAST CARIBBEAN DOLLAR' + XOF = "XOF", // 'WEST AFRICAN CFA FRANC' + XPF = "XPF", // 'CFP FRANC' + YER = "YER", // 'YEMENI RIAL' + ZAR = "ZAR", // 'SOUTH AFRICAN RAND' + ZMW = "ZMW", // 'ZAMBIAN KWACHA' + ZWL = "ZWL", // 'ZIMBABWEAN DOLLAR' } export const CURRENCY_OPTIONS = Object.entries(Currency).map( diff --git a/apps/portal/src/utils/offers/currency/currency-exchange.ts b/apps/portal/src/utils/offers/currency/currency-exchange.ts new file mode 100644 index 00000000..31e02067 --- /dev/null +++ b/apps/portal/src/utils/offers/currency/currency-exchange.ts @@ -0,0 +1,14 @@ +// API from https://github.com/fawazahmed0/currency-api#readme +export const convert = async ( + value: number, + fromCurrency: string, + toCurrency: string, +) => { + fromCurrency = fromCurrency.trim().toLowerCase(); + toCurrency = toCurrency.trim().toLowerCase(); + const url = ['https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies', fromCurrency, toCurrency].join('/'); + + return await fetch(url + '.json') + .then((res) => res.json()) + .then((data) => value * data[toCurrency]); +}; diff --git a/yarn.lock b/yarn.lock index 04c23ef7..c144d5b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4607,7 +4607,7 @@ atob@^2.1.2: attr-accept@^2.2.2: version "2.2.2" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz" integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== autoprefixer@^10.3.7, autoprefixer@^10.4.12, autoprefixer@^10.4.7: @@ -7740,7 +7740,7 @@ file-loader@^6.0.0, file-loader@^6.2.0: file-selector@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + resolved "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz" integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== dependencies: tslib "^2.4.0" @@ -12176,7 +12176,7 @@ react-dom@18.2.0, react-dom@^18.2.0: react-dropzone@^14.2.3: version "14.2.3" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" + resolved "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz" integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== dependencies: attr-accept "^2.2.2" @@ -14163,47 +14163,47 @@ tty-browserify@0.0.0: resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz" integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw== -turbo-darwin-64@1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.5.5.tgz#710d4e7999066bd4f500456f7cd1c30f6e6205ed" - integrity sha512-HvEn6P2B+NXDekq9LRpRgUjcT9/oygLTcK47U0qsAJZXRBSq/2hvD7lx4nAwgY/4W3rhYJeWtHTzbhoN6BXqGQ== +turbo-darwin-64@1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.5.6.tgz#2e0e14343c84dde33b5a09ea5389ee6a9565779c" + integrity sha512-CWdXMwenBS2+QXIR2Czx7JPnAcoMzWx/QwTDcHVxZyeayMHgz8Oq5AHCtfaHDSfV8YhD3xa0GLSk6+cFt+W8BQ== -turbo-darwin-arm64@1.5.5: - version "1.5.5" - resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.5.5.tgz" - integrity sha512-Dmxr09IUy6M0nc7/xWod9galIO2DD500B75sJSkHeT+CCdJOWnlinux0ZPF8CSygNqymwYO8AO2l15/6yxcycg== +turbo-darwin-arm64@1.5.6: + version "1.5.6" + resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.5.6.tgz" + integrity sha512-c/aXgW9JuXT2bJSKf01pdSDQKnrdcdj3WFKmKiVldb9We6eqFzI0fLHBK97k5LM/OesmRMfCMQ2Cv2DU8RqBAA== -turbo-linux-64@1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.5.5.tgz#f31eb117a9b605f5731048c50473bff903850047" - integrity sha512-wd07TZ4zXXWjzZE00FcFMLmkybQQK/NV9ff66vvAV0vdiuacSMBCNLrD6Mm4ncfrUPW/rwFW5kU/7hyuEqqtDw== +turbo-linux-64@1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.5.6.tgz#e7ddaf7a87084dfdd9c6d79efb41084d75439b31" + integrity sha512-y/jNF7SG+XJEwk2GxIqy3g4dj/a0PgZKDGyOkp24qp4KBRcHBl6dI1ZEfNed30EhEqmW4F5Dr7IpeCZoqgbrMg== -turbo-linux-arm64@1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.5.5.tgz#b9ce6912ae6477e829355d6f012500bfef58669d" - integrity sha512-q3q33tuo74R7gicnfvFbnZZvqmlq7Vakcvx0eshifnJw4PR+oMnTCb4w8ElVFx070zsb8DVTibq99y8NJH8T1Q== +turbo-linux-arm64@1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.5.6.tgz#6445f00f84e0f356a6a369ba2d75ede43aaeb796" + integrity sha512-FRcxPtW7eFrbR3QaYBVX8cK7i+2Cerqi6F0t5ulcq+d1OGSdSW3l35rPPyJdwCzCy+k/S9sBcyCV0RtbS6RKCQ== -turbo-windows-64@1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.5.5.tgz#609098de3bc6178f733615d21b06d5c1602637eb" - integrity sha512-lPp9kHonNFfqgovbaW+UAPO5cLmoAN+m3G3FzqcrRPnlzt97vXYsDhDd/4Zy3oAKoAcprtP4CGy0ddisqsKTVw== +turbo-windows-64@1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.5.6.tgz#3638d5297319157031e4dc906dbae53a1db8562c" + integrity sha512-/5KIExY7zbrbeL5fhKGuO85u5VtJ3Ue4kI0MbYCNnTGe7a10yTYkwswgtGihsgEF4AW0Nm0159aHmXZS2Le8IA== -turbo-windows-arm64@1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.5.5.tgz#60522e1e347a54c64bdddb68089fc322ee19c3d7" - integrity sha512-3AfGULKNZiZVrEzsIE+W79ZRW1+f5r4nM4wLlJ1PTBHyRxBZdD6KTH1tijGfy/uTlcV5acYnKHEkDc6Q9PAXGQ== +turbo-windows-arm64@1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.5.6.tgz#9eff9d13721be0b905b0aad07667507380f738fe" + integrity sha512-p+LQN9O39+rZuOAyc6BzyVGvdEKo+v+XmtdeyZsZpfj4xuOLtsEptW1w6cUD439u0YcPknuccGq1MQ0lXQ6Xuw== turbo@latest: - version "1.5.5" - resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.5.5.tgz#9fc3a917c914ffa113c260a4eadb4bc632eee227" - integrity sha512-PVQSDl0STC9WXIyHcYUWs9gXsf8JjQig/FuHfuB8N6+XlgCGB3mPbfMEE6zrChGz2hufH4/guKRX1XJuNL6XTA== + version "1.5.6" + resolved "https://registry.npmjs.org/turbo/-/turbo-1.5.6.tgz" + integrity sha512-xJO/fhiMo4lI62iGR9OgUfJTC9tnnuoMwNC52IfvvBDEPlA8RWGMS8SFpDVG9bNCXvVRrtUTNJXMe6pJWBiOTA== optionalDependencies: - turbo-darwin-64 "1.5.5" - turbo-darwin-arm64 "1.5.5" - turbo-linux-64 "1.5.5" - turbo-linux-arm64 "1.5.5" - turbo-windows-64 "1.5.5" - turbo-windows-arm64 "1.5.5" + turbo-darwin-64 "1.5.6" + turbo-darwin-arm64 "1.5.6" + turbo-linux-64 "1.5.6" + turbo-linux-arm64 "1.5.6" + turbo-windows-64 "1.5.6" + turbo-windows-arm64 "1.5.6" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -14641,7 +14641,7 @@ uuid-browser@^3.1.0: uuid@^3.3.2: version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^8.3.2: From 8b8fffdab188b57382141134d8e2171b6b1e59e4 Mon Sep 17 00:00:00 2001 From: Su Yin <53945359+tnsyn@users.noreply.github.com> Date: Fri, 21 Oct 2022 22:18:12 +0800 Subject: [PATCH 02/19] [resumes][feat] Add mobile filters (#408) --- apps/portal/src/pages/resumes/browse.tsx | 388 +++++++++++++++-------- 1 file changed, 258 insertions(+), 130 deletions(-) diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx index a949ed1c..84b118fd 100644 --- a/apps/portal/src/pages/resumes/browse.tsx +++ b/apps/portal/src/pages/resumes/browse.tsx @@ -1,9 +1,10 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; -import { useEffect, useState } from 'react'; -import { Disclosure } from '@headlessui/react'; -import { MinusIcon, PlusIcon } from '@heroicons/react/20/solid'; +import { Fragment, useEffect, useState } from 'react'; +import { Dialog, Disclosure, Transition } from '@headlessui/react'; +import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid'; +import { XMarkIcon } from '@heroicons/react/24/outline'; import { MagnifyingGlassIcon, NewspaperIcon, @@ -104,6 +105,7 @@ export default function ResumeHomePage() { const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE); const [shortcutSelected, setShortcutSelected] = useState('All'); const [currentPage, setCurrentPage] = useState(1); + const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); const skip = (currentPage - 1) * PAGE_LIMIT; @@ -240,88 +242,51 @@ export default function ResumeHomePage() { Resume Review Portal -
-
- -
-
-
-
-
-

- Shortcuts: -

-
-
-
-
- -
-
-
-
- - -
-
- - {Object.entries(SORT_OPTIONS).map(([key, value]) => ( - - setSortOrder(key) - }> - ))} - -
-
- -
+ + {/* Mobile Filters */} +
+ + + +
+ + +
+ + +
+

+ Shortcuts +

+
-
-
-
-
-
-
-
-

Shortcuts

+
    {SHORTCUTS.map((shortcut) => (
  • @@ -333,18 +298,16 @@ export default function ResumeHomePage() {
  • ))}
-

- Explore these filters: -

+ {filters.map((filter) => ( + className="border-t border-gray-200 px-4 py-6"> {({ open }) => ( <> -

- +

+ {filter.label} @@ -363,12 +326,8 @@ export default function ResumeHomePage() {

- - + +
{filter.options.map((option) => (
))} - +
)} ))} + + +

+ + +
+ +
+
+ +
+ +
+
+

+ Shortcuts: +

+
+
+
    + {SHORTCUTS.map((shortcut) => ( +
  • + onShortcutChange(shortcut)} + /> +
  • + ))} +
+

+ Explore these filters: +

+ {filters.map((filter) => ( + + {({ open }) => ( + <> +

+ + + {filter.label} + + + {open ? ( + + +

+ + + {filter.options.map((option) => ( +
+ + onFilterCheckboxChange( + isChecked, + filter.id, + option.value, + ) + } + /> +
+ ))} +
+
+ + )} +
+ ))} +
+
+
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+ + +
+
+ + {Object.entries(SORT_OPTIONS).map(([key, value]) => ( + setSortOrder(key)}> + ))} + +
+ + +
+
-
- {sessionData === null && - tabsValue !== BROWSE_TABS_VALUES.ALL ? ( - +
+ {sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? ( + + ) : getTabResumes().length === 0 ? ( +
+ - ) : getTabResumes().length === 0 ? ( -
- + ) : ( + <> + +
+ setCurrentPage(page)} /> - {getEmptyDataText(tabsValue, searchValue, userFilters)}
- ) : ( - <> - -
- setCurrentPage(page)} - /> -
- - )} -
+ + )}
From 2729e20351c97940f0bdabf66ecbfdbfd60bbaeb Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Fri, 21 Oct 2022 23:12:55 +0800 Subject: [PATCH 03/19] [offers][refactor] Refactor the sorting to use prisma's WHERE api --- .../components/offers/table/OffersTable.tsx | 2 +- .../src/pages/offers/test/listOffers.tsx | 3 +- .../portal/src/server/router/offers/offers.ts | 166 +++++++++++------- 3 files changed, 103 insertions(+), 68 deletions(-) diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index df0622f6..d636add7 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -9,13 +9,13 @@ import { YOE_CATEGORY, } from '~/components/offers/table/types'; +import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; import { trpc } from '~/utils/trpc'; import OffersRow from './OffersRow'; import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers'; -import { Currency } from '~/utils/offers/currency/CurrencyEnum'; const NUMBER_OF_OFFERS_IN_PAGE = 10; export type OffersTableProps = Readonly<{ diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx index 1e8545d2..143d1826 100644 --- a/apps/portal/src/pages/offers/test/listOffers.tsx +++ b/apps/portal/src/pages/offers/test/listOffers.tsx @@ -6,12 +6,11 @@ function Test() { const data = trpc.useQuery([ 'offers.list', { - currency: "aed", limit: 100, location: 'Singapore, Singapore', offset: 0, sortBy: '+totalCompensation', - yoeCategory: 2, + yoeCategory: 1, }, ]); diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index f4ba3b4f..170ce4c5 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -1,9 +1,14 @@ -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { dashboardOfferDtoMapper, getOffersResponseMapper } from "~/mappers/offers-mappers"; -import { convert } from "~/utils/offers/currency/currency-exchange"; -import { Currency } from "~/utils/offers/currency/CurrencyEnum"; -import { createRouter } from "../context"; +import { z } from 'zod'; +import { TRPCError } from '@trpc/server'; + +import { + dashboardOfferDtoMapper, + getOffersResponseMapper, +} from '~/mappers/offers-mappers'; +import { convert } from '~/utils/offers/currency/currency-exchange'; +import { Currency } from '~/utils/offers/currency/CurrencyEnum'; + +import { createRouter } from '../context'; const yoeCategoryMap: Record = { 0: 'Internship', @@ -84,18 +89,49 @@ export const offersRouter = createRouter().query('list', { where: { AND: [ { - location: input.location, + location: + input.location.length === 0 ? undefined : input.location, }, { offersIntern: { isNot: null, }, }, + { + offersIntern: { + title: + input.title && input.title.length !== 0 + ? input.title + : undefined, + }, + }, + { + offersIntern: { + monthlySalary: { + value: { + gte: input.salaryMin ?? undefined, + lte: input.salaryMax ?? undefined, + }, + }, + }, + }, { offersFullTime: { is: null, }, }, + { + companyId: + input.companyId && input.companyId.length !== 0 + ? input.companyId + : undefined, + }, + { + monthYearReceived: { + gte: input.dateStart ?? undefined, + lte: input.dateEnd ?? undefined, + }, + }, ], }, }) @@ -128,7 +164,8 @@ export const offersRouter = createRouter().query('list', { where: { AND: [ { - location: input.location, + location: + input.location.length === 0 ? undefined : input.location, }, { offersIntern: { @@ -140,6 +177,30 @@ export const offersRouter = createRouter().query('list', { isNot: null, }, }, + { + offersFullTime: { + title: + input.title && input.title.length !== 0 + ? input.title + : undefined, + }, + }, + { + offersFullTime: { + totalCompensation: { + value: { + gte: input.salaryMin ?? undefined, + lte: input.salaryMax ?? undefined, + }, + }, + }, + }, + { + companyId: + input.companyId && input.companyId.length !== 0 + ? input.companyId + : undefined, + }, { profile: { background: { @@ -150,27 +211,52 @@ export const offersRouter = createRouter().query('list', { }, }, }, + { + monthYearReceived: { + gte: input.dateStart ?? undefined, + lte: input.dateEnd ?? undefined, + }, + }, ], }, }); // CONVERTING - const currency = input.currency?.toUpperCase() + const currency = input.currency?.toUpperCase(); if (currency != null && currency in Currency) { data = await Promise.all( data.map(async (offer) => { - if (offer.offersFullTime?.totalCompensation) { - offer.offersFullTime.totalCompensation.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.totalCompensation.value = await convert( + offer.offersFullTime.totalCompensation.value, + offer.offersFullTime.totalCompensation.currency, + currency, + ); offer.offersFullTime.totalCompensation.currency = currency; - offer.offersFullTime.baseSalary.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.baseSalary.value = await convert( + offer.offersFullTime.totalCompensation.value, + offer.offersFullTime.totalCompensation.currency, + currency, + ); offer.offersFullTime.baseSalary.currency = currency; - offer.offersFullTime.stocks.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.stocks.value = await convert( + offer.offersFullTime.totalCompensation.value, + offer.offersFullTime.totalCompensation.currency, + currency, + ); offer.offersFullTime.stocks.currency = currency; - offer.offersFullTime.bonus.value = await convert(offer.offersFullTime.totalCompensation.value, offer.offersFullTime.totalCompensation.currency, currency); + offer.offersFullTime.bonus.value = await convert( + offer.offersFullTime.totalCompensation.value, + offer.offersFullTime.totalCompensation.currency, + currency, + ); offer.offersFullTime.bonus.currency = currency; } else if (offer.offersIntern?.monthlySalary) { - offer.offersIntern.monthlySalary.value = await convert(offer.offersIntern.monthlySalary.value, offer.offersIntern.monthlySalary.currency, currency); + offer.offersIntern.monthlySalary.value = await convert( + offer.offersIntern.monthlySalary.value, + offer.offersIntern.monthlySalary.currency, + currency, + ); offer.offersIntern.monthlySalary.currency = currency; } else { throw new TRPCError({ @@ -184,56 +270,6 @@ export const offersRouter = createRouter().query('list', { ); } - // FILTERING - data = data.filter((offer) => { - let validRecord = true; - - if (input.companyId && input.companyId.length !== 0) { - validRecord = validRecord && offer.company.id === input.companyId; - } - - if (input.title && input.title.length !== 0) { - validRecord = - validRecord && - (offer.offersFullTime?.title === input.title || - offer.offersIntern?.title === input.title); - } - - if ( - input.dateStart && - input.dateEnd && - input.dateStart.getTime() <= input.dateEnd.getTime() - ) { - validRecord = - validRecord && - offer.monthYearReceived.getTime() >= input.dateStart.getTime() && - offer.monthYearReceived.getTime() <= input.dateEnd.getTime(); - } - - if (input.salaryMin != null || input.salaryMax != null) { - const salary = offer.offersFullTime?.totalCompensation.value - ? offer.offersFullTime?.totalCompensation.value - : offer.offersIntern?.monthlySalary.value; - - if (salary == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total Compensation or Salary not found', - }); - } - - if (input.salaryMin != null) { - validRecord = validRecord && salary >= input.salaryMin; - } - - if (input.salaryMax != null) { - validRecord = validRecord && salary <= input.salaryMax; - } - } - - return validRecord; - }); - // SORTING data = data.sort((offer1, offer2) => { const defaultReturn = From dac178e712d91f243ae70a2b268e5bf07d384a09 Mon Sep 17 00:00:00 2001 From: Terence <45381509+Vielheim@users.noreply.github.com> Date: Fri, 21 Oct 2022 23:14:57 +0800 Subject: [PATCH 04/19] [resumes][fix] fix reply comments (#407) Co-authored-by: Terence Ho <> --- .../resumes/comments/ResumeCommentListItem.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx index 5561a49e..07d951bb 100644 --- a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx +++ b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx @@ -96,15 +96,17 @@ export default function ResumeCommentListItem({
- {/* Action buttons; only present when not editing/replying */} - {isCommentOwner && !isEditingComment && !isReplyingComment && ( + {/* Action buttons; only present for authenticated user when not editing/replying */} + {userId && !isEditingComment && !isReplyingComment && ( <> - + {isCommentOwner && ( + + )} {!comment.parentId && (
-
+
From b345ae0c8fdd66212ad82232166a8017835655c5 Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Sat, 22 Oct 2022 00:33:49 +0800 Subject: [PATCH 07/19] [offers][refactor] Refactor the sorting to use prisma's ORDERBY api --- .../src/pages/offers/test/listOffers.tsx | 4 +- .../portal/src/server/router/offers/offers.ts | 308 +++++++++++------- 2 files changed, 190 insertions(+), 122 deletions(-) diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx index 143d1826..911af3ac 100644 --- a/apps/portal/src/pages/offers/test/listOffers.tsx +++ b/apps/portal/src/pages/offers/test/listOffers.tsx @@ -9,8 +9,8 @@ function Test() { limit: 100, location: 'Singapore, Singapore', offset: 0, - sortBy: '+totalCompensation', - yoeCategory: 1, + sortBy: '-monthYearReceived', + yoeCategory: 0, }, ]); diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 170ce4c5..7e511164 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -10,6 +10,27 @@ import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import { createRouter } from '../context'; +const getOrder = (prefix: string) => { + if (prefix === '+') { + return 'asc'; + } + return 'desc'; +}; + +const sortingKeysMap = { + monthYearReceived: 'monthYearReceived', + totalCompensation: 'totalCompensation', + totalYoe: 'totalYoe', +}; + +const createSortByValidationRegex = () => { + const startsWithPlusOrMinusOnly = '^[+-]{1}'; + const sortingKeysRegex = Object.entries(sortingKeysMap) + .map((entry) => entry[0]) + .join('|'); + return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')'); +}; + const yoeCategoryMap: Record = { 0: 'Internship', 1: 'Fresh Grad', @@ -27,16 +48,6 @@ const getYoeRange = (yoeCategory: number) => { : null; // Internship }; -const ascOrder = '+'; -const descOrder = '-'; -const sortingKeys = ['monthYearReceived', 'totalCompensation', 'totalYoe']; - -const createSortByValidationRegex = () => { - const startsWithPlusOrMinusOnly = '^[+-]{1}'; - const sortingKeysRegex = sortingKeys.join('|'); - return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')'); -}; - export const offersRouter = createRouter().query('list', { input: z.object({ companyId: z.string().nullish(), @@ -59,6 +70,15 @@ export const offersRouter = createRouter().query('list', { const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe; const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe; + // Const orderBy = getSortingOrderAndKey(input.sortBy, input.yoeCategory); + + if (!input.sortBy) { + input.sortBy = '-' + sortingKeysMap.monthYearReceived; + } + + const order = getOrder(input.sortBy.charAt(0)); + const sortingKey = input.sortBy.substring(1); + let data = !yoeRange ? await ctx.prisma.offersOffer.findMany({ // Internship @@ -83,9 +103,28 @@ export const offersRouter = createRouter().query('list', { }, }, }, - orderBy: { - monthYearReceived: 'desc', - }, + orderBy: + sortingKey === sortingKeysMap.monthYearReceived + ? { + monthYearReceived: order, + } + : sortingKey === sortingKeysMap.totalCompensation + ? { + offersIntern: { + monthlySalary: { + value: order, + }, + }, + } + : sortingKey === sortingKeysMap.totalYoe + ? { + profile: { + background: { + totalYoe: order, + }, + }, + } + : undefined, where: { AND: [ { @@ -126,6 +165,16 @@ export const offersRouter = createRouter().query('list', { ? input.companyId : undefined, }, + { + profile: { + background: { + totalYoe: { + gte: yoeMin, + lte: yoeMax, + }, + }, + }, + }, { monthYearReceived: { gte: input.dateStart ?? undefined, @@ -158,9 +207,28 @@ export const offersRouter = createRouter().query('list', { }, }, }, - orderBy: { - monthYearReceived: 'desc', - }, + orderBy: + sortingKey === sortingKeysMap.monthYearReceived + ? { + monthYearReceived: order, + } + : sortingKey === sortingKeysMap.totalCompensation + ? { + offersFullTime: { + totalCompensation: { + value: order, + }, + }, + } + : sortingKey === sortingKeysMap.totalYoe + ? { + profile: { + background: { + totalYoe: order, + }, + }, + } + : undefined, where: { AND: [ { @@ -271,110 +339,110 @@ export const offersRouter = createRouter().query('list', { } // SORTING - data = data.sort((offer1, offer2) => { - const defaultReturn = - offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime(); - - if (!input.sortBy) { - return defaultReturn; - } - - const order = input.sortBy.charAt(0); - const sortingKey = input.sortBy.substring(1); - - if (order === ascOrder) { - return (() => { - if (sortingKey === 'monthYearReceived') { - return ( - offer1.monthYearReceived.getTime() - - offer2.monthYearReceived.getTime() - ); - } - - if (sortingKey === 'totalCompensation') { - const salary1 = offer1.offersFullTime?.totalCompensation.value - ? offer1.offersFullTime?.totalCompensation.value - : offer1.offersIntern?.monthlySalary.value; - - const salary2 = offer2.offersFullTime?.totalCompensation.value - ? offer2.offersFullTime?.totalCompensation.value - : offer2.offersIntern?.monthlySalary.value; - - if (salary1 == null || salary2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total Compensation or Salary not found', - }); - } - - return salary1 - salary2; - } - - if (sortingKey === 'totalYoe') { - const yoe1 = offer1.profile.background?.totalYoe; - const yoe2 = offer2.profile.background?.totalYoe; - - if (yoe1 == null || yoe2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total years of experience not found', - }); - } - - return yoe1 - yoe2; - } - - return defaultReturn; - })(); - } - - if (order === descOrder) { - return (() => { - if (sortingKey === 'monthYearReceived') { - return ( - offer2.monthYearReceived.getTime() - - offer1.monthYearReceived.getTime() - ); - } - - if (sortingKey === 'totalCompensation') { - const salary1 = offer1.offersFullTime?.totalCompensation.value - ? offer1.offersFullTime?.totalCompensation.value - : offer1.offersIntern?.monthlySalary.value; - - const salary2 = offer2.offersFullTime?.totalCompensation.value - ? offer2.offersFullTime?.totalCompensation.value - : offer2.offersIntern?.monthlySalary.value; - - if (salary1 == null || salary2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total Compensation or Salary not found', - }); - } - - return salary2 - salary1; - } - - if (sortingKey === 'totalYoe') { - const yoe1 = offer1.profile.background?.totalYoe; - const yoe2 = offer2.profile.background?.totalYoe; - - if (yoe1 == null || yoe2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total years of experience not found', - }); - } - - return yoe2 - yoe1; - } - - return defaultReturn; - })(); - } - return defaultReturn; - }); + // data = data.sort((offer1, offer2) => { + // const defaultReturn = + // offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime(); + + // if (!input.sortBy) { + // return defaultReturn; + // } + + // const order = input.sortBy.charAt(0); + // const sortingKey = input.sortBy.substring(1); + + // if (order === ascOrder) { + // return (() => { + // if (sortingKey === 'monthYearReceived') { + // return ( + // offer1.monthYearReceived.getTime() - + // offer2.monthYearReceived.getTime() + // ); + // } + + // if (sortingKey === 'totalCompensation') { + // const salary1 = offer1.offersFullTime?.totalCompensation.value + // ? offer1.offersFullTime?.totalCompensation.value + // : offer1.offersIntern?.monthlySalary.value; + + // const salary2 = offer2.offersFullTime?.totalCompensation.value + // ? offer2.offersFullTime?.totalCompensation.value + // : offer2.offersIntern?.monthlySalary.value; + + // if (salary1 == null || salary2 == null) { + // throw new TRPCError({ + // code: 'NOT_FOUND', + // message: 'Total Compensation or Salary not found', + // }); + // } + + // return salary1 - salary2; + // } + + // if (sortingKey === 'totalYoe') { + // const yoe1 = offer1.profile.background?.totalYoe; + // const yoe2 = offer2.profile.background?.totalYoe; + + // if (yoe1 == null || yoe2 == null) { + // throw new TRPCError({ + // code: 'NOT_FOUND', + // message: 'Total years of experience not found', + // }); + // } + + // return yoe1 - yoe2; + // } + + // return defaultReturn; + // })(); + // } + + // if (order === descOrder) { + // return (() => { + // if (sortingKey === 'monthYearReceived') { + // return ( + // offer2.monthYearReceived.getTime() - + // offer1.monthYearReceived.getTime() + // ); + // } + + // if (sortingKey === 'totalCompensation') { + // const salary1 = offer1.offersFullTime?.totalCompensation.value + // ? offer1.offersFullTime?.totalCompensation.value + // : offer1.offersIntern?.monthlySalary.value; + + // const salary2 = offer2.offersFullTime?.totalCompensation.value + // ? offer2.offersFullTime?.totalCompensation.value + // : offer2.offersIntern?.monthlySalary.value; + + // if (salary1 == null || salary2 == null) { + // throw new TRPCError({ + // code: 'NOT_FOUND', + // message: 'Total Compensation or Salary not found', + // }); + // } + + // return salary2 - salary1; + // } + + // if (sortingKey === 'totalYoe') { + // const yoe1 = offer1.profile.background?.totalYoe; + // const yoe2 = offer2.profile.background?.totalYoe; + + // if (yoe1 == null || yoe2 == null) { + // throw new TRPCError({ + // code: 'NOT_FOUND', + // message: 'Total years of experience not found', + // }); + // } + + // return yoe2 - yoe1; + // } + + // return defaultReturn; + // })(); + // } + // return defaultReturn; + // }); const startRecordIndex: number = input.limit * input.offset; const endRecordIndex: number = From 2c7f349043414bc01fce09f99d753527506dd050 Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Sat, 22 Oct 2022 07:48:07 +0800 Subject: [PATCH 08/19] [offers][chore] Add a relative base value to the currency model in schema --- .../migrations/20221021231817_/migration.sql | 12 + .../migration.sql | 3 + apps/portal/prisma/schema.prisma | 14 +- .../src/pages/offers/test/createProfile.tsx | 45 +- .../router/offers/offers-profile-router.ts | 609 +++++++++++------- .../portal/src/server/router/offers/offers.ts | 122 +--- .../offers/currency/currency-exchange.ts | 6 +- .../src/utils/offers/currency/index.tsx | 4 + apps/portal/src/utils/offers/zodRegex.ts | 8 + 9 files changed, 433 insertions(+), 390 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221021231817_/migration.sql create mode 100644 apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql create mode 100644 apps/portal/src/utils/offers/zodRegex.ts diff --git a/apps/portal/prisma/migrations/20221021231817_/migration.sql b/apps/portal/prisma/migrations/20221021231817_/migration.sql new file mode 100644 index 00000000..3820338d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021231817_/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `baseValue` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OffersCurrency" ADD COLUMN "baseCurrency" TEXT NOT NULL DEFAULT 'USD', +ADD COLUMN "baseValue" INTEGER NOT NULL, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql new file mode 100644 index 00000000..089e963d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "OffersCurrency" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "baseValue" SET DATA TYPE DOUBLE PRECISION; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 7f968286..67a4f6d3 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -205,9 +205,9 @@ model OffersBackground { totalYoe Int specificYoes OffersSpecificYoe[] - experiences OffersExperience[] // For extensibility in the future + experiences OffersExperience[] - educations OffersEducation[] // For extensibility in the future + educations OffersEducation[] profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) offersProfileId String @unique @@ -251,10 +251,16 @@ model OffersExperience { } model OffersCurrency { - id String @id @default(cuid()) - value Int + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + value Float currency String + baseValue Float + baseCurrency String @default("USD") + // Experience OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx index ab240dbf..7928ab12 100644 --- a/apps/portal/src/pages/offers/test/createProfile.tsx +++ b/apps/portal/src/pages/offers/test/createProfile.tsx @@ -40,7 +40,7 @@ function Test() { deleteCommentMutation.mutate({ id: 'cl97fprun001j7iyg6ev9x983', profileId: 'cl96stky5002ew32gx2kale2x', - token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', userId: 'cl97dl51k001e7iygd5v5gt58', }); }; @@ -84,7 +84,7 @@ function Test() { const handleLink = () => { addToUserProfileMutation.mutate({ profileId: 'cl9efyn9p004ww3u42mjgl1vn', - token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', userId: 'cl9ehvpng0000w3ec2mpx0bdd', }); }; @@ -103,11 +103,10 @@ function Test() { ], experiences: [ { - companyId: 'cl9h0bqu50000txxwkhmshhxz', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', durationInMonths: 24, jobType: 'FULLTIME', level: 'Junior', - // "monthlySalary": undefined, specialization: 'Front End', title: 'Software Engineer', totalCompensation: { @@ -132,7 +131,7 @@ function Test() { { comments: 'I am a Raffles Institution almumni', // Comments: '', - companyId: 'cl9h0bqu50000txxwkhmshhxz', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', jobType: 'FULLTIME', location: 'Singapore, Singapore', monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), @@ -161,7 +160,7 @@ function Test() { }, { comments: '', - companyId: 'cl9h0bqu50000txxwkhmshhxz', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', jobType: 'FULLTIME', location: 'Singapore, Singapore', monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), @@ -192,14 +191,14 @@ function Test() { }); }; - const profileId = 'cl9i68fv60000tthj8t3zkox0'; // Remember to change this filed after testing deleting + const profileId = 'cl9j50xzk008vutfqg6mta2ey'; // Remember to change this filed after testing deleting const data = trpc.useQuery( [ `offers.profile.listOne`, { profileId, token: - 'd14666ff76e267c9e99445844b41410e83874936d0c07e664db73ff0ea76919e', + '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', }, ], { @@ -223,7 +222,7 @@ function Test() { const handleDelete = (id: string) => { deleteMutation.mutate({ profileId: id, - token: 'e7effd2a40adba2deb1ddea4fb9f1e6c3c98ab0a85a88ed1567fc2a107fdb445', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', }); }; @@ -257,15 +256,15 @@ function Test() { createdAt: new Date('2022-10-12T16:19:05.196Z'), description: 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - id: 'cl9h0bqug0003txxwgkac0x40', + id: 'cl9j4yawz0003utlp1uaa1t8o', logoUrl: 'https://logo.clearbit.com/meta.com', name: 'Meta', slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl9h0bqug0003txxwgkac0x40', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', durationInMonths: 24, - // Id: 'cl9h0bqug0003txxwgkac0x40', + // Id: 'cl9j4yawz0003utlp1uaa1t8o', jobType: 'FULLTIME', level: 'Junior', monthlySalary: null, @@ -309,13 +308,13 @@ function Test() { createdAt: new Date('2022-10-12T16:19:05.196Z'), description: 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - id: 'cl9h0bqug0003txxwgkac0x40', + id: 'cl9j4yawz0003utlp1uaa1t8o', logoUrl: 'https://logo.clearbit.com/meta.com', name: 'Meta', slug: 'meta', updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: 'cl9h0bqug0003txxwgkac0x40', + companyId: 'cl9j4yawz0003utlp1uaa1t8o', id: 'cl9i68fve000ntthj5h9yvqnh', jobType: 'FULLTIME', location: 'Singapore, Singapore', @@ -362,13 +361,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl9i68fvf000ytthj0ltsqt1d', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -415,13 +414,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl96stky9003bw32gc3l955vr', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -468,13 +467,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl976wf28000t7iyga4noyz7s', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -521,13 +520,13 @@ function Test() { // createdAt: new Date('2022-10-12T16:19:05.196Z'), // description: // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', - // id: 'cl9h0bqug0003txxwgkac0x40', + // id: 'cl9j4yawz0003utlp1uaa1t8o', // logoUrl: 'https://logo.clearbit.com/meta.com', // name: 'Meta', // slug: 'meta', // updatedAt: new Date('2022-10-12T16:19:05.196Z'), // }, - // companyId: 'cl9h0bqug0003txxwgkac0x40', + // companyId: 'cl9j4yawz0003utlp1uaa1t8o', // id: 'cl96tbb3o0051w32gjrpaiiit', // jobType: 'FULLTIME', // location: 'Singapore, Singapore', @@ -570,7 +569,7 @@ function Test() { // }, ], // ProfileName: 'ailing bryann stuart ziqing', - token: 'd3509cb890f0bae0a785afdd6c1c074a140706ab1d155ed338ec22dcca5c92f1', + token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117', userId: null, }); }; diff --git a/apps/portal/src/server/router/offers/offers-profile-router.ts b/apps/portal/src/server/router/offers/offers-profile-router.ts index b8552018..8ce57731 100644 --- a/apps/portal/src/server/router/offers/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers/offers-profile-router.ts @@ -1,5 +1,6 @@ import crypto, { randomUUID } from 'crypto'; import { z } from 'zod'; +import { JobType } from '@prisma/client'; import * as trpc from '@trpc/server'; import { @@ -7,6 +8,9 @@ import { createOfferProfileResponseMapper, profileDtoMapper, } from '~/mappers/offers-mappers'; +import { baseCurrencyString } from '~/utils/offers/currency'; +import { convert } from '~/utils/offers/currency/currency-exchange'; +import { createValidationRegex } from '~/utils/offers/zodRegex'; import { createRouter } from '../context'; @@ -31,7 +35,7 @@ const offer = z.object({ company: company.nullish(), companyId: z.string(), id: z.string().optional(), - jobType: z.string(), + jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)), location: z.string(), monthYearReceived: z.date(), negotiationStrategy: z.string(), @@ -73,7 +77,10 @@ const experience = z.object({ companyId: z.string().nullish(), durationInMonths: z.number().nullish(), id: z.string().optional(), - jobType: z.string().nullish(), + jobType: z + .string() + .regex(createValidationRegex(Object.keys(JobType), null)) + .nullish(), level: z.string().nullish(), location: z.string().nullish(), monthlySalary: valuation.nullish(), @@ -94,15 +101,6 @@ const education = z.object({ type: z.string().nullish(), }); -// Const reply = z.object({ -// createdAt: z.date().nullish(), -// id: z.string().optional(), -// messages: z.string().nullish(), -// profileId: z.string().nullish(), -// replyingToId: z.string().nullish(), -// userId: z.string().nullish(), -// }); - export const offersProfileRouter = createRouter() .query('listOne', { input: z.object({ @@ -282,11 +280,11 @@ export const offersProfileRouter = createRouter() })), }, experiences: { - create: input.background.experiences.map((x) => { + create: input.background.experiences.map(async (x) => { if ( - x.jobType === 'FULLTIME' && - x.totalCompensation?.currency !== undefined && - x.totalCompensation.value !== undefined + x.jobType === JobType.FULLTIME && + x.totalCompensation?.currency != null && + x.totalCompensation?.value != null ) { if (x.companyId) { return { @@ -302,8 +300,14 @@ export const offersProfileRouter = createRouter() title: x.title, totalCompensation: { create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, }, }, }; @@ -312,20 +316,27 @@ export const offersProfileRouter = createRouter() durationInMonths: x.durationInMonths, jobType: x.jobType, level: x.level, + location: x.location, specialization: x.specialization, title: x.title, totalCompensation: { create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, }, }, }; } if ( - x.jobType === 'INTERN' && - x.monthlySalary?.currency !== undefined && - x.monthlySalary.value !== undefined + x.jobType === JobType.INTERN && + x.monthlySalary?.currency != null && + x.monthlySalary?.value != null ) { if (x.companyId) { return { @@ -338,8 +349,14 @@ export const offersProfileRouter = createRouter() jobType: x.jobType, monthlySalary: { create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, }, }, specialization: x.specialization, @@ -351,8 +368,14 @@ export const offersProfileRouter = createRouter() jobType: x.jobType, monthlySalary: { create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, }, }, specialization: x.specialization, @@ -379,107 +402,141 @@ export const offersProfileRouter = createRouter() }, editToken: token, offers: { - create: input.offers.map((x) => { - if ( - x.jobType === 'INTERN' && - x.offersIntern && - x.offersIntern.internshipCycle && - x.offersIntern.monthlySalary?.currency && - x.offersIntern.monthlySalary.value && - x.offersIntern.startYear - ) { - return { - comments: x.comments, - company: { - connect: { - id: x.companyId, + create: await Promise.all( + input.offers.map(async (x) => { + if ( + x.jobType === JobType.INTERN && + x.offersIntern && + x.offersIntern.internshipCycle != null && + x.offersIntern.monthlySalary?.currency != null && + x.offersIntern.monthlySalary?.value != null && + x.offersIntern.startYear != null + ) { + return { + comments: x.comments, + company: { + connect: { + id: x.companyId, + }, }, - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy, - offersIntern: { - create: { - internshipCycle: x.offersIntern.internshipCycle, - monthlySalary: { - create: { - currency: x.offersIntern.monthlySalary?.currency, - value: x.offersIntern.monthlySalary?.value, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + offersIntern: { + create: { + internshipCycle: x.offersIntern.internshipCycle, + monthlySalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersIntern.monthlySalary.value, + x.offersIntern.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.offersIntern.monthlySalary.currency, + value: x.offersIntern.monthlySalary.value, + }, }, + specialization: x.offersIntern.specialization, + startYear: x.offersIntern.startYear, + title: x.offersIntern.title, }, - specialization: x.offersIntern.specialization, - startYear: x.offersIntern.startYear, - title: x.offersIntern.title, }, - }, - }; - } - if ( - x.jobType === 'FULLTIME' && - x.offersFullTime && - x.offersFullTime.baseSalary?.currency && - x.offersFullTime.baseSalary?.value && - x.offersFullTime.bonus?.currency && - x.offersFullTime.bonus?.value && - x.offersFullTime.stocks?.currency && - x.offersFullTime.stocks?.value && - x.offersFullTime.totalCompensation?.currency && - x.offersFullTime.totalCompensation?.value && - x.offersFullTime.level - ) { - return { - comments: x.comments, - company: { - connect: { - id: x.companyId, + }; + } + if ( + x.jobType === JobType.FULLTIME && + x.offersFullTime && + x.offersFullTime.baseSalary?.currency != null && + x.offersFullTime.baseSalary?.value != null && + x.offersFullTime.bonus?.currency != null && + x.offersFullTime.bonus?.value != null && + x.offersFullTime.stocks?.currency != null && + x.offersFullTime.stocks?.value != null && + x.offersFullTime.totalCompensation?.currency != null && + x.offersFullTime.totalCompensation?.value != null && + x.offersFullTime.level != null && + x.offersFullTime.title != null && + x.offersFullTime.specialization != null + ) { + return { + comments: x.comments, + company: { + connect: { + id: x.companyId, + }, }, - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy, - offersFullTime: { - create: { - baseSalary: { - create: { - currency: x.offersFullTime.baseSalary?.currency, - value: x.offersFullTime.baseSalary?.value, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + offersFullTime: { + create: { + baseSalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.baseSalary.value, + x.offersFullTime.baseSalary.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.baseSalary.currency, + value: x.offersFullTime.baseSalary.value, + }, }, - }, - bonus: { - create: { - currency: x.offersFullTime.bonus?.currency, - value: x.offersFullTime.bonus?.value, + bonus: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.bonus.value, + x.offersFullTime.bonus.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.bonus.currency, + value: x.offersFullTime.bonus.value, + }, }, - }, - level: x.offersFullTime.level, - specialization: x.offersFullTime.specialization, - stocks: { - create: { - currency: x.offersFullTime.stocks?.currency, - value: x.offersFullTime.stocks?.value, + level: x.offersFullTime.level, + specialization: x.offersFullTime.specialization, + stocks: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.stocks.value, + x.offersFullTime.stocks.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.stocks.currency, + value: x.offersFullTime.stocks.value, + }, }, - }, - title: x.offersFullTime.title, - totalCompensation: { - create: { - currency: - x.offersFullTime.totalCompensation?.currency, - value: x.offersFullTime.totalCompensation?.value, + title: x.offersFullTime.title, + totalCompensation: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.totalCompensation.value, + x.offersFullTime.totalCompensation.currency, + baseCurrencyString, + ), + currency: + x.offersFullTime.totalCompensation.currency, + value: x.offersFullTime.totalCompensation.value, + }, }, }, }, - }, - }; - } + }; + } - // Throw error - throw new trpc.TRPCError({ - code: 'BAD_REQUEST', - message: 'Missing fields.', - }); - }), + // Throw error + throw new trpc.TRPCError({ + code: 'BAD_REQUEST', + message: 'Missing fields.', + }); + }), + ), }, profileName: randomUUID().substring(0, 10), }, @@ -510,7 +567,7 @@ export const offersProfileRouter = createRouter() return deletedProfile.id; } - // TODO: Throw 401 + throw new trpc.TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid token.', @@ -535,7 +592,6 @@ export const offersProfileRouter = createRouter() totalYoe: z.number(), }), createdAt: z.string().optional(), - // Discussion: z.array(reply), id: z.string(), isEditable: z.boolean().nullish(), offers: z.array(offer), @@ -573,19 +629,21 @@ export const offersProfileRouter = createRouter() }); // Delete educations - const educationsId = (await ctx.prisma.offersEducation.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const educationsId = ( + await ctx.prisma.offersEducation.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of educationsId) { if (!input.background.educations.map((x) => x.id).includes(id)) { await ctx.prisma.offersEducation.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -626,19 +684,21 @@ export const offersProfileRouter = createRouter() } // Delete experiences - const experiencesId = (await ctx.prisma.offersExperience.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const experiencesId = ( + await ctx.prisma.offersExperience.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of experiencesId) { if (!input.background.experiences.map((x) => x.id).includes(id)) { await ctx.prisma.offersExperience.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -660,6 +720,12 @@ export const offersProfileRouter = createRouter() if (exp.monthlySalary) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), currency: exp.monthlySalary.currency, value: exp.monthlySalary.value, }, @@ -672,6 +738,12 @@ export const offersProfileRouter = createRouter() if (exp.totalCompensation) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), currency: exp.totalCompensation.currency, value: exp.totalCompensation.value, }, @@ -683,9 +755,9 @@ export const offersProfileRouter = createRouter() } else if (!exp.id) { // Create new experience if ( - exp.jobType === 'FULLTIME' && - exp.totalCompensation?.currency !== undefined && - exp.totalCompensation.value !== undefined + exp.jobType === JobType.FULLTIME && + exp.totalCompensation?.currency != null && + exp.totalCompensation?.value != null ) { if (exp.companyId) { await ctx.prisma.offersBackground.update({ @@ -700,12 +772,19 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, + location: exp.location, specialization: exp.specialization, title: exp.title, totalCompensation: { create: { - currency: exp.totalCompensation?.currency, - value: exp.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, }, }, }, @@ -723,12 +802,19 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, + location: exp.location, specialization: exp.specialization, title: exp.title, totalCompensation: { create: { - currency: exp.totalCompensation?.currency, - value: exp.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, }, }, }, @@ -740,9 +826,9 @@ export const offersProfileRouter = createRouter() }); } } else if ( - exp.jobType === 'INTERN' && - exp.monthlySalary?.currency !== undefined && - exp.monthlySalary.value !== undefined + exp.jobType === JobType.INTERN && + exp.monthlySalary?.currency != null && + exp.monthlySalary?.value != null ) { if (exp.companyId) { await ctx.prisma.offersBackground.update({ @@ -756,10 +842,17 @@ export const offersProfileRouter = createRouter() }, durationInMonths: exp.durationInMonths, jobType: exp.jobType, + location: exp.location, monthlySalary: { create: { - currency: exp.monthlySalary?.currency, - value: exp.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, }, }, specialization: exp.specialization, @@ -778,10 +871,17 @@ export const offersProfileRouter = createRouter() create: { durationInMonths: exp.durationInMonths, jobType: exp.jobType, + location: exp.location, monthlySalary: { create: { - currency: exp.monthlySalary?.currency, - value: exp.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, }, }, specialization: exp.specialization, @@ -799,19 +899,21 @@ export const offersProfileRouter = createRouter() } // Delete specific yoes - const yoesId = (await ctx.prisma.offersSpecificYoe.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const yoesId = ( + await ctx.prisma.offersSpecificYoe.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of yoesId) { if (!input.background.specificYoes.map((x) => x.id).includes(id)) { await ctx.prisma.offersSpecificYoe.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -845,19 +947,21 @@ export const offersProfileRouter = createRouter() } // Delete specific offers - const offers = (await ctx.prisma.offersOffer.findMany({ - where: { - profileId: input.id - } - })).map((x) => x.id) + const offers = ( + await ctx.prisma.offersOffer.findMany({ + where: { + profileId: input.id, + }, + }) + ).map((x) => x.id); for (const id of offers) { if (!input.offers.map((x) => x.id).includes(id)) { await ctx.prisma.offersOffer.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -869,6 +973,10 @@ export const offersProfileRouter = createRouter() data: { comments: offerToUpdate.comments, companyId: offerToUpdate.companyId, + jobType: + offerToUpdate.jobType === JobType.FULLTIME + ? JobType.FULLTIME + : JobType.INTERN, location: offerToUpdate.location, monthYearReceived: offerToUpdate.monthYearReceived, negotiationStrategy: offerToUpdate.negotiationStrategy, @@ -878,21 +986,7 @@ export const offersProfileRouter = createRouter() }, }); - if ( - offerToUpdate.jobType === 'INTERN' || - offerToUpdate.jobType === 'FULLTIME' - ) { - await ctx.prisma.offersOffer.update({ - data: { - jobType: offerToUpdate.jobType, - }, - where: { - id: offerToUpdate.id, - }, - }); - } - - if (offerToUpdate.offersIntern?.monthlySalary) { + if (offerToUpdate.offersIntern?.monthlySalary != null) { await ctx.prisma.offersIntern.update({ data: { internshipCycle: @@ -907,6 +1001,12 @@ export const offersProfileRouter = createRouter() }); await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersIntern.monthlySalary.value, + offerToUpdate.offersIntern.monthlySalary.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersIntern.monthlySalary.currency, value: offerToUpdate.offersIntern.monthlySalary.value, }, @@ -916,7 +1016,7 @@ export const offersProfileRouter = createRouter() }); } - if (offerToUpdate.offersFullTime?.totalCompensation) { + if (offerToUpdate.offersFullTime?.totalCompensation != null) { await ctx.prisma.offersFullTime.update({ data: { level: offerToUpdate.offersFullTime.level ?? undefined, @@ -927,9 +1027,15 @@ export const offersProfileRouter = createRouter() id: offerToUpdate.offersFullTime.id, }, }); - if (offerToUpdate.offersFullTime.baseSalary) { + if (offerToUpdate.offersFullTime.baseSalary != null) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.baseSalary.value, + offerToUpdate.offersFullTime.baseSalary.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.baseSalary.currency, value: offerToUpdate.offersFullTime.baseSalary.value, }, @@ -941,6 +1047,12 @@ export const offersProfileRouter = createRouter() if (offerToUpdate.offersFullTime.bonus) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.bonus.value, + offerToUpdate.offersFullTime.bonus.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.bonus.currency, value: offerToUpdate.offersFullTime.bonus.value, }, @@ -952,6 +1064,12 @@ export const offersProfileRouter = createRouter() if (offerToUpdate.offersFullTime.stocks) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.stocks.value, + offerToUpdate.offersFullTime.stocks.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.stocks.currency, value: offerToUpdate.offersFullTime.stocks.value, }, @@ -962,6 +1080,12 @@ export const offersProfileRouter = createRouter() } await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.totalCompensation.value, + offerToUpdate.offersFullTime.totalCompensation.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.totalCompensation.currency, value: offerToUpdate.offersFullTime.totalCompensation.value, @@ -974,12 +1098,12 @@ export const offersProfileRouter = createRouter() } else { // Create new offer if ( - offerToUpdate.jobType === 'INTERN' && + offerToUpdate.jobType === JobType.INTERN && offerToUpdate.offersIntern && - offerToUpdate.offersIntern.internshipCycle && - offerToUpdate.offersIntern.monthlySalary?.currency && - offerToUpdate.offersIntern.monthlySalary.value && - offerToUpdate.offersIntern.startYear + offerToUpdate.offersIntern.internshipCycle != null && + offerToUpdate.offersIntern.monthlySalary?.currency != null && + offerToUpdate.offersIntern.monthlySalary?.value != null && + offerToUpdate.offersIntern.startYear != null ) { await ctx.prisma.offersProfile.update({ data: { @@ -1001,11 +1125,18 @@ export const offersProfileRouter = createRouter() offerToUpdate.offersIntern.internshipCycle, monthlySalary: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersIntern.monthlySalary.value, + offerToUpdate.offersIntern.monthlySalary + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersIntern.monthlySalary - ?.currency, + .currency, value: - offerToUpdate.offersIntern.monthlySalary?.value, + offerToUpdate.offersIntern.monthlySalary.value, }, }, specialization: @@ -1023,17 +1154,18 @@ export const offersProfileRouter = createRouter() }); } if ( - offerToUpdate.jobType === 'FULLTIME' && + offerToUpdate.jobType === JobType.FULLTIME && offerToUpdate.offersFullTime && - offerToUpdate.offersFullTime.baseSalary?.currency && - offerToUpdate.offersFullTime.baseSalary?.value && - offerToUpdate.offersFullTime.bonus?.currency && - offerToUpdate.offersFullTime.bonus?.value && - offerToUpdate.offersFullTime.stocks?.currency && - offerToUpdate.offersFullTime.stocks?.value && - offerToUpdate.offersFullTime.totalCompensation?.currency && - offerToUpdate.offersFullTime.totalCompensation?.value && - offerToUpdate.offersFullTime.level + offerToUpdate.offersFullTime.baseSalary?.currency != null && + offerToUpdate.offersFullTime.baseSalary?.value != null && + offerToUpdate.offersFullTime.bonus?.currency != null && + offerToUpdate.offersFullTime.bonus?.value != null && + offerToUpdate.offersFullTime.stocks?.currency != null && + offerToUpdate.offersFullTime.stocks?.value != null && + offerToUpdate.offersFullTime.totalCompensation?.currency != + null && + offerToUpdate.offersFullTime.totalCompensation?.value != null && + offerToUpdate.offersFullTime.level != null ) { await ctx.prisma.offersProfile.update({ data: { @@ -1053,18 +1185,31 @@ export const offersProfileRouter = createRouter() create: { baseSalary: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.baseSalary.value, + offerToUpdate.offersFullTime.baseSalary + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.baseSalary - ?.currency, + .currency, value: - offerToUpdate.offersFullTime.baseSalary?.value, + offerToUpdate.offersFullTime.baseSalary.value, }, }, bonus: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.bonus.value, + offerToUpdate.offersFullTime.bonus.currency, + baseCurrencyString, + ), currency: - offerToUpdate.offersFullTime.bonus?.currency, - value: offerToUpdate.offersFullTime.bonus?.value, + offerToUpdate.offersFullTime.bonus.currency, + value: offerToUpdate.offersFullTime.bonus.value, }, }, level: offerToUpdate.offersFullTime.level, @@ -1072,20 +1217,34 @@ export const offersProfileRouter = createRouter() offerToUpdate.offersFullTime.specialization, stocks: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.stocks.value, + offerToUpdate.offersFullTime.stocks.currency, + baseCurrencyString, + ), currency: - offerToUpdate.offersFullTime.stocks?.currency, - value: offerToUpdate.offersFullTime.stocks?.value, + offerToUpdate.offersFullTime.stocks.currency, + value: offerToUpdate.offersFullTime.stocks.value, }, }, title: offerToUpdate.offersFullTime.title, totalCompensation: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.totalCompensation + .value, + offerToUpdate.offersFullTime.totalCompensation + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.totalCompensation - ?.currency, + .currency, value: offerToUpdate.offersFullTime.totalCompensation - ?.value, + .value, }, }, }, @@ -1102,46 +1261,6 @@ export const offersProfileRouter = createRouter() } const result = await ctx.prisma.offersProfile.findFirst({ - include: { - background: { - include: { - educations: true, - experiences: { - include: { - company: true, - monthlySalary: true, - totalCompensation: true, - }, - }, - specificYoes: true, - }, - }, - discussion: { - include: { - replies: true, - replyingTo: true, - user: true, - }, - }, - offers: { - include: { - company: true, - offersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true, - }, - }, - offersIntern: { - include: { - monthlySalary: true, - }, - }, - }, - }, - }, where: { id: input.id, }, diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 7e511164..a68b19dd 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -7,6 +7,7 @@ import { } from '~/mappers/offers-mappers'; import { convert } from '~/utils/offers/currency/currency-exchange'; import { Currency } from '~/utils/offers/currency/CurrencyEnum'; +import { createValidationRegex } from '~/utils/offers/zodRegex'; import { createRouter } from '../context'; @@ -23,14 +24,6 @@ const sortingKeysMap = { totalYoe: 'totalYoe', }; -const createSortByValidationRegex = () => { - const startsWithPlusOrMinusOnly = '^[+-]{1}'; - const sortingKeysRegex = Object.entries(sortingKeysMap) - .map((entry) => entry[0]) - .join('|'); - return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')'); -}; - const yoeCategoryMap: Record = { 0: 'Internship', 1: 'Fresh Grad', @@ -59,7 +52,10 @@ export const offersRouter = createRouter().query('list', { offset: z.number().nonnegative(), salaryMax: z.number().nonnegative().nullish(), salaryMin: z.number().nonnegative().nullish(), - sortBy: z.string().regex(createSortByValidationRegex()).nullish(), + sortBy: z + .string() + .regex(createValidationRegex(Object.keys(sortingKeysMap), '[+-]{1}')) + .nullish(), title: z.string().nullish(), yoeCategory: z.number().min(0).max(3), yoeMax: z.number().max(100).nullish(), @@ -70,8 +66,6 @@ export const offersRouter = createRouter().query('list', { const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe; const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe; - // Const orderBy = getSortingOrderAndKey(input.sortBy, input.yoeCategory); - if (!input.sortBy) { input.sortBy = '-' + sortingKeysMap.monthYearReceived; } @@ -338,112 +332,6 @@ export const offersRouter = createRouter().query('list', { ); } - // SORTING - // data = data.sort((offer1, offer2) => { - // const defaultReturn = - // offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime(); - - // if (!input.sortBy) { - // return defaultReturn; - // } - - // const order = input.sortBy.charAt(0); - // const sortingKey = input.sortBy.substring(1); - - // if (order === ascOrder) { - // return (() => { - // if (sortingKey === 'monthYearReceived') { - // return ( - // offer1.monthYearReceived.getTime() - - // offer2.monthYearReceived.getTime() - // ); - // } - - // if (sortingKey === 'totalCompensation') { - // const salary1 = offer1.offersFullTime?.totalCompensation.value - // ? offer1.offersFullTime?.totalCompensation.value - // : offer1.offersIntern?.monthlySalary.value; - - // const salary2 = offer2.offersFullTime?.totalCompensation.value - // ? offer2.offersFullTime?.totalCompensation.value - // : offer2.offersIntern?.monthlySalary.value; - - // if (salary1 == null || salary2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total Compensation or Salary not found', - // }); - // } - - // return salary1 - salary2; - // } - - // if (sortingKey === 'totalYoe') { - // const yoe1 = offer1.profile.background?.totalYoe; - // const yoe2 = offer2.profile.background?.totalYoe; - - // if (yoe1 == null || yoe2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total years of experience not found', - // }); - // } - - // return yoe1 - yoe2; - // } - - // return defaultReturn; - // })(); - // } - - // if (order === descOrder) { - // return (() => { - // if (sortingKey === 'monthYearReceived') { - // return ( - // offer2.monthYearReceived.getTime() - - // offer1.monthYearReceived.getTime() - // ); - // } - - // if (sortingKey === 'totalCompensation') { - // const salary1 = offer1.offersFullTime?.totalCompensation.value - // ? offer1.offersFullTime?.totalCompensation.value - // : offer1.offersIntern?.monthlySalary.value; - - // const salary2 = offer2.offersFullTime?.totalCompensation.value - // ? offer2.offersFullTime?.totalCompensation.value - // : offer2.offersIntern?.monthlySalary.value; - - // if (salary1 == null || salary2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total Compensation or Salary not found', - // }); - // } - - // return salary2 - salary1; - // } - - // if (sortingKey === 'totalYoe') { - // const yoe1 = offer1.profile.background?.totalYoe; - // const yoe2 = offer2.profile.background?.totalYoe; - - // if (yoe1 == null || yoe2 == null) { - // throw new TRPCError({ - // code: 'NOT_FOUND', - // message: 'Total years of experience not found', - // }); - // } - - // return yoe2 - yoe1; - // } - - // return defaultReturn; - // })(); - // } - // return defaultReturn; - // }); - const startRecordIndex: number = input.limit * input.offset; const endRecordIndex: number = startRecordIndex + input.limit <= data.length diff --git a/apps/portal/src/utils/offers/currency/currency-exchange.ts b/apps/portal/src/utils/offers/currency/currency-exchange.ts index 31e02067..4c94209a 100644 --- a/apps/portal/src/utils/offers/currency/currency-exchange.ts +++ b/apps/portal/src/utils/offers/currency/currency-exchange.ts @@ -6,7 +6,11 @@ export const convert = async ( ) => { fromCurrency = fromCurrency.trim().toLowerCase(); toCurrency = toCurrency.trim().toLowerCase(); - const url = ['https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies', fromCurrency, toCurrency].join('/'); + const url = [ + 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies', + fromCurrency, + toCurrency, + ].join('/'); return await fetch(url + '.json') .then((res) => res.json()) diff --git a/apps/portal/src/utils/offers/currency/index.tsx b/apps/portal/src/utils/offers/currency/index.tsx index 373d2984..1e219c45 100644 --- a/apps/portal/src/utils/offers/currency/index.tsx +++ b/apps/portal/src/utils/offers/currency/index.tsx @@ -1,5 +1,9 @@ import type { Money } from '~/components/offers/types'; +import { Currency } from './CurrencyEnum'; + +export const baseCurrencyString = Currency.USD.toString(); + export function convertMoneyToString({ currency, value }: Money) { if (!value) { return '-'; diff --git a/apps/portal/src/utils/offers/zodRegex.ts b/apps/portal/src/utils/offers/zodRegex.ts new file mode 100644 index 00000000..614b76d4 --- /dev/null +++ b/apps/portal/src/utils/offers/zodRegex.ts @@ -0,0 +1,8 @@ +export const createValidationRegex = ( + keywordArray: Array, + prepend: string | null | undefined, +) => { + const sortingKeysRegex = keywordArray.join('|'); + prepend = prepend != null ? prepend : ''; + return new RegExp('^' + prepend + '(' + sortingKeysRegex + ')$'); +}; From 13f40ab6ae89196f3695a986a1bad51b9bbb6e79 Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Sat, 22 Oct 2022 08:01:29 +0800 Subject: [PATCH 09/19] [offers][chore] Add baseCurrency and baseValue to Valuation DTO --- apps/portal/src/mappers/offers-mappers.ts | 13 +++++++++++-- apps/portal/src/pages/offers/test/createProfile.tsx | 8 ++++---- apps/portal/src/pages/offers/test/listOffers.tsx | 4 ++-- apps/portal/src/types/offers.d.ts | 2 ++ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts index 1c1c9c33..ea0f624c 100644 --- a/apps/portal/src/mappers/offers-mappers.ts +++ b/apps/portal/src/mappers/offers-mappers.ts @@ -50,7 +50,7 @@ const analysisOfferDtoMapper = ( const analysisOfferDto: AnalysisOffer = { company: offersCompanyDtoMapper(offer.company), id: offer.id, - income: { currency: '', value: -1 }, + income: { baseCurrency: '', baseValue: -1, currency: '', value: -1 }, jobType: offer.jobType, level: offer.offersFullTime?.level ?? '', location: offer.location, @@ -219,11 +219,15 @@ export const profileAnalysisDtoMapper = ( }; export const valuationDtoMapper = (currency: { + baseCurrency: string; + baseValue: number; currency: string; id?: string; value: number; }) => { const valuationDto: Valuation = { + baseCurrency: currency.baseCurrency, + baseValue: currency.baseValue, currency: currency.currency, value: currency.value, }; @@ -554,7 +558,12 @@ export const dashboardOfferDtoMapper = ( const dashboardOfferDto: DashboardOffer = { company: offersCompanyDtoMapper(offer.company), id: offer.id, - income: valuationDtoMapper({ currency: '', value: -1 }), + income: valuationDtoMapper({ + baseCurrency: '', + baseValue: -1, + currency: '', + value: -1, + }), monthYearReceived: offer.monthYearReceived, profileId: offer.profileId, title: offer.offersFullTime?.title ?? '', diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx index 7928ab12..972ba6ee 100644 --- a/apps/portal/src/pages/offers/test/createProfile.tsx +++ b/apps/portal/src/pages/offers/test/createProfile.tsx @@ -139,22 +139,22 @@ function Test() { offersFullTime: { baseSalary: { currency: 'SGD', - value: 84000, + value: 2222, }, bonus: { currency: 'SGD', - value: 20000, + value: 2222, }, level: 'Junior', specialization: 'Front End', stocks: { currency: 'SGD', - value: 100, + value: 0, }, title: 'Software Engineer', totalCompensation: { currency: 'SGD', - value: 104100, + value: 4444, }, }, }, diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx index 911af3ac..747f5102 100644 --- a/apps/portal/src/pages/offers/test/listOffers.tsx +++ b/apps/portal/src/pages/offers/test/listOffers.tsx @@ -9,8 +9,8 @@ function Test() { limit: 100, location: 'Singapore, Singapore', offset: 0, - sortBy: '-monthYearReceived', - yoeCategory: 0, + sortBy: '-totalCompensation', + yoeCategory: 2, }, ]); diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts index 2605a30f..e2ca2d2f 100644 --- a/apps/portal/src/types/offers.d.ts +++ b/apps/portal/src/types/offers.d.ts @@ -42,6 +42,8 @@ export type OffersCompany = { }; export type Valuation = { + baseCurrency: string; + baseValue: number; currency: string; value: number; }; From b2237f97f274d9927a4f5d40896c1962ac6fcaf5 Mon Sep 17 00:00:00 2001 From: Wu Peirong Date: Sat, 22 Oct 2022 10:05:15 +0800 Subject: [PATCH 10/19] [resumes][feat] add spinner to browse page --- apps/portal/src/pages/resumes/browse.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/pages/resumes/browse.tsx b/apps/portal/src/pages/resumes/browse.tsx index af1cae93..4182fd63 100644 --- a/apps/portal/src/pages/resumes/browse.tsx +++ b/apps/portal/src/pages/resumes/browse.tsx @@ -14,6 +14,7 @@ import { CheckboxList, DropdownMenu, Pagination, + Spinner, Tabs, TextInput, } from '@tih/ui'; @@ -528,7 +529,15 @@ export default function ResumeHomePage() {
- {sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? ( + {allResumesQuery.isLoading || + starredResumesQuery.isLoading || + myResumesQuery.isLoading ? ( +
+ {' '} + {' '} +
+ ) : sessionData === null && + tabsValue !== BROWSE_TABS_VALUES.ALL ? ( Date: Sat, 22 Oct 2022 10:23:26 +0800 Subject: [PATCH 11/19] [resumes][fix] fix nouns singular/plural (s) --- .../portal/src/components/resumes/browse/ResumeListItem.tsx | 6 ++++-- apps/portal/src/pages/resumes/[resumeId].tsx | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx index b0ef8b4d..5726badd 100644 --- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx +++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx @@ -42,7 +42,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
{`${resumeInfo.numComments} comment${ - resumeInfo.numComments > 0 ? 's' : '' + resumeInfo.numComments === 1 ? '' : 's' }`}
@@ -51,7 +51,9 @@ export default function ResumeListItem({ href, resumeInfo }: Props) { ) : ( )} - {resumeInfo.numStars} stars + {`${resumeInfo.numStars} star${ + resumeInfo.numStars === 1 ? '' : 's' + }`}
diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index 7a4c1e94..782ead96 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -131,7 +131,9 @@ export default function ResumeReviewPage() { onClick={onStarButtonClick}>
- {starMutation.isLoading || unstarMutation.isLoading ? ( + {starMutation.isLoading || + unstarMutation.isLoading || + detailsQuery.isLoading ? ( ) : ( Date: Sat, 22 Oct 2022 13:24:45 +0800 Subject: [PATCH 12/19] [offers][fix] Fix analysis and offers API to accommodate new fields in OffersCurrency --- apps/portal/src/mappers/offers-mappers.ts | 10 +++- .../pages/offers/test/generateAnalysis.tsx | 2 +- .../src/pages/offers/test/getAnalysis.tsx | 2 +- .../src/pages/offers/test/listOffers.tsx | 1 + .../router/offers/offers-analysis-router.ts | 39 +++++++-------- .../portal/src/server/router/offers/offers.ts | 50 +++++++++++-------- .../offers/currency/currency-exchange.ts | 31 ++++++++++++ 7 files changed, 90 insertions(+), 45 deletions(-) diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts index ea0f624c..f8b0839f 100644 --- a/apps/portal/src/mappers/offers-mappers.ts +++ b/apps/portal/src/mappers/offers-mappers.ts @@ -56,7 +56,7 @@ const analysisOfferDtoMapper = ( location: offer.location, monthYearReceived: offer.monthYearReceived, negotiationStrategy: offer.negotiationStrategy, - previousCompanies: [], + previousCompanies: [], // TODO: Fill this up profileName, specialization: offer.jobType === JobType.FULLTIME @@ -74,10 +74,18 @@ const analysisOfferDtoMapper = ( offer.offersFullTime.totalCompensation.value; analysisOfferDto.income.currency = offer.offersFullTime.totalCompensation.currency; + analysisOfferDto.income.baseValue = + offer.offersFullTime.totalCompensation.baseValue; + analysisOfferDto.income.baseCurrency = + offer.offersFullTime.totalCompensation.baseCurrency; } else if (offer.offersIntern?.monthlySalary) { analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value; analysisOfferDto.income.currency = offer.offersIntern.monthlySalary.currency; + analysisOfferDto.income.baseValue = + offer.offersIntern.monthlySalary.baseValue; + analysisOfferDto.income.baseCurrency = + offer.offersIntern.monthlySalary.baseCurrency; } else { throw new TRPCError({ code: 'NOT_FOUND', diff --git a/apps/portal/src/pages/offers/test/generateAnalysis.tsx b/apps/portal/src/pages/offers/test/generateAnalysis.tsx index dc1fc18c..029ab5fe 100644 --- a/apps/portal/src/pages/offers/test/generateAnalysis.tsx +++ b/apps/portal/src/pages/offers/test/generateAnalysis.tsx @@ -8,7 +8,7 @@ function GenerateAnalysis() { return (
{JSON.stringify( - analysisMutation.mutate({ profileId: 'cl9h23fb1002ftxysli5iziu2' }), + analysisMutation.mutate({ profileId: 'cl9j50xzk008vutfqg6mta2ey' }), )}
); diff --git a/apps/portal/src/pages/offers/test/getAnalysis.tsx b/apps/portal/src/pages/offers/test/getAnalysis.tsx index 4f29ddf7..477ee183 100644 --- a/apps/portal/src/pages/offers/test/getAnalysis.tsx +++ b/apps/portal/src/pages/offers/test/getAnalysis.tsx @@ -5,7 +5,7 @@ import { trpc } from '~/utils/trpc'; function GetAnalysis() { const analysis = trpc.useQuery([ 'offers.analysis.get', - { profileId: 'cl9h23fb1002ftxysli5iziu2' }, + { profileId: 'cl9j50xzk008vutfqg6mta2ey' }, ]); return
{JSON.stringify(analysis.data)}
; diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx index 747f5102..b59f50c6 100644 --- a/apps/portal/src/pages/offers/test/listOffers.tsx +++ b/apps/portal/src/pages/offers/test/listOffers.tsx @@ -6,6 +6,7 @@ function Test() { const data = trpc.useQuery([ 'offers.list', { + currency: 'SGD', limit: 100, location: 'Singapore, Singapore', offset: 0, diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts index 37b0d83b..35bb9924 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -187,14 +187,14 @@ export const offersAnalysisRouter = createRouter() { offersFullTime: { totalCompensation: { - value: 'desc', + baseValue: 'desc', }, }, }, { offersIntern: { monthlySalary: { - value: 'desc', + baseValue: 'desc', }, }, }, @@ -216,11 +216,11 @@ export const offersAnalysisRouter = createRouter() // TODO: Shift yoe out of background to make it mandatory if ( !overallHighestOffer.profile.background || - overallHighestOffer.profile.background.totalYoe === undefined + overallHighestOffer.profile.background.totalYoe == null ) { throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Cannot analyse without YOE', + code: 'NOT_FOUND', + message: 'YOE not found', }); } @@ -257,14 +257,14 @@ export const offersAnalysisRouter = createRouter() { offersFullTime: { totalCompensation: { - value: 'desc', + baseValue: 'desc', }, }, }, { offersIntern: { monthlySalary: { - value: 'desc', + baseValue: 'desc', }, }, }, @@ -279,12 +279,10 @@ export const offersAnalysisRouter = createRouter() { offersFullTime: { level: overallHighestOffer.offersFullTime?.level, - specialization: - overallHighestOffer.offersFullTime?.specialization, + title: overallHighestOffer.offersFullTime?.title, }, offersIntern: { - specialization: - overallHighestOffer.offersIntern?.specialization, + title: overallHighestOffer.offersIntern?.title, }, }, ], @@ -317,7 +315,7 @@ export const offersAnalysisRouter = createRouter() similarOffers, ); const overallPercentile = - similarOffers.length === 0 ? 0 : overallIndex / similarOffers.length; + similarOffers.length === 0 ? 1.0 : overallIndex / similarOffers.length; const companyIndex = searchOfferPercentile( overallHighestOffer, @@ -325,10 +323,11 @@ export const offersAnalysisRouter = createRouter() ); const companyPercentile = similarCompanyOffers.length === 0 - ? 0 + ? 1.0 : companyIndex / similarCompanyOffers.length; - // FIND TOP >=90 PERCENTILE OFFERS + // FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE + // e.g. If there only 4 offers, it gives the 2nd and 3rd offer similarOffers = similarOffers.filter( (offer) => offer.id !== overallHighestOffer.id, ); @@ -337,10 +336,9 @@ export const offersAnalysisRouter = createRouter() ); const noOfSimilarOffers = similarOffers.length; - const similarOffers90PercentileIndex = - Math.floor(noOfSimilarOffers * 0.9) - 1; + const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1); const topPercentileOffers = - noOfSimilarOffers > 1 + noOfSimilarOffers > 2 ? similarOffers.slice( similarOffers90PercentileIndex, similarOffers90PercentileIndex + 2, @@ -348,10 +346,11 @@ export const offersAnalysisRouter = createRouter() : similarOffers; const noOfSimilarCompanyOffers = similarCompanyOffers.length; - const similarCompanyOffers90PercentileIndex = - Math.floor(noOfSimilarCompanyOffers * 0.9) - 1; + const similarCompanyOffers90PercentileIndex = Math.ceil( + noOfSimilarCompanyOffers * 0.1, + ); const topPercentileCompanyOffers = - noOfSimilarCompanyOffers > 1 + noOfSimilarCompanyOffers > 2 ? similarCompanyOffers.slice( similarCompanyOffers90PercentileIndex, similarCompanyOffers90PercentileIndex + 2, diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index a68b19dd..284063e9 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -5,7 +5,7 @@ import { dashboardOfferDtoMapper, getOffersResponseMapper, } from '~/mappers/offers-mappers'; -import { convert } from '~/utils/offers/currency/currency-exchange'; +import { convertWithDate } from '~/utils/offers/currency/currency-exchange'; import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import { createValidationRegex } from '~/utils/offers/zodRegex'; @@ -106,7 +106,7 @@ export const offersRouter = createRouter().query('list', { ? { offersIntern: { monthlySalary: { - value: order, + baseValue: order, }, }, } @@ -141,7 +141,7 @@ export const offersRouter = createRouter().query('list', { { offersIntern: { monthlySalary: { - value: { + baseValue: { gte: input.salaryMin ?? undefined, lte: input.salaryMax ?? undefined, }, @@ -210,7 +210,7 @@ export const offersRouter = createRouter().query('list', { ? { offersFullTime: { totalCompensation: { - value: order, + baseValue: order, }, }, } @@ -250,7 +250,7 @@ export const offersRouter = createRouter().query('list', { { offersFullTime: { totalCompensation: { - value: { + baseValue: { gte: input.salaryMin ?? undefined, lte: input.salaryMax ?? undefined, }, @@ -288,36 +288,42 @@ export const offersRouter = createRouter().query('list', { if (currency != null && currency in Currency) { data = await Promise.all( data.map(async (offer) => { - if (offer.offersFullTime?.totalCompensation) { - offer.offersFullTime.totalCompensation.value = await convert( - offer.offersFullTime.totalCompensation.value, - offer.offersFullTime.totalCompensation.currency, - currency, - ); + if (offer.offersFullTime?.totalCompensation != null) { + offer.offersFullTime.totalCompensation.value = + await convertWithDate( + offer.offersFullTime.totalCompensation.value, + offer.offersFullTime.totalCompensation.currency, + currency, + offer.offersFullTime.totalCompensation.updatedAt, + ); offer.offersFullTime.totalCompensation.currency = currency; - offer.offersFullTime.baseSalary.value = await convert( - offer.offersFullTime.totalCompensation.value, - offer.offersFullTime.totalCompensation.currency, + offer.offersFullTime.baseSalary.value = await convertWithDate( + offer.offersFullTime.baseSalary.value, + offer.offersFullTime.baseSalary.currency, currency, + offer.offersFullTime.baseSalary.updatedAt, ); offer.offersFullTime.baseSalary.currency = currency; - offer.offersFullTime.stocks.value = await convert( - offer.offersFullTime.totalCompensation.value, - offer.offersFullTime.totalCompensation.currency, + offer.offersFullTime.stocks.value = await convertWithDate( + offer.offersFullTime.stocks.value, + offer.offersFullTime.stocks.currency, currency, + offer.offersFullTime.stocks.updatedAt, ); offer.offersFullTime.stocks.currency = currency; - offer.offersFullTime.bonus.value = await convert( - offer.offersFullTime.totalCompensation.value, - offer.offersFullTime.totalCompensation.currency, + offer.offersFullTime.bonus.value = await convertWithDate( + offer.offersFullTime.bonus.value, + offer.offersFullTime.bonus.currency, currency, + offer.offersFullTime.bonus.updatedAt, ); offer.offersFullTime.bonus.currency = currency; - } else if (offer.offersIntern?.monthlySalary) { - offer.offersIntern.monthlySalary.value = await convert( + } else if (offer.offersIntern?.monthlySalary != null) { + offer.offersIntern.monthlySalary.value = await convertWithDate( offer.offersIntern.monthlySalary.value, offer.offersIntern.monthlySalary.currency, currency, + offer.offersIntern.monthlySalary.updatedAt, ); offer.offersIntern.monthlySalary.currency = currency; } else { diff --git a/apps/portal/src/utils/offers/currency/currency-exchange.ts b/apps/portal/src/utils/offers/currency/currency-exchange.ts index 4c94209a..0f642100 100644 --- a/apps/portal/src/utils/offers/currency/currency-exchange.ts +++ b/apps/portal/src/utils/offers/currency/currency-exchange.ts @@ -1,4 +1,5 @@ // API from https://github.com/fawazahmed0/currency-api#readme + export const convert = async ( value: number, fromCurrency: string, @@ -16,3 +17,33 @@ export const convert = async ( .then((res) => res.json()) .then((data) => value * data[toCurrency]); }; +// https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@{apiVersion}/{date}/{endpoint} + +export const convertWithDate = async ( + value: number, + fromCurrency: string, + toCurrency: string, + date: Date, +) => { + if (new Date().toDateString === date.toDateString) { + return await convert(value, fromCurrency, toCurrency); + } + + fromCurrency = fromCurrency.trim().toLowerCase(); + toCurrency = toCurrency.trim().toLowerCase(); + + // Format date to YYYY-MM-DD + const formattedDate = date.toJSON().substring(0, 10); + + const url = [ + 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1', + formattedDate, + 'currencies', + fromCurrency, + toCurrency, + ].join('/'); + + return await fetch(url + '.json') + .then((res) => res.json()) + .then((data) => value * data[toCurrency]); +}; From 5e6482aa2e5b3c7aff8fa6b0b35bb74111007c9d Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Sat, 22 Oct 2022 13:44:36 +0800 Subject: [PATCH 13/19] [offers][fix] Add previous companies to analysis DTO --- apps/portal/src/mappers/offers-mappers.ts | 35 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts index f8b0839f..4faee7ae 100644 --- a/apps/portal/src/mappers/offers-mappers.ts +++ b/apps/portal/src/mappers/offers-mappers.ts @@ -43,7 +43,13 @@ const analysisOfferDtoMapper = ( | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; - profile: OffersProfile & { background: OffersBackground | null }; + profile: OffersProfile & { + background: + | (OffersBackground & { + experiences: Array; + }) + | null; + }; }, ) => { const { background, profileName } = offer.profile; @@ -56,7 +62,10 @@ const analysisOfferDtoMapper = ( location: offer.location, monthYearReceived: offer.monthYearReceived, negotiationStrategy: offer.negotiationStrategy, - previousCompanies: [], // TODO: Fill this up + previousCompanies: + background?.experiences + ?.filter((exp) => exp.company != null) + .map((exp) => exp.company?.name ?? '') ?? [], profileName, specialization: offer.jobType === JobType.FULLTIME @@ -103,10 +112,26 @@ const analysisDtoMapper = ( OffersOffer & { company: Company; offersFullTime: - | (OffersFullTime & { totalCompensation: OffersCurrency }) + | (OffersFullTime & { + totalCompensation: OffersCurrency; + }) | null; - offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; - profile: OffersProfile & { background: OffersBackground | null }; + offersIntern: + | (OffersIntern & { + monthlySalary: OffersCurrency; + }) + | null; + profile: OffersProfile & { + background: + | (OffersBackground & { + experiences: Array< + OffersExperience & { + company: Company | null; + } + >; + }) + | null; + }; } >, ) => { From b37ce69c25f115b249e05b54342d4e039276e992 Mon Sep 17 00:00:00 2001 From: hpkoh <53825802+hpkoh@users.noreply.github.com> Date: Sat, 22 Oct 2022 14:31:29 +0800 Subject: [PATCH 14/19] [questions][feat] update question filter (#384) * [questions][chore] refactor question queries * [questions][chore] destructure values from input * [questions][feat] add sorting * [question][fix] fix frontend * [questions][feat] add sorting * [questions][feat] add sorting index * [questions][chore] push migration file * [questions][fix] fix ci issues * [questions][fix] fix import errors Co-authored-by: Jeff Sieu --- .../migration.sql | 12 ++ .../migration.sql | 12 ++ .../migration.sql | 5 + apps/portal/prisma/schema.prisma | 5 + apps/portal/src/pages/questions/index.tsx | 5 + .../router/questions-question-router.ts | 138 +++++++++++++----- apps/portal/src/types/questions.d.ts | 12 +- 7 files changed, 151 insertions(+), 38 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql create mode 100644 apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql create mode 100644 apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql diff --git a/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql b/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql new file mode 100644 index 00000000..a6319a17 --- /dev/null +++ b/apps/portal/prisma/migrations/20221021150358_add_vote_count_and_last_seen/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `upvotes` to the `QuestionsQuestion` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ADD COLUMN "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "upvotes" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "QuestionsQuestionEncounter" ADD COLUMN "netVotes" INTEGER NOT NULL DEFAULT 0; diff --git a/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql b/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql new file mode 100644 index 00000000..ef9e4229 --- /dev/null +++ b/apps/portal/prisma/migrations/20221021151424_delete_extra_encounter_fields/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `netVotes` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ALTER COLUMN "lastSeenAt" DROP DEFAULT, +ALTER COLUMN "upvotes" SET DEFAULT 0; + +-- AlterTable +ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "netVotes"; diff --git a/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql b/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql new file mode 100644 index 00000000..6ae3366a --- /dev/null +++ b/apps/portal/prisma/migrations/20221021155717_add_sorting_index/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "QuestionsQuestion_lastSeenAt_id_idx" ON "QuestionsQuestion"("lastSeenAt", "id"); + +-- CreateIndex +CREATE INDEX "QuestionsQuestion_upvotes_id_idx" ON "QuestionsQuestion"("upvotes", "id"); diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 67a4f6d3..fb263f80 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -404,6 +404,8 @@ model QuestionsQuestion { userId String? content String @db.Text questionType QuestionsQuestionType + lastSeenAt DateTime + upvotes Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -412,6 +414,9 @@ model QuestionsQuestion { votes QuestionsQuestionVote[] comments QuestionsQuestionComment[] answers QuestionsAnswer[] + + @@index([lastSeenAt, id]) + @@index([upvotes, id]) } model QuestionsQuestionEncounter { diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx index 69caac9f..feaffa80 100644 --- a/apps/portal/src/pages/questions/index.tsx +++ b/apps/portal/src/pages/questions/index.tsx @@ -26,6 +26,8 @@ import { } from '~/utils/questions/useSearchFilter'; import { trpc } from '~/utils/trpc'; +import { SortOrder, SortType } from '~/types/questions.d'; + export default function QuestionsHomePage() { const router = useRouter(); @@ -70,6 +72,9 @@ export default function QuestionsHomePage() { locations: selectedLocations, questionTypes: selectedQuestionTypes, roles: [], + // TODO: Implement sort order and sort type choices + sortOrder: SortOrder.DESC, + sortType: SortType.NEW, startDate, }, ], diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts index 75fb7d17..52c8017c 100644 --- a/apps/portal/src/server/router/questions-question-router.ts +++ b/apps/portal/src/server/router/questions-question-router.ts @@ -5,18 +5,33 @@ import { TRPCError } from '@trpc/server'; import { createProtectedRouter } from './context'; import type { Question } from '~/types/questions'; +import { SortOrder, SortType } from '~/types/questions.d'; + +const TWO_WEEK_IN_MS = 12096e5; export const questionsQuestionRouter = createProtectedRouter() .query('getQuestionsByFilter', { input: z.object({ companyNames: z.string().array(), - endDate: z.date(), + endDate: z.date().default(new Date()), locations: z.string().array(), + pageSize: z.number().default(50), questionTypes: z.nativeEnum(QuestionsQuestionType).array(), roles: z.string().array(), - startDate: z.date().optional(), + sortOrder: z.nativeEnum(SortOrder), + sortType: z.nativeEnum(SortType), + startDate: z.date().default(new Date(Date.now() - TWO_WEEK_IN_MS)), }), async resolve({ ctx, input }) { + const sortCondition = + input.sortType === SortType.TOP + ? { + upvotes: input.sortOrder, + } + : { + lastSeenAt: input.sortOrder, + }; + const questionsData = await ctx.prisma.questionsQuestion.findMany({ include: { _count: { @@ -41,7 +56,7 @@ export const questionsQuestionRouter = createProtectedRouter() votes: true, }, orderBy: { - createdAt: 'desc', + ...sortCondition, }, where: { ...(input.questionTypes.length > 0 @@ -53,6 +68,10 @@ export const questionsQuestionRouter = createProtectedRouter() : {}), encounters: { some: { + seenAt: { + gte: input.startDate, + lte: input.endDate, + }, ...(input.companyNames.length > 0 ? { company: { @@ -204,24 +223,23 @@ export const questionsQuestionRouter = createProtectedRouter() data: { content: input.content, encounters: { - create: [ - { - company: { - connect: { - id: input.companyId, - }, + create: { + company: { + connect: { + id: input.companyId, }, - location: input.location, - role: input.role, - seenAt: input.seenAt, - user: { - connect: { - id: userId, - }, + }, + location: input.location, + role: input.role, + seenAt: input.seenAt, + user: { + connect: { + id: userId, }, }, - ], + }, }, + lastSeenAt: input.seenAt, questionType: input.questionType, userId, }, @@ -316,13 +334,28 @@ export const questionsQuestionRouter = createProtectedRouter() const userId = ctx.session?.user?.id; const { questionId, vote } = input; - return await ctx.prisma.questionsQuestionVote.create({ - data: { - questionId, - userId, - vote, - }, - }); + const incrementValue = vote === Vote.UPVOTE ? 1 : -1; + + const [questionVote] = await ctx.prisma.$transaction([ + ctx.prisma.questionsQuestionVote.create({ + data: { + questionId, + userId, + vote, + }, + }), + ctx.prisma.questionsQuestion.update({ + data: { + upvotes: { + increment: incrementValue, + }, + }, + where: { + id: questionId, + }, + }), + ]); + return questionVote; }, }) .mutation('updateVote', { @@ -347,14 +380,30 @@ export const questionsQuestionRouter = createProtectedRouter() }); } - return await ctx.prisma.questionsQuestionVote.update({ - data: { - vote, - }, - where: { - id, - }, - }); + const incrementValue = vote === Vote.UPVOTE ? 2 : -2; + + const [questionVote] = await ctx.prisma.$transaction([ + ctx.prisma.questionsQuestionVote.update({ + data: { + vote, + }, + where: { + id, + }, + }), + ctx.prisma.questionsQuestion.update({ + data: { + upvotes: { + increment: incrementValue, + }, + }, + where: { + id: voteToUpdate.questionId, + }, + }), + ]); + + return questionVote; }, }) .mutation('deleteVote', { @@ -377,10 +426,25 @@ export const questionsQuestionRouter = createProtectedRouter() }); } - return await ctx.prisma.questionsQuestionVote.delete({ - where: { - id: input.id, - }, - }); + const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1; + + const [questionVote] = await ctx.prisma.$transaction([ + ctx.prisma.questionsQuestionVote.delete({ + where: { + id: input.id, + }, + }), + ctx.prisma.questionsQuestion.update({ + data: { + upvotes: { + increment: incrementValue, + }, + }, + where: { + id: voteToDelete.questionId, + }, + }), + ]); + return questionVote; }, }); diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts index 521f3b8b..d75d82de 100644 --- a/apps/portal/src/types/questions.d.ts +++ b/apps/portal/src/types/questions.d.ts @@ -20,7 +20,7 @@ export type AggregatedQuestionEncounter = { companyCounts: Record; locationCounts: Record; roleCounts: Record; -} +}; export type AnswerComment = { content: string; @@ -50,3 +50,13 @@ export type QuestionComment = { user: string; userImage: string; }; + +export enum SortOrder { + ASC = 'asc', + DESC = 'desc', +} + +export enum SortType { + TOP, + NEW, +} From 78a7e884104c7e1d64694f5e7e61502818dd62aa Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Sat, 22 Oct 2022 16:13:38 +0800 Subject: [PATCH 15/19] [offers][chore] Add monthYearReceived to the analysis generation API --- .../router/offers/offers-analysis-router.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts index 35bb9924..973f3136 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -225,6 +225,8 @@ export const offersAnalysisRouter = createRouter() } const yoe = overallHighestOffer.profile.background.totalYoe as number; + const monthYearReceived = new Date(overallHighestOffer.monthYearReceived); + monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1); let similarOffers = await ctx.prisma.offersOffer.findMany({ include: { @@ -274,6 +276,11 @@ export const offersAnalysisRouter = createRouter() { location: overallHighestOffer.location, }, + { + monthYearReceived: { + gte: monthYearReceived, + }, + }, { OR: [ { @@ -315,7 +322,9 @@ export const offersAnalysisRouter = createRouter() similarOffers, ); const overallPercentile = - similarOffers.length === 0 ? 1.0 : overallIndex / similarOffers.length; + similarOffers.length === 0 + ? 100 + : (100 * overallIndex) / similarOffers.length; const companyIndex = searchOfferPercentile( overallHighestOffer, @@ -323,8 +332,8 @@ export const offersAnalysisRouter = createRouter() ); const companyPercentile = similarCompanyOffers.length === 0 - ? 1.0 - : companyIndex / similarCompanyOffers.length; + ? 100 + : (100 * companyIndex) / similarCompanyOffers.length; // FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE // e.g. If there only 4 offers, it gives the 2nd and 3rd offer From 6948c2e4ee11d0ba44d2382b569abd63a4737fd6 Mon Sep 17 00:00:00 2001 From: Bryann Yeap Kok Keong Date: Sat, 22 Oct 2022 16:25:26 +0800 Subject: [PATCH 16/19] [offers][refactor] Rename currency exchanger file to follow camelCase --- apps/portal/src/pages/offers/test/generateAnalysis.tsx | 2 +- apps/portal/src/pages/offers/test/getAnalysis.tsx | 2 +- apps/portal/src/server/router/offers/offers-profile-router.ts | 2 +- apps/portal/src/server/router/offers/offers.ts | 2 +- .../currency/{currency-exchange.ts => currencyExchange.ts} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename apps/portal/src/utils/offers/currency/{currency-exchange.ts => currencyExchange.ts} (100%) diff --git a/apps/portal/src/pages/offers/test/generateAnalysis.tsx b/apps/portal/src/pages/offers/test/generateAnalysis.tsx index 029ab5fe..87c4cfbb 100644 --- a/apps/portal/src/pages/offers/test/generateAnalysis.tsx +++ b/apps/portal/src/pages/offers/test/generateAnalysis.tsx @@ -8,7 +8,7 @@ function GenerateAnalysis() { return (
{JSON.stringify( - analysisMutation.mutate({ profileId: 'cl9j50xzk008vutfqg6mta2ey' }), + analysisMutation.mutate({ profileId: 'cl9jj2ks1001li9fn9np47wjr' }), )}
); diff --git a/apps/portal/src/pages/offers/test/getAnalysis.tsx b/apps/portal/src/pages/offers/test/getAnalysis.tsx index 477ee183..8ad7bff0 100644 --- a/apps/portal/src/pages/offers/test/getAnalysis.tsx +++ b/apps/portal/src/pages/offers/test/getAnalysis.tsx @@ -5,7 +5,7 @@ import { trpc } from '~/utils/trpc'; function GetAnalysis() { const analysis = trpc.useQuery([ 'offers.analysis.get', - { profileId: 'cl9j50xzk008vutfqg6mta2ey' }, + { profileId: 'cl9jj2ks1001li9fn9np47wjr' }, ]); return
{JSON.stringify(analysis.data)}
; diff --git a/apps/portal/src/server/router/offers/offers-profile-router.ts b/apps/portal/src/server/router/offers/offers-profile-router.ts index 8ce57731..104cdf95 100644 --- a/apps/portal/src/server/router/offers/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers/offers-profile-router.ts @@ -9,7 +9,7 @@ import { profileDtoMapper, } from '~/mappers/offers-mappers'; import { baseCurrencyString } from '~/utils/offers/currency'; -import { convert } from '~/utils/offers/currency/currency-exchange'; +import { convert } from '~/utils/offers/currency/currencyExchange'; import { createValidationRegex } from '~/utils/offers/zodRegex'; import { createRouter } from '../context'; diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 284063e9..8b2321e6 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -5,7 +5,7 @@ import { dashboardOfferDtoMapper, getOffersResponseMapper, } from '~/mappers/offers-mappers'; -import { convertWithDate } from '~/utils/offers/currency/currency-exchange'; +import { convertWithDate } from '~/utils/offers/currency/currencyExchange'; import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import { createValidationRegex } from '~/utils/offers/zodRegex'; diff --git a/apps/portal/src/utils/offers/currency/currency-exchange.ts b/apps/portal/src/utils/offers/currency/currencyExchange.ts similarity index 100% rename from apps/portal/src/utils/offers/currency/currency-exchange.ts rename to apps/portal/src/utils/offers/currency/currencyExchange.ts From e55d08279bce7362957673fee9f76bda03342f47 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sat, 22 Oct 2022 17:46:35 +0800 Subject: [PATCH 17/19] [ui] add toasts --- apps/portal/src/pages/_app.tsx | 9 +- apps/portal/src/pages/index.tsx | 23 +---- apps/portal/src/pages/test__.tsx | 51 +++++++++++ packages/ui/src/Toast/Toast.tsx | 108 +++++++++++++++++++++++ packages/ui/src/Toast/ToastsProvider.tsx | 72 +++++++++++++++ packages/ui/src/index.tsx | 6 ++ 6 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 apps/portal/src/pages/test__.tsx create mode 100644 packages/ui/src/Toast/Toast.tsx create mode 100644 packages/ui/src/Toast/ToastsProvider.tsx diff --git a/apps/portal/src/pages/_app.tsx b/apps/portal/src/pages/_app.tsx index 11606124..9914942e 100644 --- a/apps/portal/src/pages/_app.tsx +++ b/apps/portal/src/pages/_app.tsx @@ -3,6 +3,7 @@ import type { Session } from 'next-auth'; import { SessionProvider } from 'next-auth/react'; import React from 'react'; import superjson from 'superjson'; +import { ToastsProvider } from '@tih/ui'; import { httpBatchLink } from '@trpc/client/links/httpBatchLink'; import { loggerLink } from '@trpc/client/links/loggerLink'; import { withTRPC } from '@trpc/next'; @@ -19,9 +20,11 @@ const MyApp: AppType<{ session: Session | null }> = ({ }) => { return ( - - - + + + + + ); }; diff --git a/apps/portal/src/pages/index.tsx b/apps/portal/src/pages/index.tsx index 360c81e8..51f58469 100644 --- a/apps/portal/src/pages/index.tsx +++ b/apps/portal/src/pages/index.tsx @@ -1,32 +1,11 @@ -import { useState } from 'react'; -import type { TypeaheadOption } from '@tih/ui'; -import { HorizontalDivider } from '@tih/ui'; - -import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; -import type { Month, MonthYear } from '~/components/shared/MonthYearPicker'; -import MonthYearPicker from '~/components/shared/MonthYearPicker'; - export default function HomePage() { - const [selectedCompany, setSelectedCompany] = - useState(null); - const [monthYear, setMonthYear] = useState({ - month: (new Date().getMonth() + 1) as Month, - year: new Date().getFullYear(), - }); - return (

- Homepage + Tech Interview Handbook Portal

- setSelectedCompany(option)} - /> -
{JSON.stringify(selectedCompany, null, 2)}
- -
diff --git a/apps/portal/src/pages/test__.tsx b/apps/portal/src/pages/test__.tsx new file mode 100644 index 00000000..26859c56 --- /dev/null +++ b/apps/portal/src/pages/test__.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import type { TypeaheadOption } from '@tih/ui'; +import { Button } from '@tih/ui'; +import { useToast } from '@tih/ui'; +import { HorizontalDivider } from '@tih/ui'; + +import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; +import type { Month, MonthYear } from '~/components/shared/MonthYearPicker'; +import MonthYearPicker from '~/components/shared/MonthYearPicker'; + +export default function HomePage() { + const [selectedCompany, setSelectedCompany] = + useState(null); + const [monthYear, setMonthYear] = useState({ + month: (new Date().getMonth() + 1) as Month, + year: new Date().getFullYear(), + }); + + const { showToast } = useToast(); + + return ( +
+
+
+

+ Test Page +

+ setSelectedCompany(option)} + /> +
{JSON.stringify(selectedCompany, null, 2)}
+ + + +
+
+
+ ); +} diff --git a/packages/ui/src/Toast/Toast.tsx b/packages/ui/src/Toast/Toast.tsx new file mode 100644 index 00000000..4d6efe51 --- /dev/null +++ b/packages/ui/src/Toast/Toast.tsx @@ -0,0 +1,108 @@ +import { Fragment, useEffect, useRef } from 'react'; +import { Transition } from '@headlessui/react'; +import { CheckIcon } from '@heroicons/react/24/outline'; +import { XMarkIcon } from '@heroicons/react/24/solid'; + +type ToastVariant = 'failure' | 'success'; + +export type ToastMessage = { + duration?: number; + subtitle?: string; + title: string; + variant: ToastVariant; +}; + +type Props = Readonly<{ + duration?: number; + onClose: () => void; + subtitle?: string; + title: string; + variant: ToastVariant; +}>; + +const DEFAULT_DURATION = 5000; + +function ToastIcon({ variant }: Readonly<{ variant: ToastVariant }>) { + switch (variant) { + case 'success': + return ( +
);