diff --git a/frontend/apply/src/app/components/OpenPositionCard.tsx b/frontend/apply/src/app/components/OpenPositionCard.tsx
index 6668a0b..5c10448 100644
--- a/frontend/apply/src/app/components/OpenPositionCard.tsx
+++ b/frontend/apply/src/app/components/OpenPositionCard.tsx
@@ -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";
@@ -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 (
@@ -41,7 +48,10 @@ const OpenPositionCard = ({ position }: Props) => {
- {t("openPositionCard.deadline")}: {formatDate(position.recruitment_end)}
+ {t("openPositionCard.deadline")}:{" "}
+
+ {formatDate(position.recruitment_end)}
+
@@ -49,7 +56,10 @@ export default function PositionInfoCard({
{t("applicationForm.applicationDeadline")}:{" "}
- {formatDate(position.recruitment_end)} {deadlineSuffix}
+
+ {formatDate(position.recruitment_end)}
+ {" "}
+ {deadlineSuffix}
{t("common.termOfOffice")}: {formatDate(position.term_from)} —{" "}
@@ -105,4 +115,4 @@ export default function PositionInfoCard({
)}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/apply/src/app/page.tsx b/frontend/apply/src/app/page.tsx
index 1464a1c..a5f365d 100644
--- a/frontend/apply/src/app/page.tsx
+++ b/frontend/apply/src/app/page.tsx
@@ -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";
@@ -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";
@@ -25,6 +26,14 @@ export default function Home() {
const [applicationsError, setApplicationsError] = useState
(
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) {
@@ -137,7 +146,7 @@ export default function Home() {
{openPositions.length === 0 ? (
{t("homePage.noOpenPositions")}
) : (
- openPositions.map((position) => (
+ sortedOpenPositions.map((position) => (
))
)}
diff --git a/frontend/apply/src/app/styles/card.module.css b/frontend/apply/src/app/styles/card.module.css
index 54770ee..1d74f89 100644
--- a/frontend/apply/src/app/styles/card.module.css
+++ b/frontend/apply/src/app/styles/card.module.css
@@ -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;
diff --git a/frontend/apply/src/app/utils/dateFormat.ts b/frontend/apply/src/app/utils/dateFormat.ts
index e964f76..14655a6 100644
--- a/frontend/apply/src/app/utils/dateFormat.ts
+++ b/frontend/apply/src/app/utils/dateFormat.ts
@@ -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
*/
@@ -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")