Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ yarn-error.log*
.env.sentry-build-plugin

# user data migration
cfc-website_user.csv
cfc-website_user.csv

# AI
CLAUDE.md
.CLAUDE
.llms
Comment on lines +57 to +59
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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@sindresorhus/slugify": "3.0.0",
"@square/web-sdk": "2.1.0",
"@t3-oss/env-nextjs": "0.13.10",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-form": "1.27.6",
"@tanstack/react-query": "5.90.12",
"@tanstack/react-query-devtools": "5.91.1",
Expand Down
15,040 changes: 6,693 additions & 8,347 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

150 changes: 56 additions & 94 deletions src/app/dashboard/admin/general-meetings/[slug]/cards/agenda.tsx
Original file line number Diff line number Diff line change
@@ -1,115 +1,77 @@
"use client"

import * as React from "react"
import { z } from "zod"
import { useForm } from "@tanstack/react-form"
import { useParams } from "next/navigation"
import { AnimatePresence, motion } from "motion/react"

import { cn } from "~/lib/utils"
import { api } from "~/trpc/react"
import { toast } from "~/hooks/use-toast"
import { cn } from "~/lib/utils"
import { Button } from "~/ui/button"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "~/ui/field"
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "~/ui/field"
import { Spinner } from "~/ui/spinner"
import { Textarea } from "~/ui/textarea"

interface AgendaCardProps {
className?: string
}

const formSchema = z.object({
agenda: z.string(),
})

export default function AgendaCard({ className, ...props }: AgendaCardProps) {
function AgendaForm({ meetingId, initialAgenda }: { meetingId: string; initialAgenda: string | null }) {
const utils = api.useUtils()
const btnRef = React.useRef<HTMLButtonElement>(null)
const [btnText, setText] = React.useState("Save")
// const { mutateAsync } = api.user.checkIfExists.useMutation()
const [btnText, setBtnText] = React.useState("Save agenda")
const [loading, startTransition] = React.useTransition()
const form = useForm({
defaultValues: {
agenda: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit({ value }) {
// setText("Checking if email exists")
startTransition(async () => {
// const userExists = await mutateAsync(value.email)
// if (userExists) {
// setText("Sending code to email")
// } else {
// setText("Redirecting to sign up")
// }
})
},
})
const [agenda, setAgenda] = React.useState(initialAgenda ?? "")

const updateMeeting = api.admin.generalMeetings.update.useMutation()

function handleSave() {
setBtnText("Saving agenda")
startTransition(async () => {
try {
await updateMeeting.mutateAsync({ id: meetingId, agenda: agenda || null })
await utils.generalMeetings.get.invalidate()
toast({ title: "Agenda saved" })
setBtnText("Save agenda")
} catch {
toast({ title: "Failed to save agenda", variant: "destructive" })
setBtnText("Save agenda")
}
})
}

return (
<form
className={cn("flex w-full flex-col gap-y-4 bg-white p-6 dark:bg-neutral-950", className)}
onSubmit={(e) => {
e.preventDefault()
btnRef.current?.focus()
void form.handleSubmit()
}}
>
<div className={cn("flex w-full flex-col gap-y-4 bg-white p-6 dark:bg-neutral-950")}>
<FieldSet className="h-full">
<FieldLegend variant="label">Meeting agenda</FieldLegend>
<FieldDescription>This will be viewable to everyone attending the meeting</FieldDescription>
<FieldDescription>This will be viewable to everyone attending the meeting. Supports markdown.</FieldDescription>
<FieldGroup className="h-full">
<form.Field name="agenda">
{(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid} className="h-full">
{/* <FieldLabel htmlFor={field.name}>Email address</FieldLabel> */}
<Textarea
id={field.name}
name={field.name}
placeholder="Write something here with markdown"
value={field.state.value}
onBlur={field.handleBlur}
aria-invalid={isInvalid}
disabled={loading}
className="h-full"
onChange={(e) => field.handleChange(e.target.value)}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
</form.Field>
<Textarea
placeholder="Write something here with markdown"
value={agenda}
disabled={loading}
className="h-full min-h-48"
onChange={(e) => setAgenda(e.target.value)}
/>
</FieldGroup>
</FieldSet>

<form.Subscribe selector={(state) => [state.canSubmit]}>
{([canSubmit]) => (
<Button ref={btnRef} type="submit" disabled={loading ?? !canSubmit} className="relative">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={btnText}
transition={{ type: "spring", duration: 0.2, bounce: 0 }}
initial={{ opacity: 0, y: -36 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 36 }}
>
{btnText}
</motion.span>
</AnimatePresence>
{loading && <Spinner className="absolute right-4" />}
</Button>
)}
</form.Subscribe>
</form>
<Button ref={btnRef} disabled={loading} className="relative" onClick={handleSave}>
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={btnText}
transition={{ type: "spring", duration: 0.2, bounce: 0 }}
initial={{ opacity: 0, y: -36 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 36 }}
>
{btnText}
</motion.span>
</AnimatePresence>
{loading && <Spinner className="absolute right-4" />}
</Button>
</div>
)
}

export default function AgendaCard() {
const { slug } = useParams<{ slug: string }>()
const [meeting] = api.generalMeetings.get.useSuspenseQuery({ slug })

return <AgendaForm meetingId={meeting.id} initialAgenda={meeting.agenda} />
}
Loading
Loading