mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-02-02 18:21:37 +08:00
[offers][feat] show company logo
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
import { JobType } from '@prisma/client';
|
||||
|
||||
import { JobTypeLabel } from '~/components/offers/constants';
|
||||
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
|
||||
@@ -31,8 +32,13 @@ export default function DashboardProfileCard({
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="px-4 py-4 sm:px-6">
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="col-span-1 row-span-3">
|
||||
<div className="flex justify-between gap-4">
|
||||
<CompanyProfileImage
|
||||
alt={company.name}
|
||||
className="hidden h-10 w-10 object-contain sm:block"
|
||||
src={company.logoUrl}
|
||||
/>
|
||||
<div className="grow">
|
||||
<h4 className="font-medium">
|
||||
{getLabelForJobTitleType(title as JobTitleType)}{' '}
|
||||
{jobType && <>({JobTypeLabel[jobType]})</>}
|
||||
@@ -80,4 +86,4 @@ export default function DashboardProfileCard({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { JobType } from '@prisma/client';
|
||||
|
||||
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
|
||||
@@ -89,8 +90,13 @@ export default function OfferProfileCard({
|
||||
function BottomSection() {
|
||||
return (
|
||||
<div className="px-4 py-4 sm:px-6">
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="col-span-1 row-span-3">
|
||||
<div className="flex justify-between gap-4">
|
||||
<CompanyProfileImage
|
||||
alt={company.name}
|
||||
className="hidden h-10 w-10 object-contain sm:block"
|
||||
src={company.logoUrl}
|
||||
/>
|
||||
<div className="grow">
|
||||
<h4 className="font-medium">
|
||||
{getLabelForJobTitleType(title as JobTitleType)}{' '}
|
||||
{jobType && <>({JobTypeLabel[jobType]})</>}
|
||||
@@ -125,7 +131,7 @@ export default function OfferProfileCard({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 row-span-3">
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="text-end text-lg font-medium leading-6 text-slate-900">
|
||||
{jobType === JobType.FULLTIME
|
||||
? `${convertMoneyToString(income)} / year`
|
||||
|
||||
@@ -9,6 +9,7 @@ import { JobType } from '@prisma/client';
|
||||
import { JobTypeLabel } from '~/components/offers/constants';
|
||||
import { InternshipCycleValuesToLabels } from '~/components/offers/InternshipCycles';
|
||||
import type { OfferDisplayData } from '~/components/offers/types';
|
||||
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
|
||||
|
||||
import { getLocationDisplayText } from '~/utils/offers/string';
|
||||
import { getDurationDisplayText } from '~/utils/offers/time';
|
||||
@@ -21,7 +22,7 @@ export default function OfferCard({
|
||||
offer: {
|
||||
base,
|
||||
bonus,
|
||||
companyName,
|
||||
company,
|
||||
duration,
|
||||
internshipCycle,
|
||||
jobTitle,
|
||||
@@ -40,19 +41,26 @@ export default function OfferCard({
|
||||
function UpperSection() {
|
||||
return (
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<div className="flex justify-between gap-4">
|
||||
{company && (
|
||||
<CompanyProfileImage
|
||||
alt={company.name}
|
||||
className="h-10 w-10 object-contain"
|
||||
src={company.logoUrl}
|
||||
/>
|
||||
)}
|
||||
<div className="grow">
|
||||
<h3 className="text-lg font-medium leading-6 text-slate-900">
|
||||
{jobTitle} {jobType && <>({JobTypeLabel[jobType]})</>}
|
||||
</h3>
|
||||
<div className="mt-1 flex flex-row flex-wrap sm:mt-0">
|
||||
{companyName && (
|
||||
{company?.name != null && (
|
||||
<div className="mr-4 mt-2 flex items-center text-sm text-slate-500">
|
||||
<BuildingOfficeIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{companyName}
|
||||
{company?.name}
|
||||
</div>
|
||||
)}
|
||||
{location && (
|
||||
|
||||
@@ -34,10 +34,21 @@ function ProfileOffers({ offers }: ProfileOffersProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
{offers.map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
{offers.map((offer) => (
|
||||
<OfferCard key={offer.id} offer={offer} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-1 text-end">
|
||||
<a
|
||||
className="text-xs text-slate-500"
|
||||
href="https://clearbit.com"
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
Logos provided by Clearbit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -140,6 +151,15 @@ function ProfileAnalysis({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-end">
|
||||
<a
|
||||
className="text-xs text-slate-500"
|
||||
href="https://clearbit.com"
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
Logos provided by Clearbit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -188,5 +208,6 @@ export default function ProfileDetails({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ export default function ProfileHeader({
|
||||
<h2 className="flex text-2xl font-bold">
|
||||
{profileName ?? 'anonymous'}
|
||||
</h2>
|
||||
{(experiences[0]?.companyName ||
|
||||
{(experiences[0]?.company?.name ||
|
||||
experiences[0]?.jobLevel ||
|
||||
experiences[0]?.jobTitle) && (
|
||||
<div className="flex items-center text-sm text-slate-600">
|
||||
@@ -252,7 +252,7 @@ export default function ProfileHeader({
|
||||
</span>
|
||||
<p>
|
||||
<span className="mr-2 font-bold">Current:</span>
|
||||
{`${experiences[0].companyName || ''} ${
|
||||
{`${experiences[0].company?.name || ''} ${
|
||||
experiences[0].jobLevel || ''
|
||||
} ${experiences[0].jobTitle || ''} ${
|
||||
experiences[0].jobType
|
||||
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import { JobType } from '@prisma/client';
|
||||
|
||||
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
|
||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||
|
||||
@@ -34,10 +35,19 @@ export default function OfferTableRow({
|
||||
}: OfferTableRowProps) {
|
||||
return (
|
||||
<tr key={id} className="divide-x divide-slate-200 border-b bg-white">
|
||||
<td className="space-y-0.5 py-2 px-4" scope="row">
|
||||
<div className="font-medium">{company.name}</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{location.cityName} ({location.countryCode})
|
||||
<td className="flex items-center gap-3 space-y-0.5 py-2 px-4" scope="row">
|
||||
<CompanyProfileImage
|
||||
alt={company.name}
|
||||
className="hidden h-6 w-6 object-contain sm:block"
|
||||
src={company.logoUrl}
|
||||
/>
|
||||
<div>
|
||||
<div className="line-clamp-2 sm:line-clamp-1 font-medium">
|
||||
{company.name}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{location.cityName} ({location.countryCode})
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { JobType } from '@prisma/client';
|
||||
|
||||
import type { MonthYear } from '~/components/shared/MonthYearPicker';
|
||||
|
||||
import type { OffersCompany } from '../../types/offers';
|
||||
|
||||
import type { Location } from '~/types/offers';
|
||||
|
||||
/**
|
||||
@@ -177,7 +179,7 @@ export type EducationDisplayData = {
|
||||
export type OfferDisplayData = {
|
||||
base?: string | null;
|
||||
bonus?: string | null;
|
||||
companyName?: string | null;
|
||||
company?: OffersCompany | null;
|
||||
duration?: number | null;
|
||||
id?: string;
|
||||
internshipCycle?: string;
|
||||
|
||||
31
apps/portal/src/components/shared/CompanyProfileImage.tsx
Normal file
31
apps/portal/src/components/shared/CompanyProfileImage.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { BuildingOffice2Icon } from '@heroicons/react/24/outline';
|
||||
type Props = Readonly<{
|
||||
alt: string;
|
||||
className: string;
|
||||
src: string;
|
||||
}>;
|
||||
|
||||
export default function CompanyProfileImage({ alt, className, src }: Props) {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
return hasError ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'shrink-0 rounded bg-slate-50 p-0.5 text-slate-400',
|
||||
className,
|
||||
)}>
|
||||
<BuildingOffice2Icon />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
alt={alt}
|
||||
className={clsx('object-contain', className)}
|
||||
src={src}
|
||||
onError={() => {
|
||||
setHasError(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -85,13 +85,13 @@ export default function ProfilesDashboard() {
|
||||
{!userProfilesQuery.isLoading && (
|
||||
<div className="overflow-y-auto py-8">
|
||||
<h1 className="mx-auto mb-4 text-start text-4xl font-bold text-slate-900">
|
||||
My dashboard
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="mt-4 text-xl leading-8 text-slate-500">
|
||||
Save your offer profiles to your dashboard to easily access and
|
||||
edit them later.
|
||||
Save offer profiles to your dashboard to easily access and edit
|
||||
them later.
|
||||
</p>
|
||||
<div className="mt-8 flex justify-center">
|
||||
<div className="mt-8">
|
||||
<ul className="w-full space-y-4" role="list">
|
||||
{userProfiles?.map((profile) => (
|
||||
<li key={profile.id}>
|
||||
@@ -99,6 +99,15 @@ export default function ProfilesDashboard() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-2 text-end">
|
||||
<a
|
||||
className="text-xs text-slate-500"
|
||||
href="https://clearbit.com"
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
Logos provided by Clearbit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -186,6 +186,15 @@ export default function OffersHomePage({
|
||||
selectedSortType={selectedSortType}
|
||||
onSort={onSort}
|
||||
/>
|
||||
<div className="mt-1 text-end">
|
||||
<a
|
||||
className="text-xs text-slate-500"
|
||||
href="https://clearbit.com"
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
Logos provided by Clearbit
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
</main>
|
||||
</>
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function OfferProfile() {
|
||||
res.offersFullTime.bonus != null
|
||||
? convertMoneyToString(res.offersFullTime.bonus)
|
||||
: undefined,
|
||||
companyName: res.company.name,
|
||||
company: res.company,
|
||||
id: res.offersFullTime.id,
|
||||
jobLevel: res.offersFullTime.level,
|
||||
jobTitle: getLabelForJobTitleType(
|
||||
@@ -96,7 +96,7 @@ export default function OfferProfile() {
|
||||
return filteredOffer;
|
||||
}
|
||||
const filteredOffer: OfferDisplayData = {
|
||||
companyName: res.company.name,
|
||||
company: res.company,
|
||||
id: res.offersIntern!.id,
|
||||
internshipCycle: res.offersIntern!.internshipCycle,
|
||||
jobTitle: getLabelForJobTitleType(
|
||||
@@ -130,7 +130,7 @@ export default function OfferProfile() {
|
||||
})),
|
||||
experiences: data.background.experiences.map(
|
||||
(experience): OfferDisplayData => ({
|
||||
companyName: experience.company?.name,
|
||||
company: experience.company,
|
||||
duration: experience.durationInMonths,
|
||||
jobLevel: experience.level,
|
||||
jobTitle: experience.title
|
||||
@@ -197,10 +197,10 @@ export default function OfferProfile() {
|
||||
<Error statusCode={404} title="Requested profile does not exist." />
|
||||
</div>
|
||||
) : getProfileQuery.isLoading ? (
|
||||
<div className="flex h-screen w-screen">
|
||||
<div className="m-auto mx-auto w-screen justify-center font-medium text-slate-500">
|
||||
<div className="flex h-screen w-full items-center justify-center text-slate-500">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Spinner display="block" size="lg" />
|
||||
<div className="text-center">Loading profile...</div>
|
||||
<p className="text-center">Loading profile...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -141,6 +141,15 @@ export default function OffersSubmissionResult() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 py-2 text-end sm:px-10">
|
||||
<a
|
||||
className="text-xs text-slate-500"
|
||||
href="https://clearbit.com"
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
Logos provided by Clearbit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user