Skip to content

#28 Cycle Archive/Management Page#35

Merged
b-at-neu merged 5 commits into
devfrom
copilot/add-admin-cycles-page
Feb 23, 2026
Merged

#28 Cycle Archive/Management Page#35
b-at-neu merged 5 commits into
devfrom
copilot/add-admin-cycles-page

Conversation

Copy link
Copy Markdown

Copilot AI commented Feb 23, 2026

  • Explore repository structure and understand patterns
  • Add getCyclesWithCounts() to lib/data/cycles.ts
  • Create lib/actions/cycles.ts with createCycle, setActiveCycle, deleteCycle server actions
  • Create colocated components in app/admin/cycles/
  • Create app/admin/cycles/page.tsx admin page
  • Add "Manage Cycles" link to admin dashboard (app/admin/page.tsx)
  • Address review round 1: colocate components, React Hook Form, fix link, error handling, revalidatePath
  • Address review round 2: AlertDialog, error type helper, revalidatePath cleanup, RHF setError
  • Fix build error: move isActionError / ActionError to lib/actions/utils.ts (non-server file) — 'use server' files can only export async functions
Original prompt

This section details on the original issue you should resolve

<issue_title>Cycle Archive/Management Page</issue_title>
<issue_description>#### Context
Admins need UI to view all cycles, create new cycles, and set which cycle is active.

Goal

Build admin page at /admin/cycles to manage cycles.

Features

List View:

  • Table/cards showing all cycles (active and inactive)
  • Display: name, active status badge, counts (applications, nominations, endorsements), created date
  • Active cycle visually highlighted
  • "Create New Cycle" button at top
  • For each inactive cycle: "Set Active" button
  • For each cycle: "View Details" link (goes to /cycles/[id])

Create Cycle Modal:

  • Text input for cycle name (required)
  • Two buttons: "Create & Set Active" and "Create as Inactive"
  • "Create & Set Active": Creates cycle with isActive = true, deactivates others
  • "Create as Inactive": Creates cycle with isActive = false

Set Active Flow:

  • Click "Set Active" on inactive cycle
  • Confirmation modal: "This will make [Cycle Name] the active cycle. All new applications will go to this cycle. Continue?"
  • On confirm: Set selected isActive = true, all others isActive = false
  • If cycle has no settings, create default Settings record

Validation:

  • Cycle name required
  • Cannot soft delete active cycle
  • Can soft delete inactive cycles only if no data (applications, nominations, endorsements)

Acceptance Criteria

  • Page at /admin/cycles displays all cycles
  • Shows counts for applications, nominations, endorsements per cycle
  • Active cycle clearly indicated
  • Can create new cycle with name
  • "Create & Set Active" sets new cycle active and deactivates others
  • "Create as Inactive" creates without changing active cycle
  • "Set Active" button on inactive cycles with confirmation
  • Cannot delete active cycle
  • Can delete empty inactive cycles</issue_description>

<agent_instructions>ticket #28</agent_instructions>

Comments on the Issue (you are @copilot in this section)

  • Fixes b-at-neu/senate-path#28

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
senate-path Ready Ready Preview, Comment Feb 23, 2026 4:03am

Co-authored-by: b-at-neu <144247685+b-at-neu@users.noreply.github.com>
Copilot AI changed the title [WIP] Add admin page for cycle management #28 Cycle Archive/Management Page Feb 23, 2026
Copilot AI requested a review from b-at-neu February 23, 2026 03:19
Comment thread components/CyclesManager.tsx Outdated
};

interface CyclesManagerProps {
cycles: CycleWithCounts[];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be in one interface together

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8116744. CycleWithCounts is now a single exported interface and CyclesManagerProps references it directly.

Comment thread components/CyclesManager.tsx Outdated
export default function CyclesManager({ cycles }: CyclesManagerProps) {
const router = useRouter();

// Create modal state
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use react hook form

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8116744. The create cycle modal now uses React Hook Form with a Zod schema (name: z.string().min(1, 'Cycle name is required')). Two handleSubmit wrappers are created via a shared handleCreate(setActive) factory to handle the "Create as Inactive" vs "Create & Set Active" split.

Comment thread components/CyclesManager.tsx Outdated
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Link href={`/cycles/${cycle.id}`}>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go to /admin/cycles/[id]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8116744 — the View link now goes to /admin/cycles/${cycle.id}.

Comment thread components/CyclesManager.tsx Outdated
@@ -0,0 +1,292 @@
'use client';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets split up this file into smaller components, also we can colocate this with the only page its on

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8116744. Removed components/CyclesManager.tsx and split into three colocated files in app/admin/cycles/: cycles-manager.tsx (table + state orchestration), create-cycle-modal.tsx (create form), and set-active-modal.tsx (confirmation modal).

Comment thread lib/actions/cycles.ts Outdated
* Create a new cycle. If setActive is true, deactivate all other cycles.
*/
export async function createCycle(name: string, setActive: boolean) {
if (!name.trim()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be handled with zod validation on client side

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 8116744. Name validation is now handled entirely by the Zod schema in create-cycle-modal.tsx.

Comment thread lib/actions/cycles.ts
Comment thread lib/actions/cycles.ts
Comment thread lib/actions/cycles.ts
Comment thread lib/actions/cycles.ts Outdated

return { success: true };
} catch (error) {
console.error('Error setting active cycle:', error);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't graceful, see how error handling works elsewhere

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 8116744 to return { success: false, error: 'Failed to set active cycle' } — static string matching the settings.ts pattern.

Comment thread lib/actions/cycles.ts Outdated
return { success: true };
} catch (error) {
console.error('Error deleting cycle:', error);
return {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle errors more gracefully

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 8116744 to return { success: false, error: 'Failed to delete cycle' } — static string matching the settings.ts pattern.

…w link, clean up actions

Co-authored-by: b-at-neu <144247685+b-at-neu@users.noreply.github.com>
Comment thread app/admin/cycles/create-cycle-modal.tsx Outdated
: `Cycle "${data.name}" created`
);
onClose();
router.refresh();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already revalidate the page

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in ba91dc2router.refresh() and the useRouter import are gone from create-cycle-modal.tsx. The revalidatePath('/admin') in the server action is sufficient.

Comment thread app/admin/cycles/create-cycle-modal.tsx Outdated
onClose();
router.refresh();
} else {
setServerError(result.error || 'Failed to create cycle');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this have to be a useState? Can't we use react hook form properly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba91dc2. Replaced useState(serverError) with RHF's setError('root', { message: result.error }). The error is now read from errors.root?.message instead.

Comment thread app/admin/cycles/cycles-manager.tsx Outdated
Set Active
</Button>
)}
{!cycle.isActive && isEmpty(cycle) && (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets deactivate this button on empty, not remove it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in ba91dc2 — the delete button is now always rendered for inactive cycles but has disabled={!isEmpty(cycle)} when the cycle has data.

Comment thread app/admin/cycles/set-active-modal.tsx Outdated
if (result.success) {
toast.success(`"${cycle.name}" is now the active cycle`);
onClose();
router.refresh();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in ba91dc2. Switched to AlertDialog which handles its own open/close lifecycle, so no useState or useRouter is needed.

Comment thread app/admin/cycles/set-active-modal.tsx Outdated
};

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't do this, just use a dialog or alertdialog from shadcn

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ba91dc2. Added @radix-ui/react-alert-dialog and the standard shadcn AlertDialog component (components/ui/alert-dialog.tsx), then rewrote set-active-modal.tsx to use AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogAction, and AlertDialogCancel.

Comment thread lib/actions/cycles.ts
Comment thread lib/actions/cycles.ts Outdated
return { success: true, cycle };
} catch (error) {
console.error('Error creating cycle:', error);
return { success: false, error: 'Failed to create cycle' };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to console error. also just return { error: text }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need the sucess false, just make an errortype and have a helper to check for it,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ba91dc2. Removed all console.error calls. Introduced ActionError = { error: string } type, isActionError(result: unknown): result is ActionError helper, and changed all returns to { error: 'message' } on failure and { cycle } / {} on success. The success boolean is gone entirely.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ba91dc2 — see above reply.

…ePath cleanup, rhf setError

Co-authored-by: b-at-neu <144247685+b-at-neu@users.noreply.github.com>
@b-at-neu
Copy link
Copy Markdown
Collaborator

@copilot

Build error occurred
Error: Turbopack build failed with 7 errors:
./lib/actions/cycles.ts:9:17
Ecmascript file had an error
7 | type ActionError = { error: string };
8 |
9 | export function isActionError(result: unknown): result is ActionError {
| ^^^^^^^^^^^^^
10 | return typeof result === 'object' && result !== null && 'error' in result;
11 | }
12 |

…ib/actions/utils.ts

Co-authored-by: b-at-neu <144247685+b-at-neu@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Feb 23, 2026

@copilot

Build error occurred
...

Fixed in 74b218e. The root cause was that 'use server' files can only export async functions — exporting the synchronous isActionError broke the build. Moved ActionError and isActionError to a new lib/actions/utils.ts file (no directive), and updated the three client components and cycles.ts to import from there.

@b-at-neu b-at-neu marked this pull request as ready for review February 23, 2026 04:05
@b-at-neu b-at-neu merged commit 6261077 into dev Feb 23, 2026
2 checks passed
@b-at-neu b-at-neu deleted the copilot/add-admin-cycles-page branch February 23, 2026 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cycle Archive/Management Page

2 participants