diff --git a/apps/web/src/app/[locale]/(space)/events/page.tsx b/apps/web/src/app/[locale]/(space)/events/page.tsx index 994d0780f..9d865e747 100644 --- a/apps/web/src/app/[locale]/(space)/events/page.tsx +++ b/apps/web/src/app/[locale]/(space)/events/page.tsx @@ -19,13 +19,13 @@ import { import { Pagination } from "@/components/pagination"; import { StackedList, StackedListItem } from "@/components/stacked-list"; import { Trans } from "@/components/trans"; -import { getScheduledEvents } from "@/features/scheduled-event/api/get-scheduled-events"; import { ScheduledEventListItem } from "@/features/scheduled-event/components/scheduled-event-list"; +import { getScheduledEvents } from "@/features/scheduled-event/queries"; import type { Status } from "@/features/scheduled-event/schema"; import { statusSchema } from "@/features/scheduled-event/schema"; import { getTranslation } from "@/i18n/server"; -import { requireUser } from "@/next-auth"; +import { getActiveSpace } from "@/auth/queries"; import { EventsTabbedView } from "./events-tabbed-view"; async function loadData({ @@ -39,9 +39,9 @@ async function loadData({ page?: number; pageSize?: number; }) { - const { userId } = await requireUser(); + const space = await getActiveSpace(); return getScheduledEvents({ - userId, + spaceId: space.id, status, search, page, diff --git a/apps/web/src/app/[locale]/(space)/page.tsx b/apps/web/src/app/[locale]/(space)/page.tsx index a0f816415..3e44bf49c 100644 --- a/apps/web/src/app/[locale]/(space)/page.tsx +++ b/apps/web/src/app/[locale]/(space)/page.tsx @@ -17,48 +17,26 @@ import { PageHeader, PageTitle, } from "@/app/components/page-layout"; -import { requireUser } from "@/auth/queries"; +import { getActiveSpace } from "@/auth/queries"; import { Trans } from "@/components/trans"; import { IfCloudHosted } from "@/contexts/environment"; +import { getUpcomingEventsCount } from "@/features/scheduled-event/queries"; import { getTranslation } from "@/i18n/server"; import { prisma } from "@rallly/database"; -import dayjs from "dayjs"; import { FeedbackAlert } from "./feedback-alert"; async function loadData() { - const user = await requireUser(); - - if (!user) { - return { - livePollCount: 0, - upcomingEventCount: 0, - }; - } - - const todayStart = dayjs().startOf("day").toDate(); - const todayEnd = dayjs().endOf("day").toDate(); - const now = new Date(); + const space = await getActiveSpace(); const [livePollCount, upcomingEventCount] = await Promise.all([ prisma.poll.count({ where: { - userId: user.id, + spaceId: space.id, status: "live", deleted: false, }, }), - prisma.scheduledEvent.count({ - where: { - userId: user.id, - OR: [ - { allDay: false, start: { gte: now } }, - { - allDay: true, - start: { gte: todayStart, lte: todayEnd }, - }, - ], - }, - }), + getUpcomingEventsCount(), ]); return { diff --git a/apps/web/src/app/[locale]/(space)/polls/page.tsx b/apps/web/src/app/[locale]/(space)/polls/page.tsx index 9b713915e..8bc3b7bf5 100644 --- a/apps/web/src/app/[locale]/(space)/polls/page.tsx +++ b/apps/web/src/app/[locale]/(space)/polls/page.tsx @@ -3,7 +3,6 @@ import { Button } from "@rallly/ui/button"; import { absoluteUrl, shortUrl } from "@rallly/utils/absolute-url"; import { InboxIcon } from "lucide-react"; import Link from "next/link"; -import { z } from "zod"; import { PollPageIcon } from "@/app/components/page-icons"; import { @@ -25,59 +24,27 @@ import { Trans } from "@/components/trans"; import { getPolls } from "@/features/poll/api/get-polls"; import { PollList, PollListItem } from "@/features/poll/components/poll-list"; import { getTranslation } from "@/i18n/server"; -import { requireUser } from "@/next-auth"; +import { getActiveSpace } from "@/auth/queries"; import { SearchInput } from "../../../components/search-input"; import { PollsTabbedView } from "./polls-tabbed-view"; - -const DEFAULT_PAGE_SIZE = 10; - -const pageSchema = z - .string() - .nullish() - .transform((val) => { - if (!val) return 1; - const parsed = Number.parseInt(val, 10); - return Number.isNaN(parsed) || parsed < 1 ? 1 : parsed; - }); - -const querySchema = z - .string() - .nullish() - .transform((val) => val?.trim() || undefined); - -const statusSchema = z - .enum(["live", "paused", "finalized"]) - .nullish() - .transform((val) => val || "live"); - -const pageSizeSchema = z - .string() - .nullish() - .transform((val) => { - if (!val) return DEFAULT_PAGE_SIZE; - const parsed = Number.parseInt(val, 10); - return Number.isNaN(parsed) || parsed < 1 - ? DEFAULT_PAGE_SIZE - : Math.min(parsed, 100); - }); +import { DEFAULT_PAGE_SIZE, searchParamsSchema } from "./schema"; // Combined schema for type inference async function loadData({ - userId, status = "live", page = 1, pageSize = DEFAULT_PAGE_SIZE, q, }: { - userId: string; status?: PollStatus; page?: number; pageSize?: number; q?: string; }) { + const space = await getActiveSpace(); const [{ total, data: polls }] = await Promise.all([ - getPolls({ userId, status, page, pageSize, q }), + getPolls({ spaceId: space.id, status, page, pageSize, q }), ]); return { @@ -124,24 +91,19 @@ function PollsEmptyState() { export default async function Page(props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { - const searchParams = await props.searchParams; const { t } = await getTranslation(); - const { userId } = await requireUser(); - const parsedStatus = statusSchema.parse(searchParams.status); - const parsedPage = pageSchema.parse(searchParams.page); - const parsedPageSize = pageSizeSchema.parse(searchParams.pageSize); - const parsedQuery = querySchema.parse(searchParams.q); + const searchParams = await props.searchParams; + const { status, page, pageSize, q } = searchParamsSchema.parse(searchParams); const { polls, total } = await loadData({ - userId, - status: parsedStatus, - page: parsedPage, - pageSize: parsedPageSize, - q: parsedQuery, + status, + page, + pageSize, + q, }); - const totalPages = Math.ceil(total / parsedPageSize); + const totalPages = Math.ceil(total / pageSize); return ( @@ -192,10 +154,10 @@ export default async function Page(props: { {totalPages > 1 ? ( ) : null} diff --git a/apps/web/src/app/[locale]/(space)/polls/polls-tabbed-view.tsx b/apps/web/src/app/[locale]/(space)/polls/polls-tabbed-view.tsx index 854b6de1d..80fd86557 100644 --- a/apps/web/src/app/[locale]/(space)/polls/polls-tabbed-view.tsx +++ b/apps/web/src/app/[locale]/(space)/polls/polls-tabbed-view.tsx @@ -6,13 +6,15 @@ import { Trans } from "@/components/trans"; import { cn } from "@rallly/ui"; import React from "react"; +import { statusSchema } from "./schema"; export function PollsTabbedView({ children }: { children: React.ReactNode }) { const searchParams = useSearchParams(); const name = "status"; const router = useRouter(); const [isPending, startTransition] = React.useTransition(); - const [tab, setTab] = React.useState(searchParams.get(name) ?? "live"); + const status = statusSchema.parse(searchParams.get("status")); + const [tab, setTab] = React.useState(status); const handleTabChange = React.useCallback( (value: string) => { const params = new URLSearchParams(searchParams); @@ -20,7 +22,7 @@ export function PollsTabbedView({ children }: { children: React.ReactNode }) { params.delete("page"); - setTab(value); + setTab(statusSchema.parse(value)); startTransition(() => { const newUrl = `?${params.toString()}`; diff --git a/apps/web/src/app/[locale]/(space)/polls/schema.ts b/apps/web/src/app/[locale]/(space)/polls/schema.ts new file mode 100644 index 000000000..eac5fe4a5 --- /dev/null +++ b/apps/web/src/app/[locale]/(space)/polls/schema.ts @@ -0,0 +1,33 @@ +import { pollStatusSchema } from "@/features/poll/schema"; +import { z } from "zod"; + +export const DEFAULT_PAGE_SIZE = 10; + +export const pageSchema = z.coerce.number().optional().default(1); + +export const querySchema = z + .string() + .optional() + .transform((val) => val?.trim() || undefined); + +export const statusSchema = pollStatusSchema + .optional() + .catch("live") + .default("live"); + +export const pageSizeSchema = z.coerce + .number() + .optional() + .transform((val) => { + if (!val) return DEFAULT_PAGE_SIZE; + return Number.isNaN(val) || val < 1 + ? DEFAULT_PAGE_SIZE + : Math.min(val, 100); + }); + +export const searchParamsSchema = z.object({ + status: statusSchema, + page: pageSchema, + pageSize: pageSizeSchema, + q: querySchema, +}); diff --git a/apps/web/src/auth/queries.ts b/apps/web/src/auth/queries.ts index 1cb6098b1..21ae96fb4 100644 --- a/apps/web/src/auth/queries.ts +++ b/apps/web/src/auth/queries.ts @@ -37,19 +37,7 @@ export const requireAdmin = cache(async () => { }); export const getActiveSpace = cache(async () => { - const session = await auth(); + const user = await requireUser(); - if (!session?.user?.id) { - return null; - } - - const user = await getUser(session.user.id); - - if (!user) { - return null; - } - - const space = await getDefaultSpace({ ownerId: user.id }); - - return space; + return await getDefaultSpace({ ownerId: user.id }); }); diff --git a/apps/web/src/features/poll/api/get-polls.ts b/apps/web/src/features/poll/api/get-polls.ts index a032dbcda..37e74c21f 100644 --- a/apps/web/src/features/poll/api/get-polls.ts +++ b/apps/web/src/features/poll/api/get-polls.ts @@ -2,7 +2,7 @@ import type { PollStatus, Prisma } from "@rallly/database"; import { prisma } from "@rallly/database"; type PollFilters = { - userId: string; + spaceId: string; status?: PollStatus; page?: number; pageSize?: number; @@ -10,7 +10,7 @@ type PollFilters = { }; export async function getPolls({ - userId, + spaceId, status, page = 1, pageSize = 10, @@ -18,7 +18,7 @@ export async function getPolls({ }: PollFilters) { // Build the where clause based on filters const where: Prisma.PollWhereInput = { - userId, + spaceId, status, deleted: false, }; diff --git a/apps/web/src/features/scheduled-event/api/get-scheduled-events.ts b/apps/web/src/features/scheduled-event/queries.ts similarity index 74% rename from apps/web/src/features/scheduled-event/api/get-scheduled-events.ts rename to apps/web/src/features/scheduled-event/queries.ts index 5bb5ff7b5..9f59a4fec 100644 --- a/apps/web/src/features/scheduled-event/api/get-scheduled-events.ts +++ b/apps/web/src/features/scheduled-event/queries.ts @@ -1,13 +1,12 @@ +import { getActiveSpace } from "@/auth/queries"; import type { Prisma } from "@rallly/database"; import { prisma } from "@rallly/database"; import dayjs from "dayjs"; -import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; - -import type { Status } from "../schema"; +import { cache } from "react"; +import type { Status } from "./schema"; dayjs.extend(utc); -dayjs.extend(timezone); const mapStatus = { upcoming: "confirmed", @@ -16,26 +15,22 @@ const mapStatus = { canceled: "canceled", } as const; -export async function getScheduledEvents({ - userId, +function getEventsWhereInput({ + spaceId, status, search, - page = 1, - pageSize = 10, }: { - userId: string; + spaceId: string; status: Status; search?: string; - page?: number; - pageSize?: number; }) { const now = new Date(); - const todayStart = dayjs().startOf("day").toDate(); - const todayEnd = dayjs().endOf("day").toDate(); + const todayStart = dayjs().startOf("day").utc().toDate(); + const todayEnd = dayjs().endOf("day").utc().toDate(); const where: Prisma.ScheduledEventWhereInput = { - userId, + spaceId, deletedAt: null, ...(status === "upcoming" && { OR: [ @@ -53,6 +48,28 @@ export async function getScheduledEvents({ status: mapStatus[status], }; + return where; +} + +export async function getScheduledEvents({ + spaceId, + status, + search, + page = 1, + pageSize = 10, +}: { + spaceId: string; + status: Status; + search?: string; + page?: number; + pageSize?: number; +}) { + const where = getEventsWhereInput({ + spaceId, + status, + search, + }); + const [rawEvents, totalCount] = await Promise.all([ prisma.scheduledEvent.findMany({ where, @@ -101,3 +118,13 @@ export async function getScheduledEvents({ return { events, totalCount, totalPages, hasNextPage }; } + +export const getUpcomingEventsCount = cache(async () => { + const space = await getActiveSpace(); + return prisma.scheduledEvent.count({ + where: getEventsWhereInput({ + spaceId: space.id, + status: "upcoming", + }), + }); +}); diff --git a/apps/web/src/trpc/routers/polls.ts b/apps/web/src/trpc/routers/polls.ts index db9c00ce9..84ab32cd5 100644 --- a/apps/web/src/trpc/routers/polls.ts +++ b/apps/web/src/trpc/routers/polls.ts @@ -12,7 +12,7 @@ import { z } from "zod"; import { moderateContent } from "@/features/moderation"; import { getEmailClient } from "@/utils/emails"; -import { getActiveSpace } from "@/auth/queries"; +import { getDefaultSpace } from "@/features/spaces/queries"; import { getTimeZoneAbbreviation } from "../../utils/date"; import { createRateLimitMiddleware, @@ -180,8 +180,12 @@ export const polls = router({ const adminToken = nanoid(); const participantUrlId = nanoid(); const pollId = nanoid(); + let spaceId: string | undefined; - const space = await getActiveSpace(); + if (!ctx.user.isGuest) { + const space = await getDefaultSpace({ ownerId: ctx.user.id }); + spaceId = space.id; + } const poll = await prisma.poll.create({ select: { @@ -231,7 +235,7 @@ export const polls = router({ disableComments: input.disableComments, hideScores: input.hideScores, requireParticipantEmail: input.requireParticipantEmail, - spaceId: space?.id, + spaceId, }, });