Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions frontend/src/domains/new_dashboard/NewDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AddJobModal } from "./components/jobs/AddJobModal";
import { JobDetailModal } from "./components/jobs/JobDetailModal";
import { JobTab } from "./components/jobs/JobTab";
import { Header } from "./components/layout/Header";
import { MobileTabBar } from "./components/layout/MobileTabBar";
import { Sidebar } from "./components/layout/Sidebar";
import { MentoringTab } from "./components/mentoring/MentoringTab";
import { ProfileTab } from "./components/profile/ProfileTab";
Expand Down Expand Up @@ -180,6 +181,20 @@ export default function NewDashboardPage() {
saveUserProfile,
saveSearchPreferences,
} = useUserDashboardData(user, { onError: showToast });
const preferredModelFilter = useMemo(
() => getModelFilterFromJobTypes(searchPreferences.jobTypes),
[searchPreferences.jobTypes],
);
const initialRecommendationSearch = useMemo(
() =>
isLoadingUserData
? null
: {
keywords: [],
filters: modelFilterToApiFilter(preferredModelFilter),
},
[isLoadingUserData, preferredModelFilter],
);
const {
trackedJobs,
recommendedJobs,
Expand All @@ -190,7 +205,10 @@ export default function NewDashboardPage() {
changeJobStatus,
changeJobNotesLocally,
saveJobNotes,
} = useDashboardJobs(user, { onError: showToast });
} = useDashboardJobs(user, {
onError: showToast,
initialRecommendationSearch,
});

const matchedTrackedJobs = useMemo(
() =>
Expand All @@ -213,10 +231,6 @@ export default function NewDashboardPage() {
),
[filterType, matchedRecommendedJobs],
);
const preferredModelFilter = useMemo(
() => getModelFilterFromJobTypes(searchPreferences.jobTypes),
[searchPreferences.jobTypes],
);
const showPreferenceNotice =
!hasUserChangedJobFilters &&
preferredModelFilter !== "Todos" &&
Expand Down Expand Up @@ -527,11 +541,13 @@ export default function NewDashboardPage() {
<div className="flex min-w-0 flex-1 flex-col">
<Header userProfile={userProfile} />

<main className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden bg-background">
<main className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden bg-background pb-[calc(4.5rem+env(safe-area-inset-bottom))] lg:pb-0">
{renderContent()}
</main>
</div>

<MobileTabBar />

{selectedJob ? (
<JobDetailModal
job={selectedJob}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Fragment, type ReactNode } from "react";

interface FormattedJobDescriptionProps {
description: string;
}

const discardedElements = new Set([
"button",
"canvas",
"embed",
"form",
"iframe",
"img",
"input",
"link",
"meta",
"noscript",
"object",
"script",
"style",
"svg",
]);

function decodeEncodedMarkup(value: string) {
let decoded = value;

for (
let attempt = 0;
attempt < 3 && /&(?:amp;)?(?:lt|gt);/i.test(decoded);
attempt += 1
) {
const document = new DOMParser().parseFromString(decoded, "text/html");
const nextValue = document.body.textContent ?? decoded;

if (nextValue === decoded) break;
decoded = nextValue;
}

return decoded;
}

function safeExternalUrl(value: string | null) {
if (!value) return null;

try {
const url = new URL(value, window.location.origin);
return url.protocol === "http:" || url.protocol === "https:"
? url.href
: null;
} catch {
return null;
}
}

function renderNode(node: Node, key: string): ReactNode {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}

if (node.nodeType !== Node.ELEMENT_NODE) {
return null;
}

const element = node as HTMLElement;
const tag = element.tagName.toLowerCase();

if (discardedElements.has(tag)) {
return null;
}

const children = Array.from(element.childNodes).map((child, index) =>
renderNode(child, `${key}-${index}`),
);

switch (tag) {
case "a": {
const href = safeExternalUrl(element.getAttribute("href"));

return href ? (
<a key={key} href={href} target="_blank" rel="noreferrer nofollow">
{children}
</a>
) : (
<span key={key}>{children}</span>
);
}
case "br":
return <br key={key} />;
case "strong":
case "b":
return <strong key={key}>{children}</strong>;
case "em":
case "i":
return <em key={key}>{children}</em>;
case "h1":
return <h1 key={key}>{children}</h1>;
case "h2":
return <h2 key={key}>{children}</h2>;
case "h3":
return <h3 key={key}>{children}</h3>;
case "h4":
case "h5":
case "h6":
return <h4 key={key}>{children}</h4>;
case "ul":
return <ul key={key}>{children}</ul>;
case "ol":
return <ol key={key}>{children}</ol>;
case "li":
return <li key={key}>{children}</li>;
case "p":
return <p key={key}>{children}</p>;
case "blockquote":
return <blockquote key={key}>{children}</blockquote>;
case "code":
return <code key={key}>{children}</code>;
case "pre":
return <pre key={key}>{children}</pre>;
case "hr":
return <hr key={key} />;
case "div":
return <div key={key}>{children}</div>;
default:
return <Fragment key={key}>{children}</Fragment>;
}
}

export function FormattedJobDescription({
description,
}: FormattedJobDescriptionProps) {
const decodedDescription = decodeEncodedMarkup(description);
const document = new DOMParser().parseFromString(
decodedDescription,
"text/html",
);
const hasMarkup = /<\/?[a-z][\s\S]*>/i.test(decodedDescription);

return (
<div className="max-h-80 overflow-y-auto rounded-md border border-border bg-background p-4 text-sm leading-6 text-muted-foreground [&_a]:font-semibold [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_h1]:mb-3 [&_h1]:text-xl [&_h1]:font-bold [&_h2]:mb-3 [&_h2]:text-lg [&_h2]:font-bold [&_h3]:mb-2 [&_h3]:mt-5 [&_h3]:text-base [&_h3]:font-bold [&_h4]:mb-2 [&_h4]:mt-4 [&_h4]:font-bold [&_hr]:my-4 [&_li]:mb-1 [&_ol]:mb-4 [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:mb-3 [&_pre]:overflow-x-auto [&_pre]:whitespace-pre-wrap [&_strong]:font-bold [&_strong]:text-foreground [&_ul]:mb-4 [&_ul]:list-disc [&_ul]:pl-5">
{hasMarkup ? (
Array.from(document.body.childNodes).map((node, index) =>
renderNode(node, String(index)),
)
) : (
<p className="whitespace-pre-wrap">
{document.body.textContent ?? decodedDescription}
</p>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ExternalLink } from "lucide-react";
import { jobStatusClasses, jobStatuses } from "../../constants";
import type { Job, JobStatus } from "../../types";
import { Modal } from "../shared/Modal";
import { FormattedJobDescription } from "./FormattedJobDescription";

interface JobDetailModalProps {
job: Job;
Expand Down Expand Up @@ -49,7 +50,9 @@ export function JobDetailModal({
onStatusChange,
onNotesChange,
}: JobDetailModalProps) {
const payloadEntries = Object.entries(job.rawPayload ?? {});
const payloadEntries = Object.entries(job.rawPayload ?? {}).filter(
([key]) => key !== "description",
);
const description = payloadValueToText(job.rawPayload?.description);
const hasDescription = description !== "Não informado";

Expand Down Expand Up @@ -131,9 +134,7 @@ export function JobDetailModal({
<span className="text-xs font-bold uppercase text-muted-foreground">
Descrição
</span>
<p className="max-h-48 overflow-y-auto whitespace-pre-wrap rounded-md border border-border bg-background p-3 text-sm leading-6 text-muted-foreground">
{description}
</p>
<FormattedJobDescription description={description} />
</div>
) : null}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export function JobFilter({
setMatchSort,
}: JobFilterProps) {
return (
<div className="grid gap-4 rounded-2xl border border-border bg-card p-4 md:grid-cols-[minmax(280px,1fr)_168px_168px_180px_180px_180px]">
<label className="relative block">
<div className="grid min-w-0 grid-cols-1 gap-4 rounded-2xl border border-border bg-card p-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-[minmax(280px,1fr)_repeat(5,minmax(0,180px))]">
<label className="relative block min-w-0">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
value={searchQuery}
Expand All @@ -55,7 +55,7 @@ export function JobFilter({
onChange={(event) =>
setFilterType(event.target.value as JobModelFilter)
}
className="h-11 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
className="h-11 w-full min-w-0 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
>
<option value="Todos">Modelo (Todos)</option>
{jobModelFilterOptions
Expand All @@ -70,7 +70,7 @@ export function JobFilter({
<select
value={filterLevel}
onChange={(event) => setFilterLevel(event.target.value)}
className="h-11 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
className="h-11 w-full min-w-0 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
>
<option value="Todos">Sênioridade (Todos)</option>
{jobLevels.map((level) => (
Expand All @@ -83,7 +83,7 @@ export function JobFilter({
onChange={(event) =>
setContinentFilter(event.target.value as ContinentFilter)
}
className="h-11 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
className="h-11 w-full min-w-0 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
>
{continentOptions.map((continent) => (
<option key={continent} value={continent}>
Expand All @@ -97,7 +97,7 @@ export function JobFilter({
onChange={(event) =>
setCountryFilter(event.target.value as CountryFilter)
}
className="h-11 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
className="h-11 w-full min-w-0 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
>
{countryOptions.map((country) => (
<option key={country} value={country}>
Expand All @@ -109,7 +109,7 @@ export function JobFilter({
<select
value={matchSort}
onChange={(event) => setMatchSort(event.target.value as MatchSort)}
className="h-11 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
className="h-11 w-full min-w-0 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
>
<option value="default">Match (padrão)</option>
<option value="desc">Maior match</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export function JobRow({ job, onOpen, onStatusChange }: JobRowProps) {
<td className="px-4 py-5 align-middle text-sm text-muted-foreground">
{job.level}
</td>
<td className="whitespace-nowrap px-4 py-5 align-middle text-sm text-muted-foreground">
{job.posted}
</td>
<td className="px-4 py-5 align-middle">
<span
className="rounded-full bg-emerald-100 px-2 py-1 text-[11px] font-bold text-emerald-700"
Expand Down
Loading
Loading