Skip to content

Commit db52d83

Browse files
authored
minor ui fixes (#104)
1 parent cfaa9bd commit db52d83

File tree

15 files changed

+147
-32
lines changed

15 files changed

+147
-32
lines changed

frontend/src/app/jobs/[id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default async function JobDetailPage({ params }: PageProps) {
2525
return (
2626
<FilterProvider>
2727
<div className="h-screen max-w-4xl mx-auto overflow-hidden flex flex-col">
28-
<div className="flex-grow overflow-y-auto mb-20 pb-12 mt-2">
28+
<div className="overflow-y-auto h-[calc(100svh-120px)] lg:h-[calc(100svh-180px)]">
2929
<JobDetailsWrapper job={job} />
3030
</div>
3131
</div>

frontend/src/app/jobs/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default async function JobsPage({
3838
</Suspense>
3939
</div>
4040

41-
<div className="hidden lg:block lg:w-[65%] overflow-y-auto h-[calc(100svh-150px)] lg:h-[calc(100svh-180px)]">
41+
<div className="hidden lg:block lg:w-[65%] overflow-y-auto h-[calc(100svh-140px)] lg:h-[calc(100svh-180px)]">
4242
<Suspense fallback={<JobDetailsLoading />}>
4343
<JobDetails />
4444
</Suspense>
Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,95 @@
1-
// frontend/src/components/ui/company-logo.tsx
1+
// frontend/src/components/jobs/company-logo.tsx
22
import { Image } from "@mantine/core";
3+
import { useState } from "react";
34

45
interface CompanyLogoProps {
56
name: string;
67
logo?: string;
8+
applicationUrl?: string;
79
className?: string;
810
}
911

1012
export default function CompanyLogo({
1113
name,
1214
logo,
15+
applicationUrl,
1316
className = "",
1417
}: CompanyLogoProps) {
1518
const baseClasses = "object-contain rounded-md bg-white";
19+
const [logoFailed, setLogoFailed] = useState(false);
20+
const [clearbitFailed, setClearbitFailed] = useState(false);
1621

17-
if (logo) {
22+
// Function to extract base domain from URL
23+
const extractBaseDomain = (url: string): string => {
24+
try {
25+
const urlObj = new URL(url);
26+
return urlObj.hostname;
27+
} catch {
28+
return "";
29+
}
30+
};
31+
32+
// Check if URL should be skipped for logo generation
33+
const shouldSkipUrl = (url: string): boolean => {
34+
const skipDomains = [
35+
"bit.ly",
36+
"tinyurl.com",
37+
"goo.gl",
38+
"gradconnection.com",
39+
"gradconnect.com",
40+
"prosple.com",
41+
"linkedin.com",
42+
"lnkd.in",
43+
"surveymonkey.com",
44+
"forms.gle",
45+
];
46+
47+
try {
48+
const domain = new URL(url).hostname;
49+
return skipDomains.some((skipDomain) => domain.includes(skipDomain));
50+
} catch {
51+
return true;
52+
}
53+
};
54+
55+
// Render the fallback with first letter or question mark
56+
const renderFallback = () => {
57+
return (
58+
<div
59+
className={`${baseClasses} flex items-center justify-center ${className}`}
60+
>
61+
<div>{name ? name.charAt(0).toUpperCase() : "?"}</div>
62+
</div>
63+
);
64+
};
65+
66+
// Case 1: Try company logo first
67+
if (logo && !logoFailed) {
1868
return (
19-
<Image alt={name} src={logo} className={`${baseClasses} ${className}`} />
69+
<Image
70+
alt={name || "Company"}
71+
src={logo}
72+
className={`${baseClasses} ${className}`}
73+
onError={() => setLogoFailed(true)}
74+
/>
2075
);
2176
}
2277

23-
return (
24-
<div
25-
className={`${baseClasses} flex items-center justify-center ${className}`}
26-
>
27-
<div>{name.charAt(0).toUpperCase()}</div>
28-
</div>
29-
);
78+
// Case 2: Try Clearbit logo from application URL
79+
if (applicationUrl && !shouldSkipUrl(applicationUrl) && !clearbitFailed) {
80+
const domain = extractBaseDomain(applicationUrl);
81+
if (domain) {
82+
return (
83+
<Image
84+
alt={name || "Company"}
85+
src={`https://logo.clearbit.com/${domain}`}
86+
className={`${baseClasses} ${className}`}
87+
onError={() => setClearbitFailed(true)}
88+
/>
89+
);
90+
}
91+
}
92+
93+
// Case 3 & 4: Fallback to initial letter or question mark
94+
return renderFallback();
3095
}

frontend/src/components/jobs/job-card.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default function JobCard({ job, isSelected }: JobCardProps) {
2828
<div className={"flex flex-1 min-w-0"}>
2929
<CompanyLogo
3030
name={job.company.name}
31+
applicationUrl={job.application_url}
3132
logo={job.company.logo}
3233
className="mr-2 lg:h-14 lg:w-14 h-10 w-10"
3334
/>
@@ -36,7 +37,7 @@ export default function JobCard({ job, isSelected }: JobCardProps) {
3637
"flex justify-center flex-col flex-1 min-w-0 space-y-0.5"
3738
}
3839
>
39-
<span className="text-md font-bold line-clamp-2 leading-tight pr-2">
40+
<span className="text-sm lg:text-md font-bold line-clamp-2 leading-tight pr-2">
4041
{job.title}
4142
</span>
4243
<span className="text-xs truncate">{job.company.name}</span>
@@ -50,7 +51,7 @@ export default function JobCard({ job, isSelected }: JobCardProps) {
5051
dangerouslySetInnerHTML={{
5152
__html: DOMPurify.sanitize(washedDescription),
5253
}}
53-
className="text-xs line-clamp-2 lg:line-clamp-3 mb-2"
54+
className="text-xs line-clamp-3 lg:line-clamp-3 mb-2"
5455
/>
5556
</div>
5657

@@ -71,6 +72,7 @@ export default function JobCard({ job, isSelected }: JobCardProps) {
7172
)}
7273
{job.locations && job.locations.length > 0 && (
7374
<Badge
75+
className={"hidden lg:inline"}
7476
text={`${job.locations
7577
.slice(0, 2)
7678
.map((loc) => formatCapString(loc))

frontend/src/components/jobs/job-description.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import SectionHeading from "@/components/ui/section-heading";
33
import { TypographyStylesProvider } from "@mantine/core";
44
import DOMPurify from "isomorphic-dompurify";
5+
import { IconBook } from "@tabler/icons-react";
56

67
interface JobDescriptionProps {
78
description: string;
@@ -10,7 +11,10 @@ interface JobDescriptionProps {
1011
export default function JobDescription({ description }: JobDescriptionProps) {
1112
return (
1213
<div className="flex flex-col mt-4">
13-
<SectionHeading title="Job Description" />
14+
<SectionHeading
15+
title="Job Description"
16+
icon={<IconBook size={16} stroke={1.5} />}
17+
/>
1418
<TypographyStylesProvider>
1519
<div
1620
dangerouslySetInnerHTML={{

frontend/src/components/jobs/job-details.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useFilterContext } from "@/context/filter/filter-context";
77
import JobDescription from "@/components/jobs/job-description";
88
import JobHeader from "@/components/jobs/job-header";
99
import JobDetailsLoading from "@/components/layout/job-details-loading";
10+
import JobSummary from "@/components/jobs/job-summary";
1011

1112
export default function JobDetails() {
1213
const { selectedJob, isLoading } = useFilterContext();
@@ -64,7 +65,12 @@ export default function JobDetails() {
6465
viewportRef={scrollRef}
6566
>
6667
<JobHeader job={selectedJob} />
67-
<JobDescription description={selectedJob.description || ""} />
68+
{selectedJob && selectedJob.one_liner && (
69+
<JobSummary one_liner={selectedJob.one_liner} />
70+
)}
71+
{selectedJob && selectedJob.description && (
72+
<JobDescription description={selectedJob.description || ""} />
73+
)}
6874
</ScrollArea>
6975

7076
<div className="flex justify-between items-center mt-4 gap-4">

frontend/src/components/jobs/job-header.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,38 +38,38 @@ export default function JobHeader({ job }: JobHeaderProps) {
3838
<div className="flex flex-wrap gap-3">
3939
{/* Location */}
4040
<InfoTag
41-
icon={<IconMapPin size={18} stroke={1.5} />}
41+
icon={<IconMapPin size={16} stroke={1.5} />}
4242
text={job.locations
4343
?.map((location) => formatCapString(location))
4444
.join(", ")}
4545
/>
4646

4747
{/* Date found */}
4848
<InfoTag
49-
icon={<IconCalendar size={18} stroke={1.5} />}
49+
icon={<IconCalendar size={16} stroke={1.5} />}
5050
text={`Found ${getTimeAgo(job.created_at)}`}
5151
/>
5252

5353
{/* Role type */}
5454
{job.type && (
5555
<InfoTag
56-
icon={<IconBriefcase size={18} stroke={1.5} />}
56+
icon={<IconBriefcase size={16} stroke={1.5} />}
5757
text={`${formatCapString(job.type)} Role`}
5858
/>
5959
)}
6060

6161
{/* Industry field */}
6262
{job.industry_field && (
6363
<InfoTag
64-
icon={<IconBuilding size={18} stroke={1.5} />}
64+
icon={<IconBuilding size={16} stroke={1.5} />}
6565
text={formatCapString(job.industry_field)}
6666
/>
6767
)}
6868

6969
{/* Working Rights */}
7070
{job.working_rights && (
7171
<InfoTag
72-
icon={<IconId size={18} stroke={1.5} />}
72+
icon={<IconId size={16} stroke={1.5} />}
7373
text={formatWorkingRights(job.working_rights)}
7474
/>
7575
)}
@@ -79,6 +79,7 @@ export default function JobHeader({ job }: JobHeaderProps) {
7979
{/* Company logo with responsive sizing */}
8080
<CompanyLogo
8181
name={job.company.name}
82+
applicationUrl={job.application_url}
8283
logo={job.company.logo}
8384
className="aspect-square h-12 w-12 lg:h-16 lg:w-16"
8485
/>

frontend/src/components/jobs/job-list.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { Modal, ScrollArea } from "@mantine/core";
99
import JobDetails from "@/components/jobs/job-details";
1010
import JobListLoading from "@/components/layout/job-list-loading";
1111
import JobPagination from "@/components/jobs/job-pagination";
12+
import { useMediaQuery } from "@mantine/hooks";
1213

1314
export default function JobList({ jobs }: { jobs: Job[] }) {
1415
const { selectedJob, setSelectedJob, isLoading } = useFilterContext();
1516
const [isModalOpen, setIsModalOpen] = useState(false);
17+
const isDesktop = useMediaQuery("(min-width: 1024px)");
1618

1719
useEffect(() => {
1820
// Effect to set initial selection when jobs change and no job is selected
@@ -25,10 +27,19 @@ export default function JobList({ jobs }: { jobs: Job[] }) {
2527

2628
return (
2729
<>
30+
{/* This is a workaround to ensure mobile job cards are unaffected by the scrollbar. */}
2831
<ScrollArea
29-
offsetScrollbars
30-
className={"h-[calc(100svh-150px)] lg:h-[calc(100svh-180px)]"}
32+
className={"h-[calc(100svh-140px)] lg:h-[calc(100svh-180px)]"}
3133
type="auto"
34+
scrollbarSize={isDesktop ? undefined : 2}
35+
offsetScrollbars={isDesktop}
36+
classNames={
37+
isDesktop
38+
? {
39+
scrollbar: "absolute right-[-2px] lg:right-0",
40+
}
41+
: undefined
42+
}
3243
>
3344
<div className="space-y-4 pr-1">
3445
{jobs.map((job) => (
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// frontend/src/components/jobs/details/sections/job-description.tsx
2+
import SectionHeading from "@/components/ui/section-heading";
3+
import { IconRobot } from "@tabler/icons-react";
4+
5+
interface JobSummaryProps {
6+
one_liner?: string;
7+
}
8+
9+
export default function JobSummary({ one_liner }: JobSummaryProps) {
10+
return (
11+
<div className="flex flex-col mt-4">
12+
<SectionHeading
13+
icon={<IconRobot size={16} stroke={1.5} />}
14+
title="Summary"
15+
/>
16+
<span className={"prose prose-invert lg:ml-6 leading-relaxed text-xs"}>
17+
{one_liner}
18+
</span>
19+
</div>
20+
);
21+
}

frontend/src/components/layout/job-list-loading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ScrollArea } from "@mantine/core";
33
export default function JobListLoading() {
44
return (
55
<ScrollArea
6-
className="h-[calc(100svh-150px)] lg:h-[calc(100svh-180px)]"
6+
className="h-[calc(100svh-140px)] lg:h-[calc(100svh-180px)]"
77
type="never"
88
offsetScrollbars
99
>

0 commit comments

Comments
 (0)