♻️ Fetch data from space (#1779)

This commit is contained in:
Luke Vella 2025-06-16 15:27:11 +02:00 committed by GitHub
parent 2fe17e7f32
commit dd9bdbcfc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 112 additions and 118 deletions

View file

@ -19,13 +19,13 @@ import {
import { Pagination } from "@/components/pagination"; import { Pagination } from "@/components/pagination";
import { StackedList, StackedListItem } from "@/components/stacked-list"; import { StackedList, StackedListItem } from "@/components/stacked-list";
import { Trans } from "@/components/trans"; 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 { 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 type { Status } from "@/features/scheduled-event/schema";
import { statusSchema } from "@/features/scheduled-event/schema"; import { statusSchema } from "@/features/scheduled-event/schema";
import { getTranslation } from "@/i18n/server"; import { getTranslation } from "@/i18n/server";
import { requireUser } from "@/next-auth";
import { getActiveSpace } from "@/auth/queries";
import { EventsTabbedView } from "./events-tabbed-view"; import { EventsTabbedView } from "./events-tabbed-view";
async function loadData({ async function loadData({
@ -39,9 +39,9 @@ async function loadData({
page?: number; page?: number;
pageSize?: number; pageSize?: number;
}) { }) {
const { userId } = await requireUser(); const space = await getActiveSpace();
return getScheduledEvents({ return getScheduledEvents({
userId, spaceId: space.id,
status, status,
search, search,
page, page,

View file

@ -17,48 +17,26 @@ import {
PageHeader, PageHeader,
PageTitle, PageTitle,
} from "@/app/components/page-layout"; } from "@/app/components/page-layout";
import { requireUser } from "@/auth/queries"; import { getActiveSpace } from "@/auth/queries";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { IfCloudHosted } from "@/contexts/environment"; import { IfCloudHosted } from "@/contexts/environment";
import { getUpcomingEventsCount } from "@/features/scheduled-event/queries";
import { getTranslation } from "@/i18n/server"; import { getTranslation } from "@/i18n/server";
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import dayjs from "dayjs";
import { FeedbackAlert } from "./feedback-alert"; import { FeedbackAlert } from "./feedback-alert";
async function loadData() { async function loadData() {
const user = await requireUser(); const space = await getActiveSpace();
if (!user) {
return {
livePollCount: 0,
upcomingEventCount: 0,
};
}
const todayStart = dayjs().startOf("day").toDate();
const todayEnd = dayjs().endOf("day").toDate();
const now = new Date();
const [livePollCount, upcomingEventCount] = await Promise.all([ const [livePollCount, upcomingEventCount] = await Promise.all([
prisma.poll.count({ prisma.poll.count({
where: { where: {
userId: user.id, spaceId: space.id,
status: "live", status: "live",
deleted: false, deleted: false,
}, },
}), }),
prisma.scheduledEvent.count({ getUpcomingEventsCount(),
where: {
userId: user.id,
OR: [
{ allDay: false, start: { gte: now } },
{
allDay: true,
start: { gte: todayStart, lte: todayEnd },
},
],
},
}),
]); ]);
return { return {

View file

@ -3,7 +3,6 @@ import { Button } from "@rallly/ui/button";
import { absoluteUrl, shortUrl } from "@rallly/utils/absolute-url"; import { absoluteUrl, shortUrl } from "@rallly/utils/absolute-url";
import { InboxIcon } from "lucide-react"; import { InboxIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { z } from "zod";
import { PollPageIcon } from "@/app/components/page-icons"; import { PollPageIcon } from "@/app/components/page-icons";
import { import {
@ -25,59 +24,27 @@ import { Trans } from "@/components/trans";
import { getPolls } from "@/features/poll/api/get-polls"; import { getPolls } from "@/features/poll/api/get-polls";
import { PollList, PollListItem } from "@/features/poll/components/poll-list"; import { PollList, PollListItem } from "@/features/poll/components/poll-list";
import { getTranslation } from "@/i18n/server"; import { getTranslation } from "@/i18n/server";
import { requireUser } from "@/next-auth";
import { getActiveSpace } from "@/auth/queries";
import { SearchInput } from "../../../components/search-input"; import { SearchInput } from "../../../components/search-input";
import { PollsTabbedView } from "./polls-tabbed-view"; import { PollsTabbedView } from "./polls-tabbed-view";
import { DEFAULT_PAGE_SIZE, searchParamsSchema } from "./schema";
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);
});
// Combined schema for type inference // Combined schema for type inference
async function loadData({ async function loadData({
userId,
status = "live", status = "live",
page = 1, page = 1,
pageSize = DEFAULT_PAGE_SIZE, pageSize = DEFAULT_PAGE_SIZE,
q, q,
}: { }: {
userId: string;
status?: PollStatus; status?: PollStatus;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
q?: string; q?: string;
}) { }) {
const space = await getActiveSpace();
const [{ total, data: polls }] = await Promise.all([ const [{ total, data: polls }] = await Promise.all([
getPolls({ userId, status, page, pageSize, q }), getPolls({ spaceId: space.id, status, page, pageSize, q }),
]); ]);
return { return {
@ -124,24 +91,19 @@ function PollsEmptyState() {
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const searchParams = await props.searchParams;
const { t } = await getTranslation(); const { t } = await getTranslation();
const { userId } = await requireUser();
const parsedStatus = statusSchema.parse(searchParams.status); const searchParams = await props.searchParams;
const parsedPage = pageSchema.parse(searchParams.page); const { status, page, pageSize, q } = searchParamsSchema.parse(searchParams);
const parsedPageSize = pageSizeSchema.parse(searchParams.pageSize);
const parsedQuery = querySchema.parse(searchParams.q);
const { polls, total } = await loadData({ const { polls, total } = await loadData({
userId, status,
status: parsedStatus, page,
page: parsedPage, pageSize,
pageSize: parsedPageSize, q,
q: parsedQuery,
}); });
const totalPages = Math.ceil(total / parsedPageSize); const totalPages = Math.ceil(total / pageSize);
return ( return (
<PageContainer> <PageContainer>
@ -192,10 +154,10 @@ export default async function Page(props: {
</PollList> </PollList>
{totalPages > 1 ? ( {totalPages > 1 ? (
<Pagination <Pagination
currentPage={parsedPage} currentPage={page}
totalPages={totalPages} totalPages={totalPages}
totalItems={total} totalItems={total}
pageSize={parsedPageSize} pageSize={pageSize}
className="mt-4" className="mt-4"
/> />
) : null} ) : null}

View file

@ -6,13 +6,15 @@ import { Trans } from "@/components/trans";
import { cn } from "@rallly/ui"; import { cn } from "@rallly/ui";
import React from "react"; import React from "react";
import { statusSchema } from "./schema";
export function PollsTabbedView({ children }: { children: React.ReactNode }) { export function PollsTabbedView({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const name = "status"; const name = "status";
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = React.useTransition(); 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( const handleTabChange = React.useCallback(
(value: string) => { (value: string) => {
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
@ -20,7 +22,7 @@ export function PollsTabbedView({ children }: { children: React.ReactNode }) {
params.delete("page"); params.delete("page");
setTab(value); setTab(statusSchema.parse(value));
startTransition(() => { startTransition(() => {
const newUrl = `?${params.toString()}`; const newUrl = `?${params.toString()}`;

View file

@ -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,
});

View file

@ -37,19 +37,7 @@ export const requireAdmin = cache(async () => {
}); });
export const getActiveSpace = cache(async () => { export const getActiveSpace = cache(async () => {
const session = await auth(); const user = await requireUser();
if (!session?.user?.id) { return await getDefaultSpace({ ownerId: user.id });
return null;
}
const user = await getUser(session.user.id);
if (!user) {
return null;
}
const space = await getDefaultSpace({ ownerId: user.id });
return space;
}); });

View file

@ -2,7 +2,7 @@ import type { PollStatus, Prisma } from "@rallly/database";
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
type PollFilters = { type PollFilters = {
userId: string; spaceId: string;
status?: PollStatus; status?: PollStatus;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
@ -10,7 +10,7 @@ type PollFilters = {
}; };
export async function getPolls({ export async function getPolls({
userId, spaceId,
status, status,
page = 1, page = 1,
pageSize = 10, pageSize = 10,
@ -18,7 +18,7 @@ export async function getPolls({
}: PollFilters) { }: PollFilters) {
// Build the where clause based on filters // Build the where clause based on filters
const where: Prisma.PollWhereInput = { const where: Prisma.PollWhereInput = {
userId, spaceId,
status, status,
deleted: false, deleted: false,
}; };

View file

@ -1,13 +1,12 @@
import { getActiveSpace } from "@/auth/queries";
import type { Prisma } from "@rallly/database"; import type { Prisma } from "@rallly/database";
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import dayjs from "dayjs"; import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { cache } from "react";
import type { Status } from "../schema"; import type { Status } from "./schema";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone);
const mapStatus = { const mapStatus = {
upcoming: "confirmed", upcoming: "confirmed",
@ -16,26 +15,22 @@ const mapStatus = {
canceled: "canceled", canceled: "canceled",
} as const; } as const;
export async function getScheduledEvents({ function getEventsWhereInput({
userId, spaceId,
status, status,
search, search,
page = 1,
pageSize = 10,
}: { }: {
userId: string; spaceId: string;
status: Status; status: Status;
search?: string; search?: string;
page?: number;
pageSize?: number;
}) { }) {
const now = new Date(); const now = new Date();
const todayStart = dayjs().startOf("day").toDate(); const todayStart = dayjs().startOf("day").utc().toDate();
const todayEnd = dayjs().endOf("day").toDate(); const todayEnd = dayjs().endOf("day").utc().toDate();
const where: Prisma.ScheduledEventWhereInput = { const where: Prisma.ScheduledEventWhereInput = {
userId, spaceId,
deletedAt: null, deletedAt: null,
...(status === "upcoming" && { ...(status === "upcoming" && {
OR: [ OR: [
@ -53,6 +48,28 @@ export async function getScheduledEvents({
status: mapStatus[status], 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([ const [rawEvents, totalCount] = await Promise.all([
prisma.scheduledEvent.findMany({ prisma.scheduledEvent.findMany({
where, where,
@ -101,3 +118,13 @@ export async function getScheduledEvents({
return { events, totalCount, totalPages, hasNextPage }; 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",
}),
});
});

View file

@ -12,7 +12,7 @@ import { z } from "zod";
import { moderateContent } from "@/features/moderation"; import { moderateContent } from "@/features/moderation";
import { getEmailClient } from "@/utils/emails"; import { getEmailClient } from "@/utils/emails";
import { getActiveSpace } from "@/auth/queries"; import { getDefaultSpace } from "@/features/spaces/queries";
import { getTimeZoneAbbreviation } from "../../utils/date"; import { getTimeZoneAbbreviation } from "../../utils/date";
import { import {
createRateLimitMiddleware, createRateLimitMiddleware,
@ -180,8 +180,12 @@ export const polls = router({
const adminToken = nanoid(); const adminToken = nanoid();
const participantUrlId = nanoid(); const participantUrlId = nanoid();
const pollId = 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({ const poll = await prisma.poll.create({
select: { select: {
@ -231,7 +235,7 @@ export const polls = router({
disableComments: input.disableComments, disableComments: input.disableComments,
hideScores: input.hideScores, hideScores: input.hideScores,
requireParticipantEmail: input.requireParticipantEmail, requireParticipantEmail: input.requireParticipantEmail,
spaceId: space?.id, spaceId,
}, },
}); });