@ -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,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Country" ADD COLUMN "ranking" INTEGER DEFAULT 0;
|
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 164 KiB |
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,30 @@
|
|||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Button } from '@tih/ui';
|
||||||
|
|
||||||
|
export type BackButtonLayoutProps = PropsWithChildren<{
|
||||||
|
href: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function BackButtonLayout({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
}: BackButtonLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-1 flex-col items-stretch gap-4 p-4 lg:flex-row">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
addonPosition="start"
|
||||||
|
display="inline"
|
||||||
|
href={href}
|
||||||
|
icon={ArrowSmallLeftIcon}
|
||||||
|
label="Back"
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-center overflow-y-auto">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,56 +1,82 @@
|
|||||||
export const JobTitleLabels = {
|
type JobTitleData = Record<
|
||||||
'ai-engineer': 'Artificial Intelligence (AI) Engineer',
|
string,
|
||||||
'algorithms-engineer': 'Algorithms Engineer',
|
Readonly<{
|
||||||
'android-engineer': 'Android Software Engineer',
|
label: string;
|
||||||
'applications-engineer': 'Applications Engineer',
|
ranking: number;
|
||||||
'back-end-engineer': 'Back End Engineer',
|
}>
|
||||||
'business-analyst': 'Business Analyst',
|
>;
|
||||||
'business-engineer': 'Business Engineer',
|
|
||||||
'capacity-engineer': 'Capacity Engineer',
|
export const JobTitleLabels: JobTitleData = {
|
||||||
'customer-engineer': 'Customer Engineer',
|
'ai-engineer': { label: 'Artificial Intelligence (AI) Engineer', ranking: 5 },
|
||||||
'data-analyst': 'Data Analyst',
|
'algorithms-engineer': { label: 'Algorithms Engineer', ranking: 0 },
|
||||||
'data-engineer': 'Data Engineer',
|
'android-engineer': { label: 'Android Software Engineer', ranking: 8 },
|
||||||
'data-scientist': 'Data Scientist',
|
'applications-engineer': { label: 'Applications Engineer', ranking: 0 },
|
||||||
'devops-engineer': 'DevOps Engineer',
|
'back-end-engineer': { label: 'Back End Engineer', ranking: 9 },
|
||||||
'engineering-director': 'Engineering Director',
|
'business-analyst': { label: 'Business Analyst', ranking: 0 },
|
||||||
'engineering-manager': 'Engineering Manager',
|
'business-engineer': { label: 'Business Engineer', ranking: 5 },
|
||||||
'enterprise-engineer': 'Enterprise Engineer',
|
'capacity-engineer': { label: 'Capacity Engineer', ranking: 0 },
|
||||||
'forward-deployed-engineer': 'Forward Deployed Engineer',
|
'customer-engineer': { label: 'Customer Engineer', ranking: 0 },
|
||||||
'front-end-engineer': 'Front End Engineer',
|
'data-analyst': { label: 'Data Analyst', ranking: 0 },
|
||||||
'full-stack-engineer': 'Full Stack Engineer',
|
'data-engineer': { label: 'Data Engineer', ranking: 0 },
|
||||||
'gameplay-engineer': 'Gameplay Engineer',
|
'data-scientist': { label: 'Data Scientist', ranking: 5 },
|
||||||
'hardware-engineer': 'Hardware Engineer',
|
'devops-engineer': { label: 'DevOps Engineer', ranking: 0 },
|
||||||
'infrastructure-engineer': 'Infrastructure Engineer',
|
'engineering-director': { label: 'Engineering Director', ranking: 0 },
|
||||||
'ios-engineer': 'iOS Software Engineer',
|
'engineering-manager': { label: 'Engineering Manager', ranking: 0 },
|
||||||
'machine-learning-engineer': 'Machine Learning (ML) Engineer',
|
'enterprise-engineer': { label: 'Enterprise Engineer', ranking: 0 },
|
||||||
'machine-learning-researcher': 'Machine Learning (ML) Researcher',
|
'forward-deployed-engineer': {
|
||||||
'mobile-engineer': 'Mobile Software Engineer (iOS + Android)',
|
label: 'Forward Deployed Engineer (FDE)',
|
||||||
'networks-engineer': 'Networks Engineer',
|
ranking: 0,
|
||||||
'partner-engineer': 'Partner Engineer',
|
},
|
||||||
'product-engineer': 'Product Engineer',
|
'front-end-engineer': { label: 'Front End Engineer', ranking: 9 },
|
||||||
'product-manager': 'Product Manager',
|
'full-stack-engineer': { label: 'Full Stack Engineer', ranking: 9 },
|
||||||
'production-engineer': 'Production Engineer',
|
'gameplay-engineer': { label: 'Gameplay Engineer', ranking: 0 },
|
||||||
'project-manager': 'Project Manager',
|
'hardware-engineer': { label: 'Hardware Engineer', ranking: 0 },
|
||||||
'release-engineer': 'Release Engineer',
|
'infrastructure-engineer': { label: 'Infrastructure Engineer', ranking: 0 },
|
||||||
'research-engineer': 'Research Engineer',
|
'ios-engineer': { label: 'iOS Software Engineer', ranking: 0 },
|
||||||
'research-scientist': 'Research Scientist',
|
'machine-learning-engineer': {
|
||||||
'rotational-engineer': 'Rotational Engineer',
|
label: 'Machine Learning (ML) Engineer',
|
||||||
'sales-engineer': 'Sales Engineer',
|
ranking: 5,
|
||||||
'security-engineer': 'Security Engineer',
|
},
|
||||||
'site-reliability-engineer': 'Site Reliability Engineer (SRE)',
|
'machine-learning-researcher': {
|
||||||
'software-engineer': 'Software Engineer',
|
label: 'Machine Learning (ML) Researcher',
|
||||||
'solutions-architect': 'Solutions Architect',
|
ranking: 0,
|
||||||
'solutions-engineer': 'Solutions Engineer',
|
},
|
||||||
'systems-analyst': 'Systems Analyst',
|
'mobile-engineer': {
|
||||||
'systems-engineer': 'Systems Engineer',
|
label: 'Mobile Software Engineer (iOS + Android)',
|
||||||
'tech-ops-engineer': 'Tech Ops Engineer',
|
ranking: 8,
|
||||||
'technical-program-manager': 'Technical Program Manager',
|
},
|
||||||
'test-engineer': 'QA/Test Engineer (SDET)',
|
'networks-engineer': { label: 'Networks Engineer', ranking: 0 },
|
||||||
'ux-engineer': 'User Experience (UX) Engineer',
|
'partner-engineer': { label: 'Partner Engineer', ranking: 0 },
|
||||||
|
'product-engineer': { label: 'Product Engineer', ranking: 7 },
|
||||||
|
'product-manager': { label: 'Product Manager', ranking: 0 },
|
||||||
|
'production-engineer': { label: 'Production Engineer', ranking: 8 },
|
||||||
|
'project-manager': { label: 'Project Manager', ranking: 0 },
|
||||||
|
'release-engineer': { label: 'Release Engineer', ranking: 0 },
|
||||||
|
'research-engineer': { label: 'Research Engineer', ranking: 6 },
|
||||||
|
'research-scientist': { label: 'Research Scientist', ranking: 7 },
|
||||||
|
'rotational-engineer': { label: 'Rotational Engineer', ranking: 0 },
|
||||||
|
'sales-engineer': { label: 'Sales Engineer', ranking: 0 },
|
||||||
|
'security-engineer': { label: 'Security Engineer', ranking: 7 },
|
||||||
|
'site-reliability-engineer': {
|
||||||
|
label: 'Site Reliability Engineer (SRE)',
|
||||||
|
ranking: 8,
|
||||||
|
},
|
||||||
|
'software-engineer': { label: 'Software Engineer', ranking: 10 },
|
||||||
|
'solutions-architect': { label: 'Solutions Architect', ranking: 0 },
|
||||||
|
'solutions-engineer': { label: 'Solutions Engineer', ranking: 0 },
|
||||||
|
'systems-analyst': { label: 'Systems Analyst', ranking: 0 },
|
||||||
|
'systems-engineer': { label: 'Systems Engineer', ranking: 0 },
|
||||||
|
'tech-ops-engineer': { label: 'Tech Ops Engineer', ranking: 0 },
|
||||||
|
'technical-program-manager': {
|
||||||
|
label: 'Technical Program Manager',
|
||||||
|
ranking: 0,
|
||||||
|
},
|
||||||
|
'test-engineer': { label: 'QA/Test Engineer (SDET)', ranking: 6 },
|
||||||
|
'ux-engineer': { label: 'User Experience (UX) Engineer', ranking: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JobTitleType = keyof typeof JobTitleLabels;
|
export type JobTitleType = keyof typeof JobTitleLabels;
|
||||||
|
|
||||||
export function getLabelForJobTitleType(jobTitle: JobTitleType): string {
|
export function getLabelForJobTitleType(jobTitle: JobTitleType): string {
|
||||||
return JobTitleLabels[jobTitle];
|
return JobTitleLabels[jobTitle].label;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
import Container from '~/components/shared/Container';
|
||||||
|
|
||||||
|
const people = [
|
||||||
|
{
|
||||||
|
bio: 'I like to play games so I treat life like a game.',
|
||||||
|
imageUrl: '/team/bryann.jpg',
|
||||||
|
name: 'Bryann Yeap',
|
||||||
|
role: 'Back End Engineer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bio: 'I am always up for sushi.',
|
||||||
|
imageUrl: '/team/ai-ling.jpg',
|
||||||
|
name: 'Hong Ai Ling',
|
||||||
|
role: 'Back End Engineer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bio: 'I love to watch football and code.',
|
||||||
|
imageUrl: '/team/stuart.jpg',
|
||||||
|
name: 'Stuart Long',
|
||||||
|
role: 'Front End Engineer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bio: 'Ziqing is a human who thrives under pressure, coffee and cat. In her own time, she likes playing the flute, building fun stuff with friends and watching animes.',
|
||||||
|
imageUrl: '/team/ziqing.jpg',
|
||||||
|
name: 'Zhang Ziqing',
|
||||||
|
role: 'Front End Engineer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div className="lg:py-18 bg-white py-12">
|
||||||
|
<Container variant="xs">
|
||||||
|
<div className="space-y-12">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
||||||
|
About Tech Offers Repo
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-slate-500">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||||
|
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||||
|
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||||
|
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
|
||||||
|
reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||||
|
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||||
|
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
|
Meet the Team
|
||||||
|
</h2>
|
||||||
|
<ul
|
||||||
|
className="grid grid-cols-2 space-y-12 md:grid-cols-1 md:items-start md:gap-x-8 md:gap-y-12 md:space-y-0"
|
||||||
|
role="list">
|
||||||
|
{people.map((person) => (
|
||||||
|
<li key={person.name}>
|
||||||
|
<div className="space-y-4 sm:grid sm:grid-cols-4 sm:gap-6 sm:space-y-0 lg:gap-8">
|
||||||
|
<div className="aspect-w-2 aspect-h-2 h-0">
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="rounded-lg object-cover shadow-lg"
|
||||||
|
src={person.imageUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-3">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1 text-lg font-medium leading-6">
|
||||||
|
<h3>{person.name}</h3>
|
||||||
|
<p className="text-primary-600">{person.role}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg">
|
||||||
|
<p className="text-slate-500">{person.bio}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
|
|
||||||
|
import { trpc } from '../trpc';
|
||||||
|
|
||||||
|
export function useAddQuestionToListAsync() {
|
||||||
|
const { event } = useGoogleAnalytics();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { mutateAsync: addQuestionToListAsync } = trpc.useMutation(
|
||||||
|
'questions.lists.createQuestionEntry',
|
||||||
|
{
|
||||||
|
// TODO: Add optimistic update
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.invalidateQueries(['questions.lists.getListsByUser']);
|
||||||
|
event({
|
||||||
|
action: 'questions.lists',
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'add question to list',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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 { event } = useGoogleAnalytics();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { mutateAsync: createListAsync } = trpc.useMutation(
|
||||||
|
'questions.lists.create',
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
// TODO: Add optimistic update
|
||||||
|
utils.invalidateQueries(['questions.lists.getListsByUser']);
|
||||||
|
event({
|
||||||
|
action: 'questions.lists',
|
||||||
|
category: 'engagement',
|
||||||
|
label: 'create list',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import type { Resume } from '~/types/resume';
|
||||||
|
|
||||||
|
export function resumeGetFilterCounts(data: Array<Resume>) {
|
||||||
|
const roleCounts: Record<string, number> = {};
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const { role } = data[i];
|
||||||
|
if (!(role in roleCounts)) {
|
||||||
|
roleCounts[role] = 1;
|
||||||
|
} else {
|
||||||
|
roleCounts[role]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const experienceCounts: Record<string, number> = {};
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const { experience } = data[i];
|
||||||
|
if (!(experience in experienceCounts)) {
|
||||||
|
experienceCounts[experience] = 1;
|
||||||
|
} else {
|
||||||
|
experienceCounts[experience]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationCounts: Record<string, number> = {};
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const { locationId } = data[i];
|
||||||
|
if (!(locationId in locationCounts)) {
|
||||||
|
locationCounts[locationId] = 1;
|
||||||
|
} else {
|
||||||
|
locationCounts[locationId]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
experience: experienceCounts,
|
||||||
|
location: locationCounts,
|
||||||
|
role: roleCounts,
|
||||||
|
};
|
||||||
|
}
|