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 { 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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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()}`;
|
||||||
|
|
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 () => {
|
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;
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue