@ -0,0 +1,19 @@
|
||||
import reader from 'xlsx';
|
||||
|
||||
const file = reader.readFile('prisma/salaries.xlsx');
|
||||
|
||||
export const COMPANIES: Array<CompanyData> = []
|
||||
|
||||
type CompanyData = {
|
||||
Finalized: string;
|
||||
description: string;
|
||||
logoUrl: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
website: string;
|
||||
};
|
||||
|
||||
const temp = reader.utils.sheet_to_json(file.Sheets[file.SheetNames[1]]);
|
||||
temp.forEach((res: CompanyData) => {
|
||||
COMPANIES.push(res);
|
||||
});
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OffersBackground" ALTER COLUMN "totalYoe" SET DEFAULT 0;
|
@ -0,0 +1,5 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OffersAnalysisUnit" DROP CONSTRAINT "OffersAnalysisUnit_analysedOfferId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysisUnit" ADD CONSTRAINT "OffersAnalysisUnit_analysedOfferId_fkey" FOREIGN KEY ("analysedOfferId") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,5 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "OffersAnalysis" DROP CONSTRAINT "OffersAnalysis_overallAnalysisUnitId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_overallAnalysisUnitId_fkey" FOREIGN KEY ("overallAnalysisUnitId") REFERENCES "OffersAnalysisUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,13 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `location` on the `ResumesResume` table. All the data in the column will be lost.
|
||||
- Added the required column `locationId` to the `ResumesResume` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable. Set default location to Singapore.
|
||||
ALTER TABLE "ResumesResume" DROP COLUMN "location",
|
||||
ADD COLUMN "locationId" TEXT NOT NULL DEFAULT '196';
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ResumesResume" ADD CONSTRAINT "ResumesResume_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Country"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ResumesResume" ALTER COLUMN "locationId" DROP DEFAULT;
|
@ -0,0 +1,377 @@
|
||||
import reader from 'xlsx';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import crypto from 'crypto';
|
||||
import { baseCurrencyString } from '../src/utils/offers/currency';
|
||||
import { convert } from '../src/utils/offers/currency/currencyExchange';
|
||||
import { generateAnalysis } from '../src/utils/offers/analysis/analysisGeneration';
|
||||
|
||||
import {
|
||||
generateRandomName,
|
||||
generateRandomStringForToken,
|
||||
} from '../src/utils/offers/randomGenerator';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Reading our test file
|
||||
const file = reader.readFile('prisma/salaries.xlsx');
|
||||
|
||||
let data: Array<ExcelData> = [];
|
||||
|
||||
type ExcelData = {
|
||||
Timestamp: Date;
|
||||
Type: string;
|
||||
Company: string;
|
||||
Role: string;
|
||||
Income?: number | string;
|
||||
Stocks?: number | string;
|
||||
SignOn?: number | string;
|
||||
TC?: number | string;
|
||||
Bonus?: number | string;
|
||||
Comments?: string;
|
||||
};
|
||||
|
||||
const sheets = file.SheetNames;
|
||||
|
||||
for (let i = 0; i < 1; i++) {
|
||||
const temp = reader.utils.sheet_to_json(file.Sheets[file.SheetNames[i]]);
|
||||
temp.forEach((res: ExcelData) => {
|
||||
data.push(res);
|
||||
});
|
||||
}
|
||||
|
||||
function xlSerialToJsDate(xlSerial) {
|
||||
return new Date(Date.UTC(0, 0, xlSerial - 1));
|
||||
}
|
||||
|
||||
const getJobTitle = (role: string) => {
|
||||
const processedRole = role.toUpperCase().trim();
|
||||
|
||||
if (processedRole.includes('ML ENGINEER')) {
|
||||
return 'machine-learning-engineer';
|
||||
} else if (processedRole.includes('BACKEND')) {
|
||||
return 'back-end-engineer';
|
||||
} else if (processedRole.includes('DATA')) {
|
||||
return 'data-engineer';
|
||||
} else if (processedRole.includes('DEVOPS')) {
|
||||
return 'devops-engineer';
|
||||
} else if (processedRole.includes('ENTERPRISE')) {
|
||||
return 'enterprise-engineer';
|
||||
} else if (processedRole.includes('RESEARCH')) {
|
||||
return 'research-engineer';
|
||||
} else if (
|
||||
processedRole.includes('CYBER') ||
|
||||
processedRole.includes('SECURITY')
|
||||
) {
|
||||
return 'security-engineer';
|
||||
} else if (processedRole.includes('QA')) {
|
||||
return 'test-engineer';
|
||||
} else if (processedRole.includes('SYSTEM')) {
|
||||
return 'systems-engineer';
|
||||
} else {
|
||||
return 'software-engineer'; // Assume default SWE
|
||||
}
|
||||
};
|
||||
|
||||
const getYoe = (type: string) => {
|
||||
const processedType = type.toUpperCase().trim();
|
||||
|
||||
if (
|
||||
processedType.includes('FRESH GRAD') ||
|
||||
processedType.includes('JUNIOR')
|
||||
) {
|
||||
return Math.floor(Math.random() * 3);
|
||||
} else if (processedType.includes('MID')) {
|
||||
return Math.floor(Math.random() * 3) + 3;
|
||||
} else if (processedType.includes('SENIOR')) {
|
||||
return Math.floor(Math.random() * 5) + 6;
|
||||
} else {
|
||||
return 0; // INTERNSHIP OR ERROR -> 0 YOE
|
||||
}
|
||||
};
|
||||
|
||||
const getLevel = (type: string) => {
|
||||
const processedType = type.toUpperCase().trim();
|
||||
|
||||
if (
|
||||
processedType.includes('FRESH GRAD') ||
|
||||
processedType.includes('JUNIOR')
|
||||
) {
|
||||
return 'L1';
|
||||
} else if (processedType.includes('MID')) {
|
||||
return 'L2';
|
||||
} else if (processedType.includes('SENIOR')) {
|
||||
return 'L4';
|
||||
} else {
|
||||
return 'L0';
|
||||
}
|
||||
};
|
||||
|
||||
const createdProfileIds: Array<string> = [];
|
||||
|
||||
const seedSalaries = async () => {
|
||||
console.log('Seeding from salaries sheet...');
|
||||
|
||||
const companyIdMappings = {};
|
||||
(await prisma.company.findMany()).forEach((company) => {
|
||||
companyIdMappings[company.slug] = company.id;
|
||||
});
|
||||
|
||||
// get countryId of Singapore
|
||||
const singapore = await prisma.city.findFirst({
|
||||
where: {
|
||||
name: 'Singapore',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Singapore ID: ' + singapore?.id);
|
||||
// break;
|
||||
// seed here
|
||||
|
||||
if (singapore) {
|
||||
return await Promise.all(
|
||||
data.map(async (data: ExcelData) => {
|
||||
if (data.TC && typeof data.TC === 'number') {
|
||||
// Generate random name until unique
|
||||
let uniqueName: string = await generateRandomName();
|
||||
|
||||
const jobTitle = getJobTitle(data.Role);
|
||||
const yoe = getYoe(data.Type);
|
||||
const level = getLevel(data.Type);
|
||||
|
||||
// check if we have company id
|
||||
if (companyIdMappings[data.Company]) {
|
||||
const token = crypto
|
||||
.createHash('sha256')
|
||||
.update(
|
||||
xlSerialToJsDate(data.Timestamp).toString() +
|
||||
generateRandomStringForToken(),
|
||||
)
|
||||
.digest('hex');
|
||||
|
||||
if (data.Type.toUpperCase() === 'INTERNSHIP') {
|
||||
// create profile
|
||||
const dataAdded = await prisma.offersProfile.create({
|
||||
data: {
|
||||
profileName: uniqueName,
|
||||
createdAt: xlSerialToJsDate(data.Timestamp),
|
||||
editToken: token,
|
||||
background: {
|
||||
create: {
|
||||
totalYoe: yoe,
|
||||
},
|
||||
},
|
||||
offers: {
|
||||
create: {
|
||||
comments: data.Comments ?? '',
|
||||
company: {
|
||||
connect: {
|
||||
id: companyIdMappings[data.Company],
|
||||
},
|
||||
},
|
||||
jobType: 'INTERN',
|
||||
location: {
|
||||
connect: {
|
||||
id: singapore.id,
|
||||
},
|
||||
}, // TODO: DEFAULT AS SG
|
||||
monthYearReceived: xlSerialToJsDate(data.Timestamp),
|
||||
negotiationStrategy: '',
|
||||
offersIntern: {
|
||||
create: {
|
||||
internshipCycle: 'Summer',
|
||||
monthlySalary: {
|
||||
create: {
|
||||
baseCurrency: baseCurrencyString,
|
||||
baseValue: await convert(
|
||||
data.Income
|
||||
? typeof data.Income === 'number'
|
||||
? data.Income
|
||||
: 0
|
||||
: 0,
|
||||
'SGD', // assume sgd
|
||||
baseCurrencyString,
|
||||
),
|
||||
currency: 'SGD', // assume sgd
|
||||
value: data.Income
|
||||
? typeof data.Income === 'number'
|
||||
? data.Income
|
||||
: 0
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
startYear: xlSerialToJsDate(
|
||||
data.Timestamp,
|
||||
).getFullYear(),
|
||||
title: jobTitle,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Profile created:', dataAdded.id);
|
||||
createdProfileIds.push(dataAdded.id);
|
||||
} else {
|
||||
// assume rest full time
|
||||
const dataAdded = await prisma.offersProfile.create({
|
||||
data: {
|
||||
profileName: uniqueName,
|
||||
createdAt: xlSerialToJsDate(data.Timestamp),
|
||||
editToken: token,
|
||||
background: {
|
||||
create: {
|
||||
totalYoe: yoe,
|
||||
},
|
||||
},
|
||||
offers: {
|
||||
create: {
|
||||
comments: data.Comments ?? '',
|
||||
company: {
|
||||
connect: {
|
||||
id: companyIdMappings[data.Company],
|
||||
},
|
||||
},
|
||||
jobType: 'FULLTIME',
|
||||
location: {
|
||||
connect: {
|
||||
id: singapore.id,
|
||||
},
|
||||
}, // TODO: DEFAULT AS SG
|
||||
monthYearReceived: xlSerialToJsDate(data.Timestamp),
|
||||
negotiationStrategy: '',
|
||||
offersFullTime: {
|
||||
create: {
|
||||
baseSalary: {
|
||||
create: {
|
||||
baseCurrency: baseCurrencyString,
|
||||
baseValue: await convert(
|
||||
data.Income
|
||||
? typeof data.Income === 'number'
|
||||
? data.Income
|
||||
: 0
|
||||
: 0,
|
||||
'SGD', // assume sgd
|
||||
baseCurrencyString,
|
||||
),
|
||||
currency: 'SGD', // assume sgd
|
||||
value: data.Income
|
||||
? typeof data.Income === 'number'
|
||||
? data.Income
|
||||
: 0
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
bonus: {
|
||||
create: {
|
||||
baseCurrency: baseCurrencyString,
|
||||
baseValue: await convert(
|
||||
data.Bonus
|
||||
? typeof data.Bonus === 'number'
|
||||
? data.Bonus
|
||||
: 0
|
||||
: 0,
|
||||
'SGD',
|
||||
baseCurrencyString,
|
||||
),
|
||||
currency: 'SGD',
|
||||
value: data.Bonus
|
||||
? typeof data.Bonus === 'number'
|
||||
? data.Bonus
|
||||
: 0
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
level: level,
|
||||
stocks: {
|
||||
create: {
|
||||
baseCurrency: baseCurrencyString,
|
||||
baseValue: await convert(
|
||||
data.Stocks
|
||||
? typeof data.Stocks === 'number'
|
||||
? data.Stocks
|
||||
: 0
|
||||
: 0,
|
||||
'SGD',
|
||||
baseCurrencyString,
|
||||
),
|
||||
currency: 'SGD',
|
||||
value: data.Stocks
|
||||
? typeof data.Stocks === 'number'
|
||||
? data.Stocks
|
||||
: 0
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
title: jobTitle,
|
||||
totalCompensation: {
|
||||
create: {
|
||||
baseCurrency: baseCurrencyString,
|
||||
baseValue: await convert(
|
||||
data.TC
|
||||
? typeof data.TC === 'number'
|
||||
? data.TC
|
||||
: 0
|
||||
: 0,
|
||||
'SGD',
|
||||
baseCurrencyString,
|
||||
),
|
||||
currency: 'SGD',
|
||||
value: data.TC
|
||||
? typeof data.TC === 'number'
|
||||
? data.TC
|
||||
: 0
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log('Profile created:', dataAdded.id);
|
||||
createdProfileIds.push(dataAdded.id);
|
||||
}
|
||||
} else {
|
||||
console.log('Invalid Company: ' + data.Company);
|
||||
}
|
||||
} else {
|
||||
console.log('Invalid TC not a number: ' + data.TC);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const generateAllAnalysis = async () => {
|
||||
return await Promise.all(
|
||||
createdProfileIds.map(async (profileId) => {
|
||||
await generateAnalysis({
|
||||
ctx: { prisma, session: null },
|
||||
input: { profileId },
|
||||
});
|
||||
console.log('Analysis generated for profile with id:', profileId);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
Promise.all([seedSalaries()])
|
||||
.then(() => {
|
||||
console.log(createdProfileIds.length + ' profiles created');
|
||||
console.log('Busy crunching analysis.....');
|
||||
})
|
||||
.then(() => generateAllAnalysis())
|
||||
.then((_data) => {
|
||||
console.log('Seeding from salaries sheet complete');
|
||||
})
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export {};
|
@ -1,16 +1,63 @@
|
||||
import { emptyOption } from './constants';
|
||||
|
||||
export const EducationFieldLabels = [
|
||||
'Business Analytics',
|
||||
'Computer Science',
|
||||
'Data Science and Analytics',
|
||||
'Information Security',
|
||||
'Information Systems',
|
||||
];
|
||||
const EducationFieldLabels = {
|
||||
'aerospace-engineering': 'Aerospace Engineering',
|
||||
'applied-mathematics': 'Applied Mathematics',
|
||||
biology: 'Biology',
|
||||
'biomedical-engineering': 'Biomedical Engineering',
|
||||
'business-analytics': 'Business Analytics',
|
||||
'chemical-engineering': 'Chemical Engineering',
|
||||
chemistry: 'Chemistry',
|
||||
'civil-engineering': 'Civil Engineering',
|
||||
'computational-biology': 'Computational Biology',
|
||||
'computer-engineering': 'Computer Engineering',
|
||||
'computer-science': 'Computer Science',
|
||||
'computer-science-engineering': 'Computer Science and Engineering',
|
||||
'computer-science-molecular-biology':
|
||||
'Computer Science and Molecular Biology',
|
||||
'data-science': 'Data Science',
|
||||
'data-science-analytics': 'Data Science and Analytics',
|
||||
'electrical-engineering': 'Electrical Engineering',
|
||||
'electrical-engineering-computer-science':
|
||||
'Electrical Engineering and Computer Science (EECS)',
|
||||
'electrical-science-and-engineering': 'Electrical Science and Engineering',
|
||||
'engineering-mathematics-statistics':
|
||||
'Engineering Mathematics and Statistics',
|
||||
'engineering-physics': 'Engineering Physics',
|
||||
'engineering-science': 'Engineering Science',
|
||||
'environmental-engineering': 'Environmental Engineering',
|
||||
'environmental-science': 'Environmental Science',
|
||||
'industrial-engineering-operations-research':
|
||||
'Industrial Engineering and Operations Research',
|
||||
'industrial-systems-engineering': 'Industrial Systems Engineering',
|
||||
'information-security': 'Information Security',
|
||||
'information-systems': 'Information Systems',
|
||||
'management-science-and-engineering':
|
||||
'Management Science and Engineering (MS&E)',
|
||||
'materials-science': 'Materials Science',
|
||||
mathematics: 'Mathematics',
|
||||
'mechanical-engineering': 'Mechanical Engineering',
|
||||
'nuclear-engineering': 'Nuclear Engineering',
|
||||
'operations-research': 'Operations Research',
|
||||
physics: 'Physics',
|
||||
'software-engineering': 'Software Engineering',
|
||||
'systems-engineering': 'Systems Engineering',
|
||||
'web-development': 'Web Development',
|
||||
};
|
||||
|
||||
export type EducationType = keyof typeof EducationFieldLabels;
|
||||
|
||||
export function getLabelForEducationFieldType(
|
||||
educationType: EducationType,
|
||||
): string {
|
||||
return EducationFieldLabels[educationType];
|
||||
}
|
||||
|
||||
export type EducationFieldType = keyof typeof EducationFieldLabels;
|
||||
|
||||
export const EducationFieldOptions = [emptyOption].concat(
|
||||
EducationFieldLabels.map((label) => ({
|
||||
Object.entries(EducationFieldLabels).map(([value, label]) => ({
|
||||
label,
|
||||
value: label.replace(/\s+/g, '-').toLowerCase(),
|
||||
value,
|
||||
})),
|
||||
);
|
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 358 KiB |
Before Width: | Height: | Size: 994 KiB After Width: | Height: | Size: 366 KiB |
Before Width: | Height: | Size: 923 KiB After Width: | Height: | Size: 277 KiB |
@ -0,0 +1,37 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
|
||||
|
||||
type Props = Omit<
|
||||
ComponentProps<typeof CitiesTypeahead>,
|
||||
'onSelect' | 'value'
|
||||
> & {
|
||||
names: { label: string; value: string };
|
||||
};
|
||||
|
||||
export default function FormCitiesTypeahead({ names, ...props }: Props) {
|
||||
const { setValue } = useFormContext();
|
||||
const watchCityId = useWatch({
|
||||
name: names.value,
|
||||
});
|
||||
const watchCityName = useWatch({
|
||||
name: names.label,
|
||||
});
|
||||
|
||||
return (
|
||||
<CitiesTypeahead
|
||||
label="Location"
|
||||
{...props}
|
||||
value={{
|
||||
id: watchCityId,
|
||||
label: watchCityName,
|
||||
value: watchCityId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
setValue(names.value, option?.value);
|
||||
setValue(names.label, option?.label);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
|
||||
|
||||
type Props = Omit<
|
||||
ComponentProps<typeof CompaniesTypeahead>,
|
||||
'onSelect' | 'value'
|
||||
> & {
|
||||
names: { label: string; value: string };
|
||||
};
|
||||
|
||||
export default function FormCompaniesTypeahead({ names, ...props }: Props) {
|
||||
const { setValue } = useFormContext();
|
||||
const watchCompanyId = useWatch({
|
||||
name: names.value,
|
||||
});
|
||||
const watchCompanyName = useWatch({
|
||||
name: names.label,
|
||||
});
|
||||
|
||||
return (
|
||||
<CompaniesTypeahead
|
||||
{...props}
|
||||
value={{
|
||||
id: watchCompanyId,
|
||||
label: watchCompanyName,
|
||||
value: watchCompanyId,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
setValue(names.value, option?.value);
|
||||
setValue(names.label, option?.label);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
|
||||
|
||||
type Props = Omit<
|
||||
ComponentProps<typeof JobTitlesTypeahead>,
|
||||
'onSelect' | 'value'
|
||||
> & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function FormJobTitlesTypeahead({ name, ...props }: Props) {
|
||||
const { setValue } = useFormContext();
|
||||
const watchJobTitle = useWatch({
|
||||
name,
|
||||
});
|
||||
|
||||
return (
|
||||
<JobTitlesTypeahead
|
||||
{...props}
|
||||
value={{
|
||||
id: watchJobTitle,
|
||||
label: getLabelForJobTitleType(watchJobTitle as JobTitleType),
|
||||
value: watchJobTitle,
|
||||
}}
|
||||
onSelect={(option) => {
|
||||
setValue(name, option?.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 586 KiB After Width: | Height: | Size: 429 KiB |
Before Width: | Height: | Size: 909 KiB After Width: | Height: | Size: 726 KiB |
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 134 KiB |
@ -0,0 +1,51 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { TypeaheadOption } from '@tih/ui';
|
||||
import { Typeahead } from '@tih/ui';
|
||||
|
||||
import { EXPERIENCES } from '~/utils/resumes/resumeFilters';
|
||||
|
||||
type BaseProps = Pick<
|
||||
ComponentProps<typeof Typeahead>,
|
||||
| 'disabled'
|
||||
| 'errorMessage'
|
||||
| 'isLabelHidden'
|
||||
| 'placeholder'
|
||||
| 'required'
|
||||
| 'textSize'
|
||||
>;
|
||||
|
||||
type Props = BaseProps &
|
||||
Readonly<{
|
||||
onSelect: (option: TypeaheadOption | null) => void;
|
||||
selectedValues?: Set<string>;
|
||||
value?: TypeaheadOption | null;
|
||||
}>;
|
||||
|
||||
export default function ResumeExperienceTypeahead({
|
||||
onSelect,
|
||||
selectedValues = new Set(),
|
||||
value,
|
||||
...props
|
||||
}: Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const options = EXPERIENCES.filter(
|
||||
(option) => !selectedValues.has(option.value),
|
||||
).filter(
|
||||
({ label }) =>
|
||||
label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) > -1,
|
||||
);
|
||||
|
||||
return (
|
||||
<Typeahead
|
||||
label="Experiences"
|
||||
noResultsMessage="No available experiences."
|
||||
nullable={true}
|
||||
options={options}
|
||||
value={value}
|
||||
onQueryChange={setQuery}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { TypeaheadOption } from '@tih/ui';
|
||||
import { Typeahead } from '@tih/ui';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
type BaseProps = Pick<
|
||||
ComponentProps<typeof Typeahead>,
|
||||
| 'disabled'
|
||||
| 'errorMessage'
|
||||
| 'isLabelHidden'
|
||||
| 'placeholder'
|
||||
| 'required'
|
||||
| 'textSize'
|
||||
>;
|
||||
|
||||
type Props = BaseProps &
|
||||
Readonly<{
|
||||
onSelect: (option: TypeaheadOption | null) => void;
|
||||
selectedValues?: Set<string>;
|
||||
value?: TypeaheadOption | null;
|
||||
}>;
|
||||
|
||||
export default function ResumeLocationTypeahead({
|
||||
onSelect,
|
||||
selectedValues = new Set(),
|
||||
value,
|
||||
...props
|
||||
}: Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const countries = trpc.useQuery([
|
||||
'locations.countries.list',
|
||||
{
|
||||
name: query,
|
||||
},
|
||||
]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const { data } = countries;
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data
|
||||
.map(({ id, name }) => ({
|
||||
id,
|
||||
label: name,
|
||||
value: id,
|
||||
}))
|
||||
.filter((option) => !selectedValues.has(option.value));
|
||||
}, [countries, selectedValues]);
|
||||
|
||||
return (
|
||||
<Typeahead
|
||||
label="Location"
|
||||
noResultsMessage="No location found"
|
||||
nullable={true}
|
||||
options={options}
|
||||
value={value}
|
||||
onQueryChange={setQuery}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { TypeaheadOption } from '@tih/ui';
|
||||
import { Typeahead } from '@tih/ui';
|
||||
|
||||
import { JobTitleLabels } from '~/components/shared/JobTitles';
|
||||
|
||||
type BaseProps = Pick<
|
||||
ComponentProps<typeof Typeahead>,
|
||||
| 'disabled'
|
||||
| 'errorMessage'
|
||||
| 'isLabelHidden'
|
||||
| 'placeholder'
|
||||
| 'required'
|
||||
| 'textSize'
|
||||
>;
|
||||
|
||||
type Props = BaseProps &
|
||||
Readonly<{
|
||||
onSelect: (option: TypeaheadOption | null) => void;
|
||||
selectedValues?: Set<string>;
|
||||
value?: TypeaheadOption | null;
|
||||
}>;
|
||||
|
||||
export default function ResumeRoleTypeahead({
|
||||
onSelect,
|
||||
selectedValues = new Set(),
|
||||
value,
|
||||
...props
|
||||
}: Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const options = Object.entries(JobTitleLabels)
|
||||
.map(([slug, label]) => ({
|
||||
id: slug,
|
||||
label,
|
||||
value: slug,
|
||||
}))
|
||||
.filter((option) => !selectedValues.has(option.value))
|
||||
.filter(
|
||||
({ label }) =>
|
||||
label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) > -1,
|
||||
);
|
||||
|
||||
return (
|
||||
<Typeahead
|
||||
label="Role"
|
||||
noResultsMessage="No available roles."
|
||||
nullable={true}
|
||||
options={options}
|
||||
value={value}
|
||||
onQueryChange={setQuery}
|
||||
onSelect={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import type { Config } from 'unique-names-generator';
|
||||
import { adjectives, animals,colors, uniqueNamesGenerator } from 'unique-names-generator';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const customConfig: Config = {
|
||||
dictionaries: [adjectives, colors, animals],
|
||||
length: 3,
|
||||
separator: '-',
|
||||
};
|
||||
|
||||
|
||||
export default async function generateRandomName(): Promise<string> {
|
||||
let uniqueName: string = uniqueNamesGenerator(customConfig);
|
||||
|
||||
let sameNameProfiles = await prisma.offersProfile.findMany({
|
||||
where: {
|
||||
profileName: uniqueName
|
||||
}
|
||||
})
|
||||
|
||||
while (sameNameProfiles.length !== 0) {
|
||||
uniqueName = uniqueNamesGenerator(customConfig);
|
||||
sameNameProfiles = await prisma.offersProfile.findMany({
|
||||
where: {
|
||||
profileName: uniqueName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return uniqueName
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import { trpc } from '../trpc';
|
||||
|
||||
export function useAddQuestionToListAsync() {
|
||||
const utils = trpc.useContext();
|
||||
const { mutateAsync: addQuestionToListAsync } = trpc.useMutation(
|
||||
'questions.lists.createQuestionEntry',
|
||||
{
|
||||
// TODO: Add optimistic update
|
||||
onSuccess: () => {
|
||||
utils.invalidateQueries(['questions.lists.getListsByUser']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return addQuestionToListAsync;
|
||||
}
|
||||
|
||||
export function useRemoveQuestionFromListAsync() {
|
||||
const utils = trpc.useContext();
|
||||
const { mutateAsync: removeQuestionFromListAsync } = trpc.useMutation(
|
||||
'questions.lists.deleteQuestionEntry',
|
||||
{
|
||||
// TODO: Add optimistic update
|
||||
onSuccess: () => {
|
||||
utils.invalidateQueries(['questions.lists.getListsByUser']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return removeQuestionFromListAsync;
|
||||
}
|
||||
|
||||
export function useCreateListAsync() {
|
||||
const utils = trpc.useContext();
|
||||
const { mutateAsync: createListAsync } = trpc.useMutation(
|
||||
'questions.lists.create',
|
||||
{
|
||||
onSuccess: () => {
|
||||
// TODO: Add optimistic update
|
||||
utils.invalidateQueries(['questions.lists.getListsByUser']);
|
||||
},
|
||||
},
|
||||
);
|
||||
return createListAsync;
|
||||
}
|
||||
|
||||
export function useDeleteListAsync() {
|
||||
const utils = trpc.useContext();
|
||||
const { mutateAsync: deleteListAsync } = trpc.useMutation(
|
||||
'questions.lists.delete',
|
||||
{
|
||||
onSuccess: () => {
|
||||
// TODO: Add optimistic update
|
||||
utils.invalidateQueries(['questions.lists.getListsByUser']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return deleteListAsync;
|
||||
}
|