[offers][feat] show company logo

pull/555/head
Yangshun Tay 2 years ago
parent c6941c0a5a
commit ada6a68420

@ -6,6 +6,7 @@ import {
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import { JobTypeLabel } from '~/components/offers/constants'; import { JobTypeLabel } from '~/components/offers/constants';
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
import type { JobTitleType } from '~/components/shared/JobTitles'; import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
@ -31,8 +32,13 @@ export default function DashboardProfileCard({
}: Props) { }: Props) {
return ( return (
<div className="px-4 py-4 sm:px-6"> <div className="px-4 py-4 sm:px-6">
<div className="flex items-end justify-between"> <div className="flex justify-between gap-4">
<div className="col-span-1 row-span-3"> <CompanyProfileImage
alt={company.name}
className="hidden h-10 w-10 object-contain sm:block"
src={company.logoUrl}
/>
<div className="grow">
<h4 className="font-medium"> <h4 className="font-medium">
{getLabelForJobTitleType(title as JobTitleType)}{' '} {getLabelForJobTitleType(title as JobTitleType)}{' '}
{jobType && <>({JobTypeLabel[jobType]})</>} {jobType && <>({JobTypeLabel[jobType]})</>}

@ -10,6 +10,7 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
import type { JobTitleType } from '~/components/shared/JobTitles'; import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
@ -89,8 +90,13 @@ export default function OfferProfileCard({
function BottomSection() { function BottomSection() {
return ( return (
<div className="px-4 py-4 sm:px-6"> <div className="px-4 py-4 sm:px-6">
<div className="flex items-end justify-between"> <div className="flex justify-between gap-4">
<div className="col-span-1 row-span-3"> <CompanyProfileImage
alt={company.name}
className="hidden h-10 w-10 object-contain sm:block"
src={company.logoUrl}
/>
<div className="grow">
<h4 className="font-medium"> <h4 className="font-medium">
{getLabelForJobTitleType(title as JobTitleType)}{' '} {getLabelForJobTitleType(title as JobTitleType)}{' '}
{jobType && <>({JobTypeLabel[jobType]})</>} {jobType && <>({JobTypeLabel[jobType]})</>}
@ -125,7 +131,7 @@ export default function OfferProfileCard({
)} )}
</div> </div>
</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"> <p className="text-end text-lg font-medium leading-6 text-slate-900">
{jobType === JobType.FULLTIME {jobType === JobType.FULLTIME
? `${convertMoneyToString(income)} / year` ? `${convertMoneyToString(income)} / year`

@ -9,6 +9,7 @@ import { JobType } from '@prisma/client';
import { JobTypeLabel } from '~/components/offers/constants'; import { JobTypeLabel } from '~/components/offers/constants';
import { InternshipCycleValuesToLabels } from '~/components/offers/InternshipCycles'; import { InternshipCycleValuesToLabels } from '~/components/offers/InternshipCycles';
import type { OfferDisplayData } from '~/components/offers/types'; import type { OfferDisplayData } from '~/components/offers/types';
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
import { getLocationDisplayText } from '~/utils/offers/string'; import { getLocationDisplayText } from '~/utils/offers/string';
import { getDurationDisplayText } from '~/utils/offers/time'; import { getDurationDisplayText } from '~/utils/offers/time';
@ -21,7 +22,7 @@ export default function OfferCard({
offer: { offer: {
base, base,
bonus, bonus,
companyName, company,
duration, duration,
internshipCycle, internshipCycle,
jobTitle, jobTitle,
@ -40,19 +41,26 @@ export default function OfferCard({
function UpperSection() { function UpperSection() {
return ( return (
<div className="px-4 py-5 sm:px-6"> <div className="px-4 py-5 sm:px-6">
<div className="flex justify-between"> <div className="flex justify-between gap-4">
<div> {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"> <h3 className="text-lg font-medium leading-6 text-slate-900">
{jobTitle} {jobType && <>({JobTypeLabel[jobType]})</>} {jobTitle} {jobType && <>({JobTypeLabel[jobType]})</>}
</h3> </h3>
<div className="mt-1 flex flex-row flex-wrap sm:mt-0"> <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"> <div className="mr-4 mt-2 flex items-center text-sm text-slate-500">
<BuildingOfficeIcon <BuildingOfficeIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/> />
{companyName} {company?.name}
</div> </div>
)} )}
{location && ( {location && (

@ -34,11 +34,22 @@ function ProfileOffers({ offers }: ProfileOffersProps) {
} }
return ( return (
<div className="space-y-4 p-4"> <div className="p-4">
<div className="space-y-4">
{offers.map((offer) => ( {offers.map((offer) => (
<OfferCard key={offer.id} offer={offer} /> <OfferCard key={offer.id} offer={offer} />
))} ))}
</div> </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>
)} )}
<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> </div>
); );
} }
@ -188,5 +208,6 @@ export default function ProfileDetails({
/> />
); );
} }
return null; return null;
} }

@ -243,7 +243,7 @@ export default function ProfileHeader({
<h2 className="flex text-2xl font-bold"> <h2 className="flex text-2xl font-bold">
{profileName ?? 'anonymous'} {profileName ?? 'anonymous'}
</h2> </h2>
{(experiences[0]?.companyName || {(experiences[0]?.company?.name ||
experiences[0]?.jobLevel || experiences[0]?.jobLevel ||
experiences[0]?.jobTitle) && ( experiences[0]?.jobTitle) && (
<div className="flex items-center text-sm text-slate-600"> <div className="flex items-center text-sm text-slate-600">
@ -252,7 +252,7 @@ export default function ProfileHeader({
</span> </span>
<p> <p>
<span className="mr-2 font-bold">Current:</span> <span className="mr-2 font-bold">Current:</span>
{`${experiences[0].companyName || ''} ${ {`${experiences[0].company?.name || ''} ${
experiences[0].jobLevel || '' experiences[0].jobLevel || ''
} ${experiences[0].jobTitle || ''} ${ } ${experiences[0].jobTitle || ''} ${
experiences[0].jobType experiences[0].jobType

@ -2,6 +2,7 @@ import clsx from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import CompanyProfileImage from '~/components/shared/CompanyProfileImage';
import type { JobTitleType } from '~/components/shared/JobTitles'; import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
@ -34,11 +35,20 @@ export default function OfferTableRow({
}: OfferTableRowProps) { }: OfferTableRowProps) {
return ( return (
<tr key={id} className="divide-x divide-slate-200 border-b bg-white"> <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"> <td className="flex items-center gap-3 space-y-0.5 py-2 px-4" scope="row">
<div className="font-medium">{company.name}</div> <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"> <div className="text-xs text-slate-500">
{location.cityName} ({location.countryCode}) {location.cityName} ({location.countryCode})
</div> </div>
</div>
</td> </td>
<td className="py-2 px-4"> <td className="py-2 px-4">
{getLabelForJobTitleType(title as JobTitleType)} {getLabelForJobTitleType(title as JobTitleType)}

@ -2,6 +2,8 @@ import type { JobType } from '@prisma/client';
import type { MonthYear } from '~/components/shared/MonthYearPicker'; import type { MonthYear } from '~/components/shared/MonthYearPicker';
import type { OffersCompany } from '../../types/offers';
import type { Location } from '~/types/offers'; import type { Location } from '~/types/offers';
/** /**
@ -177,7 +179,7 @@ export type EducationDisplayData = {
export type OfferDisplayData = { export type OfferDisplayData = {
base?: string | null; base?: string | null;
bonus?: string | null; bonus?: string | null;
companyName?: string | null; company?: OffersCompany | null;
duration?: number | null; duration?: number | null;
id?: string; id?: string;
internshipCycle?: string; internshipCycle?: string;

@ -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 && ( {!userProfilesQuery.isLoading && (
<div className="overflow-y-auto py-8"> <div className="overflow-y-auto py-8">
<h1 className="mx-auto mb-4 text-start text-4xl font-bold text-slate-900"> <h1 className="mx-auto mb-4 text-start text-4xl font-bold text-slate-900">
My dashboard Dashboard
</h1> </h1>
<p className="mt-4 text-xl leading-8 text-slate-500"> <p className="mt-4 text-xl leading-8 text-slate-500">
Save your offer profiles to your dashboard to easily access and Save offer profiles to your dashboard to easily access and edit
edit them later. them later.
</p> </p>
<div className="mt-8 flex justify-center"> <div className="mt-8">
<ul className="w-full space-y-4" role="list"> <ul className="w-full space-y-4" role="list">
{userProfiles?.map((profile) => ( {userProfiles?.map((profile) => (
<li key={profile.id}> <li key={profile.id}>
@ -99,6 +99,15 @@ export default function ProfilesDashboard() {
</li> </li>
))} ))}
</ul> </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>
</div> </div>
)} )}

@ -186,6 +186,15 @@ export default function OffersHomePage({
selectedSortType={selectedSortType} selectedSortType={selectedSortType}
onSort={onSort} 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> </Container>
</main> </main>
</> </>

@ -74,7 +74,7 @@ export default function OfferProfile() {
res.offersFullTime.bonus != null res.offersFullTime.bonus != null
? convertMoneyToString(res.offersFullTime.bonus) ? convertMoneyToString(res.offersFullTime.bonus)
: undefined, : undefined,
companyName: res.company.name, company: res.company,
id: res.offersFullTime.id, id: res.offersFullTime.id,
jobLevel: res.offersFullTime.level, jobLevel: res.offersFullTime.level,
jobTitle: getLabelForJobTitleType( jobTitle: getLabelForJobTitleType(
@ -96,7 +96,7 @@ export default function OfferProfile() {
return filteredOffer; return filteredOffer;
} }
const filteredOffer: OfferDisplayData = { const filteredOffer: OfferDisplayData = {
companyName: res.company.name, company: res.company,
id: res.offersIntern!.id, id: res.offersIntern!.id,
internshipCycle: res.offersIntern!.internshipCycle, internshipCycle: res.offersIntern!.internshipCycle,
jobTitle: getLabelForJobTitleType( jobTitle: getLabelForJobTitleType(
@ -130,7 +130,7 @@ export default function OfferProfile() {
})), })),
experiences: data.background.experiences.map( experiences: data.background.experiences.map(
(experience): OfferDisplayData => ({ (experience): OfferDisplayData => ({
companyName: experience.company?.name, company: experience.company,
duration: experience.durationInMonths, duration: experience.durationInMonths,
jobLevel: experience.level, jobLevel: experience.level,
jobTitle: experience.title jobTitle: experience.title
@ -197,10 +197,10 @@ export default function OfferProfile() {
<Error statusCode={404} title="Requested profile does not exist." /> <Error statusCode={404} title="Requested profile does not exist." />
</div> </div>
) : getProfileQuery.isLoading ? ( ) : getProfileQuery.isLoading ? (
<div className="flex h-screen w-screen"> <div className="flex h-screen w-full items-center justify-center text-slate-500">
<div className="m-auto mx-auto w-screen justify-center font-medium text-slate-500"> <div className="flex flex-col gap-2">
<Spinner display="block" size="lg" /> <Spinner display="block" size="lg" />
<div className="text-center">Loading profile...</div> <p className="text-center">Loading profile...</p>
</div> </div>
</div> </div>
) : ( ) : (

@ -141,6 +141,15 @@ export default function OffersSubmissionResult() {
</div> </div>
)} )}
</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> </div>
</div> </div>

Loading…
Cancel
Save