Skip to content

Commit 2e1adfe

Browse files
authored
Merge pull request #33 from tamucsa/develop
added workflow keepalive and editable schedule post timing
2 parents f88b95c + b108c5c commit 2e1adfe

3 files changed

Lines changed: 189 additions & 0 deletions

File tree

.github/workflows/publish-scheduled-events.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,10 @@ jobs:
4242
echo "Response was not a publish JSON payload."
4343
exit 1
4444
fi
45+
workflow-keepalive:
46+
if: github.event_name == 'schedule'
47+
runs-on: ubuntu-latest
48+
permissions:
49+
actions: write
50+
steps:
51+
- uses: liskin/gh-workflow-keepalive@v1

src/app/(dashboard)/(officer)/officer/events/components/EventDetailClient.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import EventRsvpPanel from "@/app/(dashboard)/(officer)/officer/events/component
88
import { officerRemoveCheckIn } from "@/app/actions/attendance";
99
import {
1010
publishEvent,
11+
updateScheduledPublish,
1112
updateEventMixerFamilies,
1213
updateEventRsvp,
1314
updateEventSchedule,
@@ -160,6 +161,15 @@ export default function EventDetailClient({
160161
const [scheduleSaving, setScheduleSaving] = useState(false);
161162
const [scheduleError, setScheduleError] = useState<string | null>(null);
162163
const [scheduleSaved, setScheduleSaved] = useState(false);
164+
const [publishDate, setPublishDate] = useState(() =>
165+
event.publish_at ? eventTimestampToFormDate(event.publish_at) : "",
166+
);
167+
const [publishTime, setPublishTime] = useState(() =>
168+
event.publish_at ? eventTimestampToFormTime(event.publish_at) : "",
169+
);
170+
const [publishTimeSaving, setPublishTimeSaving] = useState(false);
171+
const [publishTimeError, setPublishTimeError] = useState<string | null>(null);
172+
const [publishTimeSaved, setPublishTimeSaved] = useState(false);
163173
const [uncheckTarget, setUncheckTarget] = useState<AttendanceRow | null>(
164174
null,
165175
);
@@ -208,13 +218,16 @@ export default function EventDetailClient({
208218
setLocation(event.location ?? "");
209219
setLocationMapsUrl(event.location_maps_url ?? null);
210220
setDescription(event.description ?? "");
221+
setPublishDate(event.publish_at ? eventTimestampToFormDate(event.publish_at) : "");
222+
setPublishTime(event.publish_at ? eventTimestampToFormTime(event.publish_at) : "");
211223
}, [
212224
event.name,
213225
event.starts_at,
214226
event.ends_at,
215227
event.location,
216228
event.location_maps_url,
217229
event.description,
230+
event.publish_at,
218231
]);
219232

220233
const toggleMixerFamily = (id: string) => {
@@ -300,6 +313,25 @@ export default function EventDetailClient({
300313
router.refresh();
301314
};
302315

316+
const handleSavePublishTime = async () => {
317+
setPublishTimeSaving(true);
318+
setPublishTimeError(null);
319+
setPublishTimeSaved(false);
320+
321+
const result = await updateScheduledPublish(event.id, {
322+
scheduleDate: publishDate,
323+
scheduleTime: publishTime,
324+
});
325+
326+
setPublishTimeSaving(false);
327+
if (!result.success) {
328+
setPublishTimeError(result.error ?? "Failed to update publish time.");
329+
return;
330+
}
331+
setPublishTimeSaved(true);
332+
router.refresh();
333+
};
334+
303335
const handlePublishStandings = async () => {
304336
if (
305337
!window.confirm(
@@ -499,6 +531,74 @@ export default function EventDetailClient({
499531
{publishError && (
500532
<p className="mt-3 text-sm text-red-600">{publishError}</p>
501533
)}
534+
{publishStatus === "scheduled" && (
535+
<CollapsibleSettings
536+
title="Schedule publish"
537+
defaultOpen
538+
summary={
539+
event.publish_at
540+
? new Date(event.publish_at).toLocaleString("en-US", {
541+
timeZone: EVENT_TIMEZONE,
542+
dateStyle: "medium",
543+
timeStyle: "short",
544+
}) + " CT"
545+
: "No publish time set"
546+
}
547+
>
548+
<p className="text-xs leading-5 text-subtitle">
549+
Update when this event should become visible to members. Times are
550+
Central Time.
551+
</p>
552+
<div className="grid gap-3 sm:grid-cols-2">
553+
<div>
554+
<label className={labelClassName} htmlFor="event-publish-date">
555+
Publish date
556+
</label>
557+
<input
558+
id="event-publish-date"
559+
type="date"
560+
className={inputClassName}
561+
value={publishDate}
562+
onChange={(e) => {
563+
setPublishDate(e.target.value);
564+
setPublishTimeSaved(false);
565+
setPublishTimeError(null);
566+
}}
567+
/>
568+
</div>
569+
<div>
570+
<label className={labelClassName} htmlFor="event-publish-time">
571+
Publish time
572+
</label>
573+
<input
574+
id="event-publish-time"
575+
type="time"
576+
className={inputClassName}
577+
value={publishTime}
578+
onChange={(e) => {
579+
setPublishTime(e.target.value);
580+
setPublishTimeSaved(false);
581+
setPublishTimeError(null);
582+
}}
583+
/>
584+
</div>
585+
</div>
586+
{publishTimeError && (
587+
<p className="text-sm text-red-600">{publishTimeError}</p>
588+
)}
589+
{publishTimeSaved && (
590+
<p className="text-sm text-green-700">Publish time saved.</p>
591+
)}
592+
<button
593+
type="button"
594+
onClick={() => void handleSavePublishTime()}
595+
disabled={publishTimeSaving}
596+
className={btnPrimaryClassName}
597+
>
598+
{publishTimeSaving ? "Saving…" : "Save publish time"}
599+
</button>
600+
</CollapsibleSettings>
601+
)}
502602
<CollapsibleSettings
503603
title="Event details"
504604
defaultOpen

src/app/actions/events.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,88 @@ export async function publishEvent(eventId: string) {
181181
return { success: true, error: null };
182182
}
183183

184+
/** Update the auto-publish date/time for an event that is still scheduled. */
185+
export async function updateScheduledPublish(
186+
eventId: string,
187+
input: { scheduleDate: string; scheduleTime: string },
188+
) {
189+
if (!eventId) return { success: false, error: "Event not found." };
190+
if (!input.scheduleDate || !input.scheduleTime) {
191+
return {
192+
success: false,
193+
error: "Publish date and time are required.",
194+
};
195+
}
196+
197+
const { supabase, error: authError } = await requireOfficer();
198+
if (authError) return { success: false, error: authError };
199+
200+
const { data: event } = await supabase
201+
.from("events")
202+
.select("id, publish_status, parent_event_id")
203+
.eq("id", eventId)
204+
.maybeSingle();
205+
206+
if (!event) return { success: false, error: "Event not found." };
207+
if (event.parent_event_id) {
208+
return {
209+
success: false,
210+
error: "Update the parent event schedule instead.",
211+
};
212+
}
213+
if (event.publish_status !== "scheduled") {
214+
return {
215+
success: false,
216+
error: "Only scheduled events can have a publish time.",
217+
};
218+
}
219+
220+
const { value: publishAt, error: scheduleError } =
221+
await resolveCentralEventTimestamp(
222+
supabase,
223+
input.scheduleDate,
224+
input.scheduleTime,
225+
);
226+
227+
if (!publishAt) {
228+
return {
229+
success: false,
230+
error: scheduleError ?? "Invalid publish date or time.",
231+
};
232+
}
233+
if (new Date(publishAt).getTime() <= Date.now()) {
234+
return {
235+
success: false,
236+
error: "Publish time must be in the future (Central Time).",
237+
};
238+
}
239+
240+
const { error } = await supabase
241+
.from("events")
242+
.update({ publish_at: publishAt })
243+
.eq("id", eventId)
244+
.eq("publish_status", "scheduled");
245+
246+
if (error) {
247+
return { success: false, error: "Failed to update publish time." };
248+
}
249+
250+
const { error: spectatorError } = await supabase
251+
.from("events")
252+
.update({ publish_at: publishAt })
253+
.eq("parent_event_id", eventId)
254+
.eq("publish_status", "scheduled");
255+
256+
if (spectatorError) {
257+
return {
258+
success: false,
259+
error: "Publish time saved, but linked spectator event failed to update.",
260+
};
261+
}
262+
263+
return { success: true, error: null };
264+
}
265+
184266
/** Used by cron: publish all scheduled events whose publish_at has passed. */
185267
export async function publishDueScheduledEvents() {
186268
const { createAdminSupabase } = await import("@/utils/supabase/admin");

0 commit comments

Comments
 (0)