diff --git a/.env.example b/.env.example index 2a82774..b99f48d 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ PROJECT_NAME=jama # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.9.84 +JAMA_VERSION=0.9.85 # App port — the host port Docker maps to the container PORT=3000 diff --git a/backend/package.json b/backend/package.json index ec23665..7fe4495 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.84", + "version": "0.9.85", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index b7b7c43..6c5efeb 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.9.84}" +VERSION="${1:-0.9.85}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index e8ffb15..896e4db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.9.84", + "version": "0.9.85", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index ba585f8..7c6868d 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -52,9 +52,9 @@ const RESP_ICON = { ), }; const BELL_ICON = ( - + Awaiting your response - + ); @@ -715,9 +715,12 @@ function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filter const today=new Date(); today.setHours(0,0,0,0); const terms=parseKeywords(filterKeyword); const hasFilters = terms.length > 0 || !!filterTypeId; - // No filters: show selected month only (desktop and mobile — month nav arrows control the month). - // Filters active: show today + all future on both platforms. - const from = hasFilters ? today : new Date(y,m,1); + const now = new Date(); // exact now for end-time comparison + const isCurrentMonth = y === today.getFullYear() && m === today.getMonth(); + // No filters: show from today (if current month) or start of month (future months) to end of month. + // Past months show nothing — navigate forward to see events. + // Filters active: today + all future events. + const from = hasFilters ? today : (isCurrentMonth ? today : new Date(y,m,1)); const to = hasFilters ? new Date(9999,11,31) : new Date(y,m+1,0,23,59,59); const filtered=events.filter(e=>{ const s=new Date(e.start_at); @@ -729,19 +732,62 @@ function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filter } return true; }); - const emptyMsg = hasFilters ? 'No events match your filters' : `No events in ${MONTHS[m]} ${y}`; + const emptyMsg = hasFilters + ? 'No events match your filters' + : new Date(y,m+1,0) < today + ? `No events — ${MONTHS[m]} ${y} is in the past` + : `No events in ${MONTHS[m]} ${y}`; if(!filtered.length) return
{emptyMsg}
; - return <>{filtered.map(e=>{const s=new Date(e.start_at);const col=e.event_type?.colour||'#9ca3af'; - // Desktop: original pre-v0.9.64 sizes. Mobile: compact sizes from v0.9.64 - const rowPad=isMobile?'12px 14px':'14px 20px'; - const rowGap=isMobile?10:20; - const datW=isMobile?36:44; const datFs=isMobile?20:22; const datSFs=isMobile?10:11; - const timeW=isMobile?62:100; const timeGap=isMobile?5:8; const timeFs=isMobile?11:13; - const dotSz=isMobile?8:10; - const typeFs=isMobile?10:11; const titleGap=isMobile?6:8; - return(
onSelect(e)} style={{display:'flex',alignItems:'center',gap:rowGap,padding:rowPad,borderBottom:'1px solid var(--border)',cursor:'pointer'}} onMouseEnter={el=>el.currentTarget.style.background='var(--background)'} onMouseLeave={el=>el.currentTarget.style.background=''}>
{s.getDate()}
{SHORT_MONTHS[s.getMonth()]}, {DAYS[s.getDay()]}
{e.all_day?All day:{fmtTime(e.start_at)} –
{fmtTime(e.end_at)}
}
{e.event_type?.name&&{e.event_type.name}:}{e.title}{!!e.track_availability&&( - e.my_response ? RESP_ICON[e.my_response](RESP_COLOR[e.my_response]) : BELL_ICON - )}
{e.location&&
{e.location}
}
);})}; + return <>{filtered.map(e=>{ + const s=new Date(e.start_at); + const end=new Date(e.end_at); + const isPast = !e.all_day && end < now; // event fully ended + const col = isPast ? '#9ca3af' : (e.event_type?.colour||'#9ca3af'); + const textColor = isPast ? 'var(--text-tertiary)' : 'var(--text-primary)'; + const subColor = isPast ? 'var(--text-tertiary)' : 'var(--text-secondary)'; + // Use CSS media query breakpoint logic — compact below 640px regardless of isMobile prop + // so responsive desktop doesn't compact when there's plenty of room + const compact = isMobile; // isMobile is only true on genuine mobile, not responsive desktop + const rowPad=compact?'12px 14px':'14px 20px'; + const rowGap=compact?10:20; + const datW=compact?36:44; const datFs=compact?20:22; const datSFs=compact?10:11; + const timeW=compact?80:100; const timeGap=compact?5:8; const timeFs=compact?11:13; + const dotSz=compact?8:10; + const availIcon = !!e.track_availability && ( + e.my_response + ? RESP_ICON[e.my_response](isPast ? '#9ca3af' : RESP_COLOR[e.my_response]) + : isPast + ? + : BELL_ICON + ); + return( +
onSelect(e)} style={{display:'flex',alignItems:'center',gap:rowGap,padding:rowPad,borderBottom:'1px solid var(--border)',cursor:'pointer',opacity:isPast?0.7:1}} onMouseEnter={el=>el.currentTarget.style.background='var(--background)'} onMouseLeave={el=>el.currentTarget.style.background=''}> + {/* Date column */} +
+
{s.getDate()}
+
{SHORT_MONTHS[s.getMonth()]}, {DAYS[s.getDay()]}
+
+ {/* Time + dot column */} +
+ + {e.all_day?All day:{fmtTime(e.start_at)} –
{fmtTime(e.end_at)}
} +
+ {/* Title + meta column */} +
+
+ {e.title} + {availIcon} +
+ {(e.event_type?.name||e.location) && ( +
+ {e.event_type?.name&&{e.event_type.name}{e.location?' ·':''}} + {e.location&&{e.location}} +
+ )} +
+
+ ); + })}; } const HOUR_H = 52; // px per hour row diff --git a/frontend/src/index.css b/frontend/src/index.css index 3d00ac9..dd4e26a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -7,6 +7,7 @@ --primary-dark: #1557b0; --primary-light: #e8f0fe; --surface: #ffffff; + --warning-stroke: #92400e; /* amber-900 — dark stroke on yellow fill */ --surface-variant: #f8f9fa; --background: #f1f3f4; --border: #e0e0e0; @@ -211,6 +212,7 @@ a { color: inherit; text-decoration: none; } --primary-dark: #4d9de0; --primary-light: #1a2d4a; --surface: #1e1e2e; + --warning-stroke: #fde68a; /* amber-200 — light stroke on yellow fill in dark mode */ --surface-variant: #252535; --background: #13131f; --border: #2e2e45;