♻️ 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 { 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,

View file

@ -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 {

View file

@ -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 (
<PageContainer>
@ -192,10 +154,10 @@ export default async function Page(props: {
</PollList>
{totalPages > 1 ? (
<Pagination
currentPage={parsedPage}
currentPage={page}
totalPages={totalPages}
totalItems={total}
pageSize={parsedPageSize}
pageSize={pageSize}
className="mt-4"
/>
) : null}

View file

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

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 () => {
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 });
});

View file

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

View file

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

View file

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