Skip to content
Open
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
14 changes: 12 additions & 2 deletions frontend/apply/src/app/components/OpenPositionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState } from "react";
import type { Position } from "@/utils/types";
import { useTranslation } from "react-i18next";
import "@/i18n/config";
import { formatDate } from "@/utils/dateFormat";
import { formatDate, getDeadlineUrgency } from "@/utils/dateFormat";
import Image from "next/image";
import { getImageUrl } from "@/utils/imageUrl";
import Link from "next/link";
Expand All @@ -16,6 +16,13 @@ type Props = {
const OpenPositionCard = ({ position }: Props) => {
const [showInfo, setShowInfo] = useState(false);
const { t } = useTranslation();
const deadlineUrgency = getDeadlineUrgency(position.recruitment_end);
const deadlineClassName =
deadlineUrgency === "urgent"
? styles.deadlineUrgent
: deadlineUrgency === "soon"
? styles.deadlineSoon
: styles.deadlineDefault;

return (
<div className={styles.card}>
Expand All @@ -41,7 +48,10 @@ const OpenPositionCard = ({ position }: Props) => {

<div className={styles.cardRightSection}>
<h4>
{t("openPositionCard.deadline")}: {formatDate(position.recruitment_end)}
{t("openPositionCard.deadline")}:{" "}
<span className={deadlineClassName}>
{formatDate(position.recruitment_end)}
</span>
</h4>
<Link
className="smallButton"
Expand Down
16 changes: 13 additions & 3 deletions frontend/apply/src/app/components/PositionInfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Position } from "@/utils/types";
import styles from "@/styles/application.module.css";
import cardStyles from "@/styles/card.module.css";
import { useTranslation } from "react-i18next";
import { formatDate } from "@/utils/dateFormat";
import { formatDate, getDeadlineUrgency } from "@/utils/dateFormat";

type PositionInfoCardProps = {
position: Position;
Expand Down Expand Up @@ -41,6 +41,13 @@ export default function PositionInfoCard({
};

const deadlineSuffix = getDeadlineSuffix(position.recruitment_end);
const deadlineUrgency = getDeadlineUrgency(position.recruitment_end);
const deadlineClassName =
deadlineUrgency === "urgent"
? cardStyles.deadlineUrgent
: deadlineUrgency === "soon"
? cardStyles.deadlineSoon
: cardStyles.deadlineDefault;

return (
<div className={cardStyles.cardDark}>
Expand All @@ -49,7 +56,10 @@ export default function PositionInfoCard({
</p>
<p>
<strong>{t("applicationForm.applicationDeadline")}:</strong>{" "}
{formatDate(position.recruitment_end)} {deadlineSuffix}
<span className={deadlineClassName}>
{formatDate(position.recruitment_end)}
</span>{" "}
{deadlineSuffix}
</p>
<p>
<strong>{t("common.termOfOffice")}:</strong> {formatDate(position.term_from)} —{" "}
Expand Down Expand Up @@ -105,4 +115,4 @@ export default function PositionInfoCard({
)}
</div>
);
}
}
13 changes: 11 additions & 2 deletions frontend/apply/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import styles from "./page.module.css";
import ApplicationCard from "./components/ApplicationCard";
import LoadingRam from "./components/LoadingRam";
Expand All @@ -10,6 +10,7 @@ import { useIsLoggedIn } from "@/utils/auth";
import type { Position, Application } from "@/utils/types";
import { useTranslation } from "react-i18next";
import "@/i18n/config";
import { parseDeadlineDate } from "@/utils/dateFormat";

type Tab = "Open Positions" | "My Applications" | "My Positions";

Expand All @@ -25,6 +26,14 @@ export default function Home() {
const [applicationsError, setApplicationsError] = useState<string | null>(
null,
);
const sortedOpenPositions = useMemo(() => {
return [...openPositions].sort((a, b) => {
const aDeadline = parseDeadlineDate(a.recruitment_end)?.getTime();
const bDeadline = parseDeadlineDate(b.recruitment_end)?.getTime();

return (aDeadline ?? Infinity) - (bDeadline ?? Infinity);
});
}, [openPositions]);

useEffect(() => {
if (!isLoggedIn) {
Expand Down Expand Up @@ -137,7 +146,7 @@ export default function Home() {
{openPositions.length === 0 ? (
<p>{t("homePage.noOpenPositions")}</p>
) : (
openPositions.map((position) => (
sortedOpenPositions.map((position) => (
<OpenPositionCard key={position.id} position={position} />
))
)}
Expand Down
12 changes: 12 additions & 0 deletions frontend/apply/src/app/styles/card.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@
white-space: nowrap;
}

.deadlineDefault {
color: var(--black);
}

.deadlineSoon {
color: #FBC02D;
}

.deadlineUrgent {
color: var(--error-500);
}

@media screen and (max-width: 767px) {
.cardRightSection {
align-items: flex-start;
Expand Down
48 changes: 48 additions & 0 deletions frontend/apply/src/app/utils/dateFormat.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import i18n from '../i18n/config';

const dateOnlyPattern = /^(\d{4})-(\d{2})-(\d{2})$/;
const urgentDeadlineHours = 48;
const soonDeadlineDays = 5;

/**
* Get the current locale from i18n
*/
Expand All @@ -8,6 +12,50 @@ const getLocale = (): string => {
return language === 'sv' ? 'sv-SE' : 'en-GB';
};

/**
* Parse a deadline string. Date-only deadlines are treated as the end of that
* local calendar day, since they represent the last day to apply.
*/
export const parseDeadlineDate = (dateString: string): Date | null => {
if (!dateString) return null;

const dateOnlyMatch = dateOnlyPattern.exec(dateString);
if (dateOnlyMatch) {
const [, year, month, day] = dateOnlyMatch;
return new Date(
Number(year),
Number(month) - 1,
Number(day),
23,
59,
59,
999,
);
}

const date = new Date(dateString);
return isNaN(date.getTime()) ? null : date;
};

export const getDeadlineUrgency = (
dateString: string,
): "default" | "soon" | "urgent" => {
const deadline = parseDeadlineDate(dateString);
if (!deadline) return "default";

const hoursRemaining = (deadline.getTime() - Date.now()) / (1000 * 60 * 60);

if (hoursRemaining < urgentDeadlineHours) {
return "urgent";
}

if (hoursRemaining <= soonDeadlineDays * 24) {
return "soon";
}

return "default";
};

/**
* Format a date string to a human-readable format based on the current i18n locale
* @param dateString - ISO date string ("2026-09-01")
Expand Down
Loading