mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-06 19:17:27 +02:00
♻️ Fetch data from space (#1779)
This commit is contained in:
parent
2fe17e7f32
commit
dd9bdbcfc4
9 changed files with 112 additions and 118 deletions
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()}`;
|
||||
|
|
33
apps/web/src/app/[locale]/(space)/polls/schema.ts
Normal file
33
apps/web/src/app/[locale]/(space)/polls/schema.ts
Normal 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,
|
||||
});
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
}),
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue