+
+ {/* Contests - bigger with live indicators */}
+
+
+
+
{/* DXpeditions */}
-
+
{/* POTA */}
-
+
{
onToggleMap={togglePOTA}
/>
-
- {/* Contests */}
-
-
-
)}
diff --git a/src/components/ContestPanel.jsx b/src/components/ContestPanel.jsx
index 3991a7d..1aa7714 100644
--- a/src/components/ContestPanel.jsx
+++ b/src/components/ContestPanel.jsx
@@ -1,6 +1,6 @@
/**
* ContestPanel Component
- * Displays upcoming contests with contestcalendar.com credit
+ * Displays upcoming and active contests with live indicators
*/
import React from 'react';
@@ -22,13 +22,100 @@ export const ContestPanel = ({ data, loading }) => {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
+ const formatTime = (dateStr) => {
+ if (!dateStr) return '';
+ const date = new Date(dateStr);
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }) + 'z';
+ };
+
+ // Check if contest is live (happening now)
+ const isContestLive = (contest) => {
+ if (!contest.start || !contest.end) return false;
+ const now = new Date();
+ const start = new Date(contest.start);
+ const end = new Date(contest.end);
+ return now >= start && now <= end;
+ };
+
+ // Check if contest starts within 24 hours
+ const isStartingSoon = (contest) => {
+ if (!contest.start) return false;
+ const now = new Date();
+ const start = new Date(contest.start);
+ const hoursUntil = (start - now) / (1000 * 60 * 60);
+ return hoursUntil > 0 && hoursUntil <= 24;
+ };
+
+ // Get time remaining or time until start
+ const getTimeInfo = (contest) => {
+ if (!contest.start || !contest.end) return formatDate(contest.start);
+
+ const now = new Date();
+ const start = new Date(contest.start);
+ const end = new Date(contest.end);
+
+ if (now >= start && now <= end) {
+ // Contest is live - show time remaining
+ const hoursLeft = Math.floor((end - now) / (1000 * 60 * 60));
+ const minsLeft = Math.floor(((end - now) % (1000 * 60 * 60)) / (1000 * 60));
+ if (hoursLeft > 0) {
+ return `${hoursLeft}h ${minsLeft}m left`;
+ }
+ return `${minsLeft}m left`;
+ } else if (now < start) {
+ // Contest hasn't started
+ const hoursUntil = Math.floor((start - now) / (1000 * 60 * 60));
+ if (hoursUntil < 24) {
+ return `Starts in ${hoursUntil}h`;
+ }
+ return formatDate(contest.start);
+ }
+ return formatDate(contest.start);
+ };
+
+ // Sort contests: live first, then starting soon, then by date
+ const sortedContests = data ? [...data].sort((a, b) => {
+ const aLive = isContestLive(a);
+ const bLive = isContestLive(b);
+ const aSoon = isStartingSoon(a);
+ const bSoon = isStartingSoon(b);
+
+ if (aLive && !bLive) return -1;
+ if (!aLive && bLive) return 1;
+ if (aSoon && !bSoon) return -1;
+ if (!aSoon && bSoon) return 1;
+
+ return new Date(a.start) - new Date(b.start);
+ }) : [];
+
+ // Count live contests
+ const liveCount = sortedContests.filter(isContestLive).length;
+
return (
-
- 🏆 CONTESTS
+ 🏆 CONTESTS
+ {liveCount > 0 && (
+
+ 🔴 {liveCount} LIVE
+
+ )}
@@ -36,31 +123,61 @@ export const ContestPanel = ({ data, loading }) => {
- ) : data && data.length > 0 ? (
+ ) : sortedContests.length > 0 ? (
- {data.slice(0, 6).map((contest, i) => (
-
-
- {contest.name}
-
-
-
{contest.mode}
-
{formatDate(contest.start)}
+ {sortedContests.slice(0, 8).map((contest, i) => {
+ const live = isContestLive(contest);
+ const soon = isStartingSoon(contest);
+
+ return (
+
+
+ {live && (
+ ●
+ )}
+ {soon && !live && (
+ ◐
+ )}
+
+ {contest.name}
+
+
+
+ {contest.mode}
+
+ {getTimeInfo(contest)}
+
+
-
- ))}
+ );
+ })}
) : (
@@ -71,8 +188,8 @@ export const ContestPanel = ({ data, loading }) => {
{/* Contest Calendar Credit */}