Skip to content

[WIP] Add AGM view for users#1671

Draft
Drewbi wants to merge 7 commits intov4from
add-meeting-user-view
Draft

[WIP] Add AGM view for users#1671
Drewbi wants to merge 7 commits intov4from
add-meeting-user-view

Conversation

@Drewbi
Copy link
Contributor

@Drewbi Drewbi commented Feb 22, 2026

image image image image image

@vercel
Copy link

vercel bot commented Feb 22, 2026

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

Project Deployment Actions Updated (UTC)
website Error Error Feb 22, 2026 3:56pm

Request Review

Comment on lines +57 to +59
CLAUDE.md
.CLAUDE
.llms No newline at end of file
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shout out to my homies


export const positionsInGeneralMeetingsRouter = createTRPCRouter({
get: getPositions,
getWithCounts: getPositionsWithCounts,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this can probably just be calculated from the returned candidates for a position

Comment on lines +17 to +25
.select({
...getTableColumns(positions),
nomineeCount: count(nominations.candidateId),
})
.from(positions)
.leftJoin(nominations, eq(nominations.positionId, positions.id))
.where(eq(positions.meetingId, input.meetingId))
.groupBy(positions.id)
.orderBy(asc(positions.priority))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

You reckon there would be a way to do this with query instead of select? Will probably delete this endpoint anyway

if (!position) throw new TRPCError({ code: "NOT_FOUND", message: "Position not found" })

const positionNominations = await ctx.db
.select()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

change to query

questions: meetingQuestions,
candidates: candidateRows.map((candidate) => ({
...candidate,
answers: allAnswers.filter((a) => a.candidateId === candidate.id),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is BAD. Should really just include questions and answers in candidate query

const getNominations = publicRatedProcedure()
.input(z.object({ positionId: z.uuidv7() }))
.query(async ({ ctx, input }) => {
return ctx.db.select().from(nominations).where(eq(nominations.positionId, input.positionId))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

query

const getNominationsByCandidate = publicRatedProcedure()
.input(z.object({ candidateId: z.uuidv7() }))
.query(async ({ ctx, input }) => {
return ctx.db.select().from(nominations).where(eq(nominations.candidateId, input.candidateId))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

query

const getCandidates = publicRatedProcedure()
.input(z.object({ meetingId: z.uuidv7() }))
.query(async ({ ctx, input }) => {
return ctx.db.select().from(candidates).where(eq(candidates.meetingId, input.meetingId))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

query

Comment on lines +10 to +14
const [candidate] = await ctx.db
.select()
.from(candidates)
.where(and(eq(candidates.meetingId, input.meetingId), eq(candidates.userId, ctx.session.user.id)))
return candidate ?? null
Copy link
Contributor Author

Choose a reason for hiding this comment

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

query

const sameDay = meeting.end && meeting.start.toDateString() === meeting.end.toDateString()

return (
<main className="container mx-auto px-4 py-12">
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this needs extra styles to work in light mode

<h1 className="scroll-m-20 font-mono text-4xl tracking-tight text-balance">{meeting.title}</h1>
<div className="flex flex-col gap-1.5 font-mono text-sm text-neutral-500 dark:text-neutral-400">
<div className="flex items-center gap-2">
<span className="material-symbols-sharp text-base! leading-none!">schedule</span>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lots of important usage here, not really sure why

Copy link
Collaborator

Choose a reason for hiding this comment

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

material symbols jank and the styles don't work unless important

</section>
)}

{(meeting.status === "upcoming" || meeting.status === "ongoing") && (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This allows them to access nominations page even when meeting is open. I think this is desired behaviour

</section>
)}

{meeting.status === "ongoing" && (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might need an else here to tell them to hang tight if the meeting hasn't opened yet

Comment on lines +7 to +9
<Header />
{children}
<Footer />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could probably put some of the general layout styles in this top level layout

</ul>
)}

<p className="text-sm text-neutral-500 dark:text-neutral-400">Voting coming soon.</p>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure how we want to handle which position is open for voting. Might need to add a property to the positions table

Comment on lines +11 to +12
const key = `cfc-stars-${positionId}`
const [starred, setStarred] = React.useState<Set<string>>(new Set())
Copy link
Contributor Author

@Drewbi Drewbi Feb 22, 2026

Choose a reason for hiding this comment

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

this is a non-essential feature but is kinda useful for remembering your favs before voting has opened

Comment on lines +17 to +23
type Question = {
id: string
text: string
type: "short" | "long" | "checkbox" | null
required: boolean | null
order: number
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

should this stuff be coming from tRPC?

{ enabled: !!myCandidate },
)

const [isEditing, setIsEditing] = React.useState(false)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

do we really gotta preface React. all the time?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, it's the recommended way (by Dan Abramov)

Comment on lines +155 to +159
await Promise.all([
...[...selectedPositions].map((positionId) =>
createNomination.mutateAsync({ meetingId: meeting.id, positionId, candidateId: candidate.id }),
),
...questions
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this looks hella jank, lmk if theres a better way

Copy link
Collaborator

Choose a reason for hiding this comment

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

I skimmed over this. Way too much to read on a PR

{btnText}
</motion.span>
</AnimatePresence>
{loading && <Spinner className="absolute right-4" />}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This loading spinner is inside the text every time, gotta fix

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This probably has huge conflicts with your changes @JeremiahPinto, m'pologies

setBtnText("Saving agenda")
startTransition(async () => {
try {
await updateMeeting.mutateAsync({ id: meetingId, agenda: agenda || null })
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should use mutate instead of mutateAsync if u're not using the values returned


return (
<form
className={cn("flex w-full flex-col gap-y-4 bg-white p-6 dark:bg-neutral-950", className)}
Copy link
Collaborator

Choose a reason for hiding this comment

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

y remove this? it fixes accessibility focus with tanstack-form

Copy link
Collaborator

Choose a reason for hiding this comment

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

I mean the next line. misclick hehe

Copy link
Collaborator

@JeremiahPinto JeremiahPinto left a comment

Choose a reason for hiding this comment

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

PR too chonk. Have a look at some of the comments

)
}}
</form.Field>
<Textarea
Copy link
Collaborator

Choose a reason for hiding this comment

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

why change this from uncontrolled to controlled. Will cause unnecessary re-renders


import * as React from "react"
import { useParams } from "next/navigation"
import { CalendarClock, CircleCheck, CircleX, LoaderCircle } from "lucide-react"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm trying to use material-symbols instead of lucide-icons

Copy link
Collaborator

Choose a reason for hiding this comment

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

The dependency is left just to deal with shadcn cli

<FieldSet>
<FieldLegend variant="label">Meeting status</FieldLegend>
<FieldDescription>Manually override the current meeting status.</FieldDescription>
<RadioGroup value={status} onValueChange={handleChange} disabled={loading} className="mt-1">
Copy link
Collaborator

Choose a reason for hiding this comment

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

would prefer uncontrolled instead of controlled but not as big an issue

.max(nextFiveYears, "Date must be within the next five years"),
start_time: z.iso.time({ precision: -1, error: "Start time is required" }),
venue: z.string(),
end_time: z.iso.time({ precision: -1, error: "End time is required" }),
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will conflict with mine, but I didn't want end time to be required coz agms always run overtime lol

{ enabled: !!myCandidate },
)

const [isEditing, setIsEditing] = React.useState(false)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, it's the recommended way (by Dan Abramov)

Comment on lines +155 to +159
await Promise.all([
...[...selectedPositions].map((positionId) =>
createNomination.mutateAsync({ meetingId: meeting.id, positionId, candidateId: candidate.id }),
),
...questions
Copy link
Collaborator

Choose a reason for hiding this comment

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

I skimmed over this. Way too much to read on a PR

<h1 className="scroll-m-20 font-mono text-4xl tracking-tight text-balance">{meeting.title}</h1>
<div className="flex flex-col gap-1.5 font-mono text-sm text-neutral-500 dark:text-neutral-400">
<div className="flex items-center gap-2">
<span className="material-symbols-sharp text-base! leading-none!">schedule</span>
Copy link
Collaborator

Choose a reason for hiding this comment

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

material symbols jank and the styles don't work unless important

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.

2 participants