Little rework
This commit is contained in:
+102
-121
@@ -1,133 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import type { RealWork } from "../types";
|
||||
import type { WorkExperience as WorkExperienceType } from "../types";
|
||||
|
||||
interface WorkExperienceProps {
|
||||
realWork: RealWork[];
|
||||
experiences: WorkExperienceType[];
|
||||
}
|
||||
|
||||
function getWorkDateTimestamp(dateValue?: string) {
|
||||
if (!dateValue) return Number.NEGATIVE_INFINITY;
|
||||
const timestamp = Date.parse(dateValue);
|
||||
return Number.isNaN(timestamp) ? Number.NEGATIVE_INFINITY : timestamp;
|
||||
function formatMonth(iso: string) {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", timeZone: "UTC" });
|
||||
}
|
||||
|
||||
function getSortTimestamp(entry: RealWork) {
|
||||
if (!entry.until || entry.until.trim().toLowerCase() === "present") {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
const untilTimestamp = getWorkDateTimestamp(entry.until);
|
||||
if (untilTimestamp !== Number.NEGATIVE_INFINITY) return untilTimestamp;
|
||||
return getWorkDateTimestamp(entry.from);
|
||||
/** "X yr Y mo" duration between two dates (toDate null = now). */
|
||||
function formatDuration(fromIso: string, toIso?: string | null) {
|
||||
const from = new Date(fromIso);
|
||||
const to = toIso ? new Date(toIso) : new Date();
|
||||
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return "";
|
||||
|
||||
let months =
|
||||
(to.getFullYear() - from.getFullYear()) * 12 + (to.getMonth() - from.getMonth());
|
||||
if (to.getDate() >= from.getDate()) months += 1; // count the current/partial month
|
||||
months = Math.max(months, 1);
|
||||
|
||||
const years = Math.floor(months / 12);
|
||||
const rem = months % 12;
|
||||
const parts: string[] = [];
|
||||
if (years > 0) parts.push(`${years} yr`);
|
||||
if (rem > 0) parts.push(`${rem} mo`);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function formatWorkDate(dateValue: string) {
|
||||
const yearMonthMatch = /^(\d{4})-(\d{2})$/.exec(dateValue.trim());
|
||||
if (yearMonthMatch) {
|
||||
const year = Number.parseInt(yearMonthMatch[1], 10);
|
||||
const monthIndex = Number.parseInt(yearMonthMatch[2], 10) - 1;
|
||||
if (monthIndex >= 0 && monthIndex <= 11) {
|
||||
return new Date(Date.UTC(year, monthIndex, 1)).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
}
|
||||
export function WorkExperience({ experiences }: WorkExperienceProps) {
|
||||
return (
|
||||
<section className="w-full max-w-4xl mx-auto px-4 space-y-6 sm:space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-2xl font-semibold text-white sm:text-3xl">Work Experience</h2>
|
||||
<p className="mx-auto max-w-2xl text-sm text-gray-500 sm:text-base">
|
||||
Actual work that i did at actual companies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
const timestamp = Date.parse(dateValue);
|
||||
if (Number.isNaN(timestamp)) return dateValue;
|
||||
return new Date(timestamp).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
});
|
||||
}
|
||||
|
||||
export function WorkExperience({ realWork }: WorkExperienceProps) {
|
||||
const sortedRealWork = [...realWork].sort(
|
||||
(a, b) =>
|
||||
getSortTimestamp(b) - getSortTimestamp(a) ||
|
||||
getWorkDateTimestamp(b.from) - getWorkDateTimestamp(a.from),
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="w-full max-w-4xl mx-auto px-4 space-y-6 sm:space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500 sm:text-3xl">
|
||||
Work Experience
|
||||
</h2>
|
||||
<p className="mx-auto max-w-2xl text-sm text-gray-400 sm:text-base">
|
||||
Actual work that i did at actual companies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{realWork.length === 0 ? (
|
||||
<div className="rounded-2xl border border-cyan-500/20 bg-gradient-to-br from-cyan-500/10 to-blue-500/5 p-5 text-center backdrop-blur-sm sm:p-6 md:p-8">
|
||||
<p className="text-gray-300">No work experience entries available yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{sortedRealWork.map((entry, index) => (
|
||||
<div
|
||||
key={`${entry.company}-${index}`}
|
||||
className="w-full space-y-4 rounded-2xl border border-cyan-500/20 bg-gradient-to-br from-cyan-500/10 to-blue-500/5 p-5 backdrop-blur-sm sm:space-y-5 sm:p-6 md:p-8"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-2">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white sm:text-2xl">{entry.company}</h3>
|
||||
{entry.role ? (
|
||||
<p className="text-sm font-medium text-cyan-200/90 mt-1">{entry.role}</p>
|
||||
) : null}
|
||||
{entry.from || entry.until ? (
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-cyan-300/80 mt-1">
|
||||
{entry.from ? formatWorkDate(entry.from) : "Unknown"} -{" "}
|
||||
{entry.until
|
||||
? entry.until.trim().toLowerCase() === "present"
|
||||
? "Present"
|
||||
: formatWorkDate(entry.until)
|
||||
: "Present"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{entry.url ? (
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 self-start sm:self-auto rounded-full border border-cyan-400/30 bg-cyan-400/10 px-3 py-1.5 text-sm font-medium text-cyan-200 hover:bg-cyan-400/20 hover:border-cyan-300/50 transition-colors"
|
||||
>
|
||||
<span>{new URL(entry.url).hostname.replace(/^www\./, "")}</span>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<path d="M5 6.75A1.75 1.75 0 0 1 6.75 5h2a.75.75 0 0 0 0-1.5h-2A3.25 3.25 0 0 0 3.5 6.75v6.5A3.25 3.25 0 0 0 6.75 16.5h6.5a3.25 3.25 0 0 0 3.25-3.25v-2a.75.75 0 0 0-1.5 0v2A1.75 1.75 0 0 1 13.25 15h-6.5A1.75 1.75 0 0 1 5 13.25v-6.5Z" />
|
||||
<path d="M11.25 3.5a.75.75 0 0 0 0 1.5h2.69l-4.72 4.72a.75.75 0 1 0 1.06 1.06L15 6.06v2.69a.75.75 0 0 0 1.5 0V3.5h-5.25Z" />
|
||||
</svg>
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-300 leading-relaxed">{entry.summary}</p>
|
||||
|
||||
{entry.tags && entry.tags.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{entry.tags.map((tag) => (
|
||||
<span
|
||||
key={`${entry.company}-${tag}`}
|
||||
className="px-3 py-1 rounded-full text-xs font-semibold bg-cyan-500/20 text-cyan-200 border border-cyan-500/30"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
{experiences.length === 0 ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-5 text-center sm:p-6 md:p-8">
|
||||
<p className="text-gray-400">No work experience entries available yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{experiences.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="w-full space-y-4 rounded-2xl border border-white/10 bg-white/[0.03] p-5 transition-colors hover:border-white/20 sm:space-y-5 sm:p-6 md:p-8"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
||||
<div className="flex items-start gap-3">
|
||||
{entry.iconUrl ? (
|
||||
<img
|
||||
src={entry.iconUrl}
|
||||
alt=""
|
||||
className="mt-1 h-10 w-10 shrink-0 rounded-lg object-contain"
|
||||
/>
|
||||
) : null}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white sm:text-2xl">
|
||||
{entry.company}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm font-medium text-gray-300">{entry.role}</p>
|
||||
<p className="mt-1 text-xs font-medium uppercase tracking-wide text-gray-500">
|
||||
{formatMonth(entry.fromDate)} —{" "}
|
||||
{entry.toDate ? formatMonth(entry.toDate) : "Present"}
|
||||
<span className="ml-2 text-gray-600">
|
||||
· {formatDuration(entry.fromDate, entry.toDate)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{entry.url ? (
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 self-start rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-sm font-medium text-gray-300 transition-colors hover:border-white/25 hover:bg-white/10 sm:self-auto"
|
||||
>
|
||||
<span>{new URL(entry.url).hostname.replace(/^www\./, "")}</span>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden className="h-4 w-4">
|
||||
<path d="M5 6.75A1.75 1.75 0 0 1 6.75 5h2a.75.75 0 0 0 0-1.5h-2A3.25 3.25 0 0 0 3.5 6.75v6.5A3.25 3.25 0 0 0 6.75 16.5h6.5a3.25 3.25 0 0 0 3.25-3.25v-2a.75.75 0 0 0-1.5 0v2A1.75 1.75 0 0 1 13.25 15h-6.5A1.75 1.75 0 0 1 5 13.25v-6.5Z" />
|
||||
<path d="M11.25 3.5a.75.75 0 0 0 0 1.5h2.69l-4.72 4.72a.75.75 0 1 0 1.06 1.06L15 6.06v2.69a.75.75 0 0 0 1.5 0V3.5h-5.25Z" />
|
||||
</svg>
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="leading-relaxed text-gray-300">{entry.summary}</p>
|
||||
|
||||
{entry.tags.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{entry.tags.map((tag) => (
|
||||
<span
|
||||
key={`${entry.id}-${tag}`}
|
||||
className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-medium text-gray-300"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user