mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-10 23:46:49 +02:00
✨ New and Improved Screens (#1151)
This commit is contained in:
parent
5461c57228
commit
997a1eec78
75 changed files with 1517 additions and 743 deletions
|
@ -5,8 +5,11 @@ SECRET_PASSWORD=abcdef1234567890abcdef1234567890
|
|||
# Example: https://example.com
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
|
||||
# NEXTAUTH_URL should be the same as NEXT_PUBLIC_BASE_URL
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# A connection string to your Postgres database
|
||||
DATABASE_URL="postgres://postgres:postgres@rallly_db:5450/rallly"
|
||||
DATABASE_URL="postgres://postgres:postgres@localhost:5450/rallly"
|
||||
|
||||
# Required to be able to send emails
|
||||
SUPPORT_EMAIL=support@rallly.co
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -26,10 +26,7 @@ yarn-error.log*
|
|||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env*.local
|
||||
|
||||
# ts
|
||||
tsconfig.tsbuildinfo
|
||||
|
|
|
@ -46,11 +46,6 @@ const nextConfig = {
|
|||
destination: "/settings/profile",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/",
|
||||
destination: "/polls",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"lint:tsc": "tsc --noEmit",
|
||||
"i18n:scan": "i18next-scanner --config i18next-scanner.config.js",
|
||||
"prettier": "prettier --write ./src",
|
||||
"test:integration": "playwright test",
|
||||
"test:integration": "NODE_ENV=test playwright test",
|
||||
"test:unit": "vitest run",
|
||||
"test": "yarn test:unit && yarn test:e2e",
|
||||
"test:codegen": "playwright codegen http://localhost:3000",
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { loadEnvConfig } from "@next/env";
|
||||
import { devices, PlaywrightTestConfig } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
|
||||
const ci = process.env.CI === "true";
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, ".env.test") });
|
||||
loadEnvConfig(process.cwd());
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
const port = process.env.PORT || 3002;
|
||||
// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port
|
||||
const baseURL = `http://localhost:${port}`;
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"copied": "Copied",
|
||||
"createAnAccount": "Create an account",
|
||||
"createdBy": "by <b>{name}</b>",
|
||||
"createPoll": "Create poll",
|
||||
"delete": "Delete",
|
||||
"deleteDate": "Delete date",
|
||||
"deletedPoll": "Deleted poll",
|
||||
|
@ -209,9 +208,6 @@
|
|||
"continueAs": "Continue as",
|
||||
"pageMovedDescription": "Redirecting to <a>{newUrl}</a>",
|
||||
"notRegistered": "Don't have an account? <a>Register</a>",
|
||||
"comingSoon": "Coming Soon",
|
||||
"integrations": "Integrations",
|
||||
"contacts": "Contacts",
|
||||
"unlockFeatures": "Unlock all Pro features.",
|
||||
"pollStatusFinalized": "Finalized",
|
||||
"share": "Share",
|
||||
|
@ -222,7 +218,6 @@
|
|||
"aboutGuestDescription": "Profile settings are not available for guest users. <0>Sign in</0> to your existing account or <1>create a new account</1> to customize your profile.",
|
||||
"logoutDescription": "Sign out of your existing session",
|
||||
"events": "Events",
|
||||
"registrations": "Registrations",
|
||||
"inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.",
|
||||
"inviteLink": "Invite Link",
|
||||
"inviteParticipantLinkInfo": "Anyone with this link will be able to vote on your poll.",
|
||||
|
@ -233,14 +228,8 @@
|
|||
"autoTimeZoneHelp": "Enable this setting to automatically adjust event times to each participant's local time zone.",
|
||||
"commentsDisabled": "Comments have been disabled",
|
||||
"allParticipants": "All Participants",
|
||||
"host": "Host",
|
||||
"created": "Created",
|
||||
"pollStatus": "Status",
|
||||
"pollsListAll": "All",
|
||||
"pollsListMine": "Mine",
|
||||
"pollsListOther": "Other",
|
||||
"noParticipantsDescription": "Click <b>Share</b> to invite participants",
|
||||
"back": "Back",
|
||||
"timeShownIn": "Times shown in {timeZone}",
|
||||
"pollStatusPausedDescription": "Votes cannot be submitted or edited at this time",
|
||||
"eventHostTitle": "Manage Access",
|
||||
|
@ -261,5 +250,18 @@
|
|||
"advancedSettingsDescription": "Hide participants, hide scores, require participant email address.",
|
||||
"keepPollsIndefinitely": "Keep Polls Indefinitely",
|
||||
"keepPollsIndefinitelyDescription": "Inactive polls will not be auto-deleted.",
|
||||
"verificationCodeSentTo": "We sent a verification code to <b>{{ email }}</b>"
|
||||
"verificationCodeSentTo": "We sent a verification code to <b>{{ email }}</b>",
|
||||
"home": "Home",
|
||||
"groupPoll": "Group Poll",
|
||||
"groupPollDescription": "Share your availability with a group of people and find the best time to meet.",
|
||||
"create": "Create",
|
||||
"upcoming": "Upcoming",
|
||||
"past": "Past",
|
||||
"copyLink": "Copy Link",
|
||||
"upcomingEventsEmptyStateTitle": "No Upcoming Events",
|
||||
"upcomingEventsEmptyStateDescription": "When you schedule events, they will appear here.",
|
||||
"pastEventsEmptyStateTitle": "No Past Events",
|
||||
"pastEventsEmptyStateDescription": "When you schedule events, they will appear here.",
|
||||
"activePollCount": "{{activePollCount}} Live",
|
||||
"createPoll": "Create poll"
|
||||
}
|
||||
|
|
115
apps/web/src/app/[locale]/(admin)/app-card.tsx
Normal file
115
apps/web/src/app/[locale]/(admin)/app-card.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
import { cn } from "@rallly/ui";
|
||||
import { BarChart2Icon } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
import { Squircle } from "@/app/components/squircle";
|
||||
|
||||
export function AppCard({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-col justify-between rounded-lg border bg-white p-4 shadow-sm ring-1 ring-inset ring-white/50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppCardContent({ children }: { children?: React.ReactNode }) {
|
||||
return <div className="">{children}</div>;
|
||||
}
|
||||
|
||||
export function GroupPollIcon({
|
||||
size = "md",
|
||||
}: {
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
}) {
|
||||
return (
|
||||
<Squircle
|
||||
aria-label="Group Poll"
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center bg-gradient-to-br from-purple-500 to-violet-500 text-purple-100",
|
||||
{
|
||||
"size-6": size === "xs",
|
||||
"size-8": size === "sm",
|
||||
"size-9": size === "md",
|
||||
"size-10": size === "lg",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<BarChart2Icon
|
||||
className={cn({
|
||||
"size-4": size === "sm" || size === "xs",
|
||||
"size-5": size === "md",
|
||||
"size-6": size === "lg",
|
||||
})}
|
||||
/>
|
||||
</Squircle>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppCardIcon({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative mb-4 inline-flex size-12 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppCardName({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <h2 className={cn("font-semibold", className)}>{children}</h2>;
|
||||
}
|
||||
|
||||
export function AppCardDescription({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"text-muted-foreground mt-1 text-sm leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppCardFooter({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("mt-6 border-t pt-3", className)}>{children}</div>;
|
||||
}
|
75
apps/web/src/app/[locale]/(admin)/dashboard.tsx
Normal file
75
apps/web/src/app/[locale]/(admin)/dashboard.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import {
|
||||
AppCard,
|
||||
AppCardContent,
|
||||
AppCardDescription,
|
||||
AppCardFooter,
|
||||
AppCardIcon,
|
||||
AppCardName,
|
||||
GroupPollIcon,
|
||||
} from "@/app/[locale]/(admin)/app-card";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data } = trpc.dashboard.info.useQuery();
|
||||
|
||||
if (!data) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<AppCard>
|
||||
<AppCardIcon>
|
||||
<GroupPollIcon size="lg" />
|
||||
</AppCardIcon>
|
||||
<AppCardContent>
|
||||
<div>
|
||||
<AppCardName>
|
||||
<Trans i18nKey="groupPoll" defaults="Group Poll" />
|
||||
</AppCardName>
|
||||
<AppCardDescription>
|
||||
<Trans
|
||||
i18nKey="groupPollDescription"
|
||||
defaults="Share your availability with a group of people and find the best time to meet."
|
||||
/>
|
||||
</AppCardDescription>
|
||||
</div>
|
||||
</AppCardContent>
|
||||
<AppCardFooter className="flex items-center justify-between gap-4">
|
||||
<div className="inline-flex items-center gap-1 text-sm">
|
||||
<Link
|
||||
className="text-primary font-medium hover:underline"
|
||||
href="/polls?status=live"
|
||||
>
|
||||
<Trans
|
||||
i18nKey="activePollCount"
|
||||
defaults="{{activePollCount}} Live"
|
||||
values={{
|
||||
activePollCount: data.activePollCount,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href="/new">
|
||||
<Icon>
|
||||
<PlusIcon />
|
||||
</Icon>
|
||||
<Trans i18nKey="create" defaults="Create" />
|
||||
</Link>
|
||||
</Button>
|
||||
</AppCardFooter>
|
||||
</AppCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
79
apps/web/src/app/[locale]/(admin)/events/event-list.tsx
Normal file
79
apps/web/src/app/[locale]/(admin)/events/event-list.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@rallly/ui/card";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ScheduledEvent } from "@/app/[locale]/(admin)/events/types";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { generateGradient } from "@/utils/color-hash";
|
||||
import { useDayjs } from "@/utils/dayjs";
|
||||
|
||||
export function EventList({ data }: { data: ScheduledEvent[] }) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: [],
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const { adjustTimeZone } = useDayjs();
|
||||
return (
|
||||
<Card>
|
||||
<ul className="divide-y divide-gray-100">
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
const start = adjustTimeZone(
|
||||
row.original.start,
|
||||
!row.original.timeZone,
|
||||
);
|
||||
|
||||
const end = adjustTimeZone(
|
||||
dayjs(row.original.start).add(row.original.duration, "minutes"),
|
||||
!row.original.timeZone,
|
||||
);
|
||||
return (
|
||||
<li key={row.id}>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:gap-8">
|
||||
<div className="flex shrink-0 justify-between gap-1 sm:w-24 sm:flex-col sm:text-right">
|
||||
<time
|
||||
dateTime={start.toISOString()}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
{start.format("ddd, D MMM")}
|
||||
</time>
|
||||
<time
|
||||
dateTime={start.toISOString()}
|
||||
className="text-muted-foreground text-sm"
|
||||
>
|
||||
{start.format("YYYY")}
|
||||
</time>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span
|
||||
className="h-4 w-1 shrink-0 rounded-full"
|
||||
style={{
|
||||
background: generateGradient(row.original.id),
|
||||
}}
|
||||
></span>
|
||||
<h2 className="truncate text-base font-semibold">
|
||||
{row.original.title}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
{row.original.duration === 0 ? (
|
||||
<Trans i18nKey="allDay" />
|
||||
) : (
|
||||
<span>{`${start.format("LT")} - ${end.format("LT")}`}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
3
apps/web/src/app/[locale]/(admin)/events/layout.tsx
Normal file
3
apps/web/src/app/[locale]/(admin)/events/layout.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Layout({ children }: { children?: React.ReactNode }) {
|
||||
return <div>{children}</div>;
|
||||
}
|
|
@ -1,34 +1,28 @@
|
|||
import { Button } from "@rallly/ui/button";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Trans } from "react-i18next/TransWithoutContext";
|
||||
|
||||
import { UserScheduledEvents } from "@/app/[locale]/(admin)/events/user-scheduled-events";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import {
|
||||
PageContainer,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
} from "@/app/components/page-layout";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
import { CreatePoll } from "@/components/create-poll";
|
||||
|
||||
export default async function Page({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Button asChild>
|
||||
<Link href="/polls">
|
||||
<Icon>
|
||||
<ArrowLeftIcon />
|
||||
</Icon>
|
||||
<Trans i18nKey="back" t={t} defaults="Back" />
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<PageTitle>
|
||||
{t("events", {
|
||||
defaultValue: "Events",
|
||||
})}
|
||||
</PageTitle>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<CreatePoll />
|
||||
<UserScheduledEvents />
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
|
@ -41,6 +35,8 @@ export async function generateMetadata({
|
|||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("newPoll"),
|
||||
title: t("events", {
|
||||
defaultValue: "Events",
|
||||
}),
|
||||
};
|
||||
}
|
47
apps/web/src/app/[locale]/(admin)/events/past-events.tsx
Normal file
47
apps/web/src/app/[locale]/(admin)/events/past-events.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
"use client";
|
||||
import { CalendarPlusIcon } from "lucide-react";
|
||||
|
||||
import { EventList } from "@/app/[locale]/(admin)/events/event-list";
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateDescription,
|
||||
EmptyStateIcon,
|
||||
EmptyStateTitle,
|
||||
} from "@/app/components/empty-state";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
export function PastEvents() {
|
||||
const { data } = trpc.scheduledEvents.list.useQuery({
|
||||
period: "past",
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<EmptyState className="h-96">
|
||||
<EmptyStateIcon>
|
||||
<CalendarPlusIcon />
|
||||
</EmptyStateIcon>
|
||||
<EmptyStateTitle>
|
||||
<Trans
|
||||
i18nKey="pastEventsEmptyStateTitle"
|
||||
defaults="No Past Events"
|
||||
/>
|
||||
</EmptyStateTitle>
|
||||
<EmptyStateDescription>
|
||||
<Trans
|
||||
i18nKey="pastEventsEmptyStateDescription"
|
||||
defaults="When you schedule events, they will appear here."
|
||||
/>
|
||||
</EmptyStateDescription>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
return <EventList data={data} />;
|
||||
}
|
8
apps/web/src/app/[locale]/(admin)/events/types.ts
Normal file
8
apps/web/src/app/[locale]/(admin)/events/types.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export type ScheduledEvent = {
|
||||
id: string;
|
||||
title: string;
|
||||
start: Date;
|
||||
duration: number;
|
||||
timeZone: string | null;
|
||||
participants: { name: string }[];
|
||||
};
|
45
apps/web/src/app/[locale]/(admin)/events/upcoming-events.tsx
Normal file
45
apps/web/src/app/[locale]/(admin)/events/upcoming-events.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
"use client";
|
||||
import { CalendarPlusIcon } from "lucide-react";
|
||||
|
||||
import { EventList } from "@/app/[locale]/(admin)/events/event-list";
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateDescription,
|
||||
EmptyStateIcon,
|
||||
EmptyStateTitle,
|
||||
} from "@/app/components/empty-state";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
export function UpcomingEvents() {
|
||||
const { data } = trpc.scheduledEvents.list.useQuery({ period: "upcoming" });
|
||||
|
||||
if (!data) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<EmptyState className="h-96">
|
||||
<EmptyStateIcon>
|
||||
<CalendarPlusIcon />
|
||||
</EmptyStateIcon>
|
||||
<EmptyStateTitle>
|
||||
<Trans
|
||||
i18nKey="upcomingEventsEmptyStateTitle"
|
||||
defaults="No Upcoming Events"
|
||||
/>
|
||||
</EmptyStateTitle>
|
||||
<EmptyStateDescription>
|
||||
<Trans
|
||||
i18nKey="upcomingEventsEmptyStateDescription"
|
||||
defaults="When you schedule events, they will appear here."
|
||||
/>
|
||||
</EmptyStateDescription>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
return <EventList data={data} />;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import { RadioCards, RadioCardsItem } from "@rallly/ui/radio-pills";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PastEvents } from "@/app/[locale]/(admin)/events/past-events";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
import { UpcomingEvents } from "./upcoming-events";
|
||||
|
||||
const eventPeriodSchema = z.enum(["upcoming", "past"]).catch("upcoming");
|
||||
|
||||
export function UserScheduledEvents() {
|
||||
const searchParams = useSearchParams();
|
||||
const period = eventPeriodSchema.parse(searchParams?.get("period"));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<RadioCards
|
||||
value={period}
|
||||
onValueChange={(value) => {
|
||||
const newParams = new URLSearchParams(searchParams?.toString());
|
||||
newParams.set("period", value);
|
||||
window.history.pushState(null, "", `?${newParams.toString()}`);
|
||||
}}
|
||||
>
|
||||
<RadioCardsItem value="upcoming">
|
||||
<Trans i18nKey="upcoming" defaults="Upcoming" />
|
||||
</RadioCardsItem>
|
||||
<RadioCardsItem value="past">
|
||||
<Trans i18nKey="past" defaults="Past" />
|
||||
</RadioCardsItem>
|
||||
</RadioCards>
|
||||
</div>
|
||||
<div>
|
||||
{period === "upcoming" && <UpcomingEvents />}
|
||||
{period === "past" && <PastEvents />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -11,11 +11,10 @@ export default async function Layout({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-gray-100">
|
||||
<MobileNavigation />
|
||||
<div className="flex flex-col pb-16 md:pb-0">
|
||||
<div
|
||||
className={cn(
|
||||
"inset-y-0 z-50 hidden shrink-0 flex-col gap-y-5 overflow-y-auto px-5 py-4 lg:fixed lg:flex lg:w-72 lg:px-6 lg:py-4",
|
||||
"fixed inset-y-0 z-50 hidden w-72 shrink-0 flex-col gap-y-4 overflow-y-auto p-6 py-5 md:flex",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
|
@ -23,9 +22,12 @@ export default async function Layout({
|
|||
</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className={cn("min-h-screen grow space-y-4 lg:ml-72")}>
|
||||
<div className={cn("grow space-y-4 p-3 md:ml-72 md:p-4 lg:px-8 lg:pb-8")}>
|
||||
{children}
|
||||
</div>
|
||||
<div className="fixed bottom-0 z-20 flex h-16 w-full flex-col justify-center bg-gray-100/90 backdrop-blur-md md:hidden">
|
||||
<MobileNavigation />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,27 @@
|
|||
import { Sidebar } from "@/app/[locale]/(admin)/sidebar";
|
||||
import { Trans } from "react-i18next/TransWithoutContext";
|
||||
|
||||
export default function Page() {
|
||||
import { Sidebar } from "@/app/[locale]/(admin)/sidebar";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import {
|
||||
PageContainer,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
} from "@/app/components/page-layout";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default async function Page({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return (
|
||||
<div className="p-3">
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
<Trans t={t} i18nKey="menu" defaults="Menu" />
|
||||
</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent className="px-2">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,27 +1,77 @@
|
|||
"use client";
|
||||
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
BarChart2Icon,
|
||||
CalendarIcon,
|
||||
HomeIcon,
|
||||
MenuIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { MobileMenuButton } from "@/app/[locale]/(admin)/menu/menu-button";
|
||||
import { CurrentUserAvatar } from "@/components/user";
|
||||
function MobileNavigationIcon({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Slot className="group-[.is-active]:text-primary group-focus:text-primary group-hover:text-foreground size-5 text-gray-500">
|
||||
{children}
|
||||
</Slot>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNavigationItem({
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
href: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"group flex grow basis-1/5 flex-col items-center gap-1 rounded-lg",
|
||||
{
|
||||
"is-active pointer-events-none": pathname === href,
|
||||
},
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileNavigation() {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isOpen = pathname === "/menu";
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-20 flex h-12 items-center justify-between border-b bg-gray-100 px-2 lg:hidden lg:px-4">
|
||||
<MobileMenuButton open={isOpen} />
|
||||
<div className="flex justify-end gap-x-2.5">
|
||||
<Button asChild variant="ghost">
|
||||
<Link href="/settings/profile">
|
||||
<CurrentUserAvatar size="xs" />
|
||||
<div className="flex items-center justify-between gap-x-4">
|
||||
<MobileNavigationItem href="/">
|
||||
<MobileNavigationIcon>
|
||||
<HomeIcon />
|
||||
</MobileNavigationIcon>
|
||||
</MobileNavigationItem>
|
||||
<MobileNavigationItem href="/polls">
|
||||
<MobileNavigationIcon>
|
||||
<BarChart2Icon />
|
||||
</MobileNavigationIcon>
|
||||
</MobileNavigationItem>
|
||||
<Button asChild variant="primary">
|
||||
<Link href="/new">
|
||||
<PlusIcon className="size-5 text-white" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<MobileNavigationItem href="/events">
|
||||
<MobileNavigationIcon>
|
||||
<CalendarIcon />
|
||||
</MobileNavigationIcon>
|
||||
</MobileNavigationItem>
|
||||
<MobileNavigationItem href="/menu">
|
||||
<MobileNavigationIcon>
|
||||
<MenuIcon />
|
||||
</MobileNavigationIcon>
|
||||
</MobileNavigationItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
49
apps/web/src/app/[locale]/(admin)/page.tsx
Normal file
49
apps/web/src/app/[locale]/(admin)/page.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { HomeIcon } from "lucide-react";
|
||||
import { Trans } from "react-i18next/TransWithoutContext";
|
||||
|
||||
import Dashboard from "@/app/[locale]/(admin)/dashboard";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import {
|
||||
PageContainer,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageIcon,
|
||||
PageTitle,
|
||||
} from "@/app/components/page-layout";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default async function Page({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return (
|
||||
<div>
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<PageIcon>
|
||||
<HomeIcon />
|
||||
</PageIcon>
|
||||
<PageTitle>
|
||||
<Trans t={t} i18nKey="home" defaults="Home" />
|
||||
</PageTitle>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<Dashboard />
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("home", {
|
||||
defaultValue: "Home",
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
import { PollStatus } from "@rallly/database";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
import { BarChart2Icon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PollStatusBadge } from "@/components/poll-status";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { UserAvatar } from "@/components/user";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { useDayjs } from "@/utils/dayjs";
|
||||
|
||||
export type PollData = {
|
||||
id: string;
|
||||
status: PollStatus;
|
||||
title: string;
|
||||
createdAt: Date;
|
||||
participants: { name: string }[];
|
||||
timeZone: string | null;
|
||||
userId: string;
|
||||
user: {
|
||||
name: string;
|
||||
id: string;
|
||||
} | null;
|
||||
event: {
|
||||
start: Date;
|
||||
duration: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<PollData>();
|
||||
|
||||
export const usePollColumns = () => {
|
||||
const { t } = useTranslation();
|
||||
const { adjustTimeZone } = useDayjs();
|
||||
const { user } = useUser();
|
||||
return React.useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("title", {
|
||||
id: "title",
|
||||
header: t("title"),
|
||||
size: 400,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/invite/${row.original.id}`}
|
||||
className="focus:text-primary group inset-0 flex h-9 min-w-0 items-center gap-x-2.5 whitespace-nowrap rounded-md px-2.5 text-sm font-medium hover:underline focus:bg-gray-200"
|
||||
>
|
||||
<Icon>
|
||||
<BarChart2Icon />
|
||||
</Icon>
|
||||
<span className="min-w-0 truncate">{row.original.title}</span>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("user", {
|
||||
header: () => (
|
||||
<div className="text-center">
|
||||
{t("host", { defaultValue: "Host" })}
|
||||
</div>
|
||||
),
|
||||
size: 75,
|
||||
cell: ({ getValue }) => {
|
||||
const isYou = getValue()?.id === user.id;
|
||||
return (
|
||||
<div className="text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<UserAvatar size="xs" name={getValue()?.name} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isYou ? t("you") : getValue()?.name ?? t("guest")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: () => <Trans i18nKey="created" defaults="Created" />,
|
||||
cell: ({ row }) => {
|
||||
const { createdAt } = row.original;
|
||||
return (
|
||||
<p className="text-muted-foreground whitespace-nowrap text-sm">
|
||||
<time dateTime={createdAt.toDateString()}>
|
||||
{dayjs(createdAt).fromNow()}
|
||||
</time>
|
||||
</p>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: t("pollStatus", { defaultValue: "Status" }),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-muted-foreground flex text-sm">
|
||||
{row.original.event ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<PollStatusBadge status={row.original.status} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{adjustTimeZone(
|
||||
row.original.event.start,
|
||||
!row.original.timeZone,
|
||||
).format(row.original.event.duration === 0 ? "LL" : "LLLL")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<PollStatusBadge status={row.original.status} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
columnHelper.accessor("participants", {
|
||||
header: () => null,
|
||||
cell: ({ row }) => {
|
||||
if (row.original.userId !== user.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button size="sm" asChild>
|
||||
<Link
|
||||
className="focus:bg-gray-200"
|
||||
href={`/poll/${row.original.id}`}
|
||||
>
|
||||
<Trans i18nKey="manage" />
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
[adjustTimeZone, t, user.id],
|
||||
);
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
export default function Loader() {
|
||||
return null;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import { PollsList } from "@/app/[locale]/(admin)/polls/[[...list]]/polls-list";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
interface PageParams extends Params {
|
||||
list?: string;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: PageParams }) {
|
||||
const list = params.list ? params.list[0] : "all";
|
||||
return <PollsList list={list} />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("polls"),
|
||||
};
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ResponsiveMenu,
|
||||
ResponsiveMenuItem,
|
||||
} from "@/app/components/responsive-menu";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
export function PollFolders() {
|
||||
return (
|
||||
<ResponsiveMenu>
|
||||
<ResponsiveMenuItem href="/polls">
|
||||
<Trans i18nKey="pollsListAll" defaults="All" />
|
||||
</ResponsiveMenuItem>
|
||||
<ResponsiveMenuItem href="/polls/mine">
|
||||
<Trans i18nKey="pollsListMine" defaults="Mine" />
|
||||
</ResponsiveMenuItem>
|
||||
<ResponsiveMenuItem href="/polls/other">
|
||||
<Trans i18nKey="pollsListOther" defaults="Other" />
|
||||
</ResponsiveMenuItem>
|
||||
</ResponsiveMenu>
|
||||
);
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Card } from "@rallly/ui/card";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { PaginationState } from "@tanstack/react-table";
|
||||
import { BarChart2Icon, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateDescription,
|
||||
EmptyStateFooter,
|
||||
EmptyStateIcon,
|
||||
EmptyStateTitle,
|
||||
} from "@/app/components/empty-state";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { Table } from "@/components/table";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
import { PollData, usePollColumns } from "./columns";
|
||||
|
||||
function PollsEmptyState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<EmptyState className="h-96 rounded-lg border-2 border-dashed">
|
||||
<EmptyStateIcon>
|
||||
<BarChart2Icon />
|
||||
</EmptyStateIcon>
|
||||
<EmptyStateTitle>
|
||||
{t("noPolls", { defaultValue: "No Polls" })}
|
||||
</EmptyStateTitle>
|
||||
<EmptyStateDescription>{t("noPollsDescription")}</EmptyStateDescription>
|
||||
<EmptyStateFooter>
|
||||
<Button variant="primary" asChild>
|
||||
<Link href="/new">
|
||||
<Icon>
|
||||
<PlusIcon />
|
||||
</Icon>
|
||||
<Trans i18nKey="newPoll" />
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
export function PollsList({ list }: { list?: string }) {
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const router = useRouter();
|
||||
const pagination = React.useMemo<PaginationState>(
|
||||
() => ({
|
||||
pageIndex: (Number(searchParams?.get("page")) || 1) - 1,
|
||||
pageSize: Number(searchParams?.get("pageSize")) || 10,
|
||||
}),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
// const sorting = React.useMemo<SortingState>(() => {
|
||||
// const id = searchParams?.get("sort");
|
||||
// const desc = searchParams?.get("desc");
|
||||
// if (!id) {
|
||||
// return [{ id: "createdAt", desc: true }];
|
||||
// }
|
||||
// return [{ id, desc: desc === "desc" }];
|
||||
// }, [searchParams]);
|
||||
|
||||
const { data, isFetching } = trpc.polls.paginatedList.useQuery(
|
||||
{ list, pagination },
|
||||
{
|
||||
staleTime: Infinity,
|
||||
cacheTime: Infinity,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
const columns = usePollColumns();
|
||||
|
||||
if (!data) {
|
||||
// return a table using <Skeleton /> components
|
||||
return (
|
||||
<div className="flex h-96 items-center justify-center">
|
||||
<Spinner className="text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{data.total ? (
|
||||
<Card>
|
||||
<Table
|
||||
className={isFetching ? "opacity-50" : undefined}
|
||||
layout="auto"
|
||||
paginationState={pagination}
|
||||
enableTableHeader={true}
|
||||
data={data.rows as PollData[]}
|
||||
pageCount={Math.ceil(data.total / pagination.pageSize)}
|
||||
// sortingState={sorting}
|
||||
// onSortingChange={(updater) => {
|
||||
// const newSorting =
|
||||
// typeof updater === "function" ? updater(sorting) : updater;
|
||||
|
||||
// const current = new URLSearchParams(searchParams ?? undefined);
|
||||
// const sortColumn = newSorting[0];
|
||||
// if (sortColumn === undefined) {
|
||||
// current.delete("sort");
|
||||
// current.delete("desc");
|
||||
// } else {
|
||||
// current.set("sort", sortColumn.id);
|
||||
// current.set("desc", sortColumn.desc ? "desc" : "asc");
|
||||
// }
|
||||
// // current.set("pageSize", String(newPagination.pageSize));
|
||||
// router.replace(`${pathname}?${current.toString()}`);
|
||||
// }}
|
||||
onPaginationChange={(updater) => {
|
||||
const newPagination =
|
||||
typeof updater === "function" ? updater(pagination) : updater;
|
||||
|
||||
const current = new URLSearchParams(searchParams ?? undefined);
|
||||
current.set("page", String(newPagination.pageIndex + 1));
|
||||
// current.set("pageSize", String(newPagination.pageSize));
|
||||
router.replace(`${pathname}?${current.toString()}`);
|
||||
}}
|
||||
columns={columns}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<PollsEmptyState />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import { Button } from "@rallly/ui/button";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { PollFolders } from "@/app/[locale]/(admin)/polls/[[...list]]/polls-folders";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import {
|
||||
PageContainer,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
} from "@/app/components/page-layout";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
interface PageParams extends Params {
|
||||
list?: string;
|
||||
}
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
params: PageParams;
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<div className="flex items-center gap-x-2.5">
|
||||
<PageTitle>{t("polls")}</PageTitle>
|
||||
<Button size="sm" asChild>
|
||||
<Link href="/new">
|
||||
<Icon>
|
||||
<PlusIcon />
|
||||
</Icon>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent className="space-y-3 lg:space-y-4">
|
||||
<PollFolders />
|
||||
{children}
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
53
apps/web/src/app/[locale]/(admin)/polls/page.tsx
Normal file
53
apps/web/src/app/[locale]/(admin)/polls/page.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { BarChart2Icon } from "lucide-react";
|
||||
|
||||
import { UserPolls } from "@/app/[locale]/(admin)/polls/user-polls";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import {
|
||||
PageContainer,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageIcon,
|
||||
PageTitle,
|
||||
} from "@/app/components/page-layout";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Params;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<PageIcon>
|
||||
<BarChart2Icon />
|
||||
</PageIcon>
|
||||
<PageTitle>
|
||||
{t("polls", {
|
||||
defaultValue: "Polls",
|
||||
})}
|
||||
</PageTitle>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<UserPolls />
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("polls", {
|
||||
defaultValue: "Polls",
|
||||
}),
|
||||
};
|
||||
}
|
237
apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx
Normal file
237
apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx
Normal file
|
@ -0,0 +1,237 @@
|
|||
"use client";
|
||||
import { PollStatus } from "@rallly/database";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { RadioCards, RadioCardsItem } from "@rallly/ui/radio-pills";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
import { CalendarPlusIcon, CheckIcon, LinkIcon, UserIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
import useCopyToClipboard from "react-use/lib/useCopyToClipboard";
|
||||
import { z } from "zod";
|
||||
|
||||
import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card";
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateDescription,
|
||||
EmptyStateIcon,
|
||||
EmptyStateTitle,
|
||||
} from "@/app/components/empty-state";
|
||||
import { PollStatusBadge } from "@/components/poll-status";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
function PollCount({ count }: { count?: number }) {
|
||||
return <span className="font-semibold">{count || 0}</span>;
|
||||
}
|
||||
|
||||
function FilteredPolls({ status }: { status: PollStatus }) {
|
||||
const { data, isFetching } = trpc.polls.list.useQuery(
|
||||
{
|
||||
status,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"animate-pulse": isFetching,
|
||||
})}
|
||||
>
|
||||
<PollsListView data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PollStatusMenu({
|
||||
status,
|
||||
onStatusChange,
|
||||
}: {
|
||||
status?: PollStatus;
|
||||
onStatusChange?: (status: PollStatus) => void;
|
||||
}) {
|
||||
const { data: countByStatus, isFetching } =
|
||||
trpc.polls.getCountByStatus.useQuery();
|
||||
|
||||
if (!countByStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RadioCards value={status} onValueChange={onStatusChange}>
|
||||
<RadioCardsItem className="flex items-center gap-2.5" value="live">
|
||||
<Trans i18nKey="pollStatusOpen" />
|
||||
<PollCount count={countByStatus.live} />
|
||||
</RadioCardsItem>
|
||||
<RadioCardsItem className="flex items-center gap-2.5" value="paused">
|
||||
<Trans i18nKey="pollStatusPaused" />
|
||||
<PollCount count={countByStatus.paused} />
|
||||
</RadioCardsItem>
|
||||
<RadioCardsItem className="flex items-center gap-2.5" value="finalized">
|
||||
<Trans i18nKey="pollStatusFinalized" />
|
||||
<PollCount count={countByStatus.finalized} />
|
||||
</RadioCardsItem>
|
||||
{isFetching && <Spinner />}
|
||||
</RadioCards>
|
||||
);
|
||||
}
|
||||
|
||||
function useQueryParam(name: string) {
|
||||
const searchParams = useSearchParams();
|
||||
return [
|
||||
searchParams?.get(name),
|
||||
function (value: string) {
|
||||
const newParams = new URLSearchParams(searchParams?.toString());
|
||||
newParams.set(name, value);
|
||||
window.history.replaceState(null, "", `?${newParams.toString()}`);
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
const pollStatusSchema = z.enum(["live", "paused", "finalized"]).catch("live");
|
||||
|
||||
const pollStatusQueryKey = "status";
|
||||
|
||||
export function UserPolls() {
|
||||
const [pollStatus, setPollStatus] = useQueryParam(pollStatusQueryKey);
|
||||
const parsedPollStatus = pollStatusSchema.parse(pollStatus);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PollStatusMenu
|
||||
status={parsedPollStatus}
|
||||
onStatusChange={setPollStatus}
|
||||
/>
|
||||
<FilteredPolls status={parsedPollStatus} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyLinkButton({ pollId }: { pollId: string }) {
|
||||
const [, copy] = useCopyToClipboard();
|
||||
const [didCopy, setDidCopy] = React.useState(false);
|
||||
|
||||
if (didCopy) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-x-1.5 text-sm font-medium text-green-600">
|
||||
<CheckIcon className="size-4" />
|
||||
<Trans i18nKey="copied" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
copy(`${window.location.origin}/invite/${pollId}`);
|
||||
setDidCopy(true);
|
||||
setTimeout(() => {
|
||||
setDidCopy(false);
|
||||
}, 1000);
|
||||
}}
|
||||
className="text-foreground inline-flex items-center gap-x-1.5 text-sm hover:underline"
|
||||
>
|
||||
<LinkIcon className="size-4" />
|
||||
|
||||
<Trans i18nKey="copyLink" defaults="Copy Link" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ParticipantCount({ count }: { count: number }) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-x-1 text-sm font-medium">
|
||||
<Icon>
|
||||
<UserIcon />
|
||||
</Icon>
|
||||
<span>{count}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PollsListView({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
id: string;
|
||||
status: PollStatus;
|
||||
title: string;
|
||||
createdAt: Date;
|
||||
userId: string;
|
||||
participants: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
}[];
|
||||
}) {
|
||||
const table = useReactTable({
|
||||
columns: [],
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
if (data?.length === 0) {
|
||||
return (
|
||||
<EmptyState className="h-96">
|
||||
<EmptyStateIcon>
|
||||
<CalendarPlusIcon />
|
||||
</EmptyStateIcon>
|
||||
<EmptyStateTitle>
|
||||
<Trans i18nKey="noPolls" />
|
||||
</EmptyStateTitle>
|
||||
<EmptyStateDescription>
|
||||
<Trans i18nKey="noPollsDescription" />
|
||||
</EmptyStateDescription>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<div
|
||||
className={cn("overflow-hidden rounded-lg border bg-white p-1")}
|
||||
key={row.id}
|
||||
>
|
||||
<div className="relative space-y-4 p-3 focus-within:bg-gray-100">
|
||||
<div className="flex items-start justify-between">
|
||||
<GroupPollIcon size="sm" />
|
||||
<PollStatusBadge status={row.original.status} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="truncate text-base font-medium">
|
||||
<Link
|
||||
href={`/poll/${row.original.id}`}
|
||||
className="absolute inset-0 z-10"
|
||||
/>
|
||||
{row.original.title}
|
||||
</h2>
|
||||
<ParticipantCount count={row.original.participants.length} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-between p-3">
|
||||
<CopyLinkButton pollId={row.original.id} />
|
||||
<p className="text-muted-foreground whitespace-nowrap text-sm">
|
||||
<Trans
|
||||
i18nKey="createdTime"
|
||||
values={{
|
||||
relativeTime: dayjs(row.original.createdAt).fromNow(),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -22,8 +22,8 @@ export default async function ProfileLayout({
|
|||
<PageHeader>
|
||||
<PageTitle>{t("settings")}</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent className="space-y-3 lg:space-y-4">
|
||||
<div>
|
||||
<PageContent className="space-y-3 sm:space-y-4">
|
||||
<div className="scrollbar-none -mx-3 overflow-auto bg-gray-100 px-3 sm:mx-0 sm:px-0">
|
||||
<SettingsMenu />
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
|
|
|
@ -4,35 +4,32 @@ import { Icon } from "@rallly/ui/icon";
|
|||
import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import {
|
||||
ResponsiveMenu,
|
||||
ResponsiveMenuItem,
|
||||
} from "@/app/components/responsive-menu";
|
||||
import { TabMenu, TabMenuItem } from "@/app/components/tab-menu";
|
||||
import { IfCloudHosted } from "@/contexts/environment";
|
||||
|
||||
export function SettingsMenu() {
|
||||
return (
|
||||
<ResponsiveMenu>
|
||||
<ResponsiveMenuItem href="/settings/profile">
|
||||
<TabMenu>
|
||||
<TabMenuItem href="/settings/profile">
|
||||
<Icon>
|
||||
<UserIcon />
|
||||
</Icon>
|
||||
<Trans i18nKey="profile" />
|
||||
</ResponsiveMenuItem>
|
||||
<ResponsiveMenuItem href="/settings/preferences">
|
||||
</TabMenuItem>
|
||||
<TabMenuItem href="/settings/preferences">
|
||||
<Icon>
|
||||
<Settings2Icon />
|
||||
</Icon>
|
||||
<Trans i18nKey="preferences" />
|
||||
</ResponsiveMenuItem>
|
||||
</TabMenuItem>
|
||||
<IfCloudHosted>
|
||||
<ResponsiveMenuItem href="/settings/billing">
|
||||
<TabMenuItem href="/settings/billing">
|
||||
<Icon>
|
||||
<CreditCardIcon />
|
||||
</Icon>
|
||||
<Trans i18nKey="billing" />
|
||||
</ResponsiveMenuItem>
|
||||
</TabMenuItem>
|
||||
</IfCloudHosted>
|
||||
</ResponsiveMenu>
|
||||
</TabMenu>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,16 +6,14 @@ import { Icon } from "@rallly/ui/icon";
|
|||
import {
|
||||
ArrowUpRightIcon,
|
||||
BarChart2Icon,
|
||||
BlocksIcon,
|
||||
BookMarkedIcon,
|
||||
CalendarIcon,
|
||||
ChevronRightIcon,
|
||||
HomeIcon,
|
||||
LifeBuoyIcon,
|
||||
LogInIcon,
|
||||
PlusIcon,
|
||||
Settings2Icon,
|
||||
SparklesIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
@ -46,18 +44,12 @@ function NavItem({
|
|||
target={target}
|
||||
className={cn(
|
||||
current
|
||||
? "bg-gray-200 text-gray-800"
|
||||
: "text-gray-700 hover:bg-gray-200 active:bg-gray-300",
|
||||
"group flex items-center gap-x-2.5 rounded-md px-3 py-2 text-sm font-semibold leading-6",
|
||||
? "text-foreground bg-gray-200"
|
||||
: "text-muted-foreground border-transparent hover:bg-gray-200 focus:bg-gray-300",
|
||||
"group flex items-center gap-x-3 rounded-md px-3 py-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
current ? "text-gray-500" : "text-gray-400 group-hover:text-gray-500",
|
||||
"size-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Icon className={cn("size-5 shrink-0")} aria-hidden="true" />
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
@ -70,56 +62,49 @@ export function Sidebar() {
|
|||
<nav className="flex flex-1 flex-col ">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="space-y-1 lg:-mx-2">
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
<li>
|
||||
<NavItem current={pathname === "/"} href="/" icon={HomeIcon}>
|
||||
<Trans i18nKey="home" defaults="Home" />
|
||||
</NavItem>
|
||||
</li>
|
||||
<li>
|
||||
<NavItem
|
||||
current={pathname?.startsWith("/poll")}
|
||||
current={pathname?.startsWith("/polls")}
|
||||
href="/polls"
|
||||
icon={BarChart2Icon}
|
||||
>
|
||||
<Trans i18nKey="polls" defaults="Polls" />
|
||||
</NavItem>
|
||||
</li>
|
||||
<li>
|
||||
<NavItem
|
||||
current={pathname?.startsWith("/events")}
|
||||
href="/events"
|
||||
icon={CalendarIcon}
|
||||
>
|
||||
<Trans i18nKey="events" defaults="Events" />
|
||||
</NavItem>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li className="space-y-1 lg:-mx-2">
|
||||
<Button className="w-full rounded-full" variant="primary" asChild>
|
||||
<li className="-mx-2 space-y-1">
|
||||
<Button variant="primary" className="w-full rounded-full" asChild>
|
||||
<Link href="/new">
|
||||
<Icon>
|
||||
<PlusIcon />
|
||||
</Icon>
|
||||
<Trans i18nKey="newPoll" defaults="New Poll" />
|
||||
<Trans i18nKey="create" defaults="create" />
|
||||
</Link>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
<Trans i18nKey="comingSoon" defaults="Coming Soon" />
|
||||
</div>
|
||||
<ul role="list" className="mt-2 space-y-1 lg:-mx-2">
|
||||
<li className="pointer-events-none grid gap-1 opacity-50">
|
||||
<NavItem href="/events" icon={CalendarIcon}>
|
||||
<Trans i18nKey="events" defaults="Events" />
|
||||
</NavItem>
|
||||
<NavItem href="/b" icon={BookMarkedIcon}>
|
||||
<Trans i18nKey="registrations" defaults="Registrations" />
|
||||
</NavItem>
|
||||
<NavItem href="/contacts" icon={UsersIcon}>
|
||||
<Trans i18nKey="contacts" defaults="Contacts" />
|
||||
</NavItem>
|
||||
<NavItem href="/integrations" icon={BlocksIcon}>
|
||||
<Trans i18nKey="integrations" defaults="Integrations" />
|
||||
</NavItem>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li className="mt-auto">
|
||||
<ul role="list" className="space-y-1 lg:-mx-2">
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
<IfFreeUser>
|
||||
<li>
|
||||
<Link
|
||||
href="/settings/billing"
|
||||
className="mb-4 grid rounded-md border border-gray-200 bg-gray-50 px-4 py-3 hover:border-gray-300 hover:bg-gray-200 active:bg-gray-300"
|
||||
className="mb-4 grid rounded-md border bg-gray-50 px-4 py-3 focus:border-gray-300 focus:bg-gray-200"
|
||||
>
|
||||
<span className="mb-2 flex items-center gap-x-2">
|
||||
<SparklesIcon className="size-5 text-gray-400" />
|
||||
|
@ -157,13 +142,17 @@ export function Sidebar() {
|
|||
</NavItem>
|
||||
</li>
|
||||
<li>
|
||||
<NavItem href="/settings/preferences" icon={Settings2Icon}>
|
||||
<NavItem
|
||||
href="/settings/preferences"
|
||||
current={pathname === "/settings/preferences"}
|
||||
icon={Settings2Icon}
|
||||
>
|
||||
<Trans i18nKey="preferences" />
|
||||
</NavItem>
|
||||
</li>
|
||||
</ul>
|
||||
<hr className="my-2" />
|
||||
<ul role="list" className="space-y-1 lg:-mx-2">
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
<li>
|
||||
<Button
|
||||
asChild
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Viewport } from "next";
|
|||
import { Inter } from "next/font/google";
|
||||
import React from "react";
|
||||
|
||||
import { SquircleClipPath } from "@/app/components/squircle";
|
||||
import { Providers } from "@/app/providers";
|
||||
|
||||
const inter = Inter({
|
||||
|
@ -36,6 +37,7 @@ export default function Root({
|
|||
<html lang={locale} className={inter.className}>
|
||||
<body>
|
||||
<Toaster />
|
||||
<SquircleClipPath />
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
|
23
apps/web/src/app/[locale]/new/close-button.tsx
Normal file
23
apps/web/src/app/[locale]/new/close-button.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function CloseButton() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.back();
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
<Icon>
|
||||
<XIcon />
|
||||
</Icon>
|
||||
</Button>
|
||||
);
|
||||
}
|
44
apps/web/src/app/[locale]/new/page.tsx
Normal file
44
apps/web/src/app/[locale]/new/page.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Trans } from "react-i18next/TransWithoutContext";
|
||||
|
||||
import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card";
|
||||
import { BackButton } from "@/app/[locale]/(admin)/menu/menu-button";
|
||||
import { Params } from "@/app/[locale]/types";
|
||||
import { getTranslation } from "@/app/i18n";
|
||||
import { CreatePoll } from "@/components/create-poll";
|
||||
import { UserDropdown } from "@/components/user-dropdown";
|
||||
|
||||
export default async function Page({ params }: { params: Params }) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return (
|
||||
<div>
|
||||
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-gray-100/90 p-3 backdrop-blur-md sm:grid-cols-3">
|
||||
<div className="flex items-center justify-center gap-x-4">
|
||||
<BackButton />
|
||||
<GroupPollIcon size="xs" />
|
||||
<div className="flex items-baseline gap-x-8">
|
||||
<h1 className="text-sm font-semibold">
|
||||
<Trans t={t} i18nKey="groupPoll" defaults="Group Poll" />
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto max-w-4xl p-3 sm:px-6 sm:py-5">
|
||||
<CreatePoll />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("newPoll"),
|
||||
};
|
||||
}
|
|
@ -1,21 +1,12 @@
|
|||
"use client";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
|
||||
export function PageContainer({
|
||||
children,
|
||||
className,
|
||||
}: React.PropsWithChildren<{ className?: string }>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full max-w-4xl grow px-3 py-4 lg:px-4 lg:py-6",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={cn(className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function PageIcon({
|
||||
|
@ -26,8 +17,8 @@ export function PageIcon({
|
|||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<Icon size="lg">{children}</Icon>
|
||||
<div className={cn("hidden", className)}>
|
||||
<Slot className="size-4">{children}</Slot>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -40,9 +31,14 @@ export function PageTitle({
|
|||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<h2 className={cn("truncate text-base font-semibold", className)}>
|
||||
<h1
|
||||
className={cn(
|
||||
"inline-flex items-center truncate text-xl font-bold tracking-tight text-gray-700",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -54,7 +50,15 @@ export function PageHeader({
|
|||
className?: string;
|
||||
variant?: "default" | "ghost";
|
||||
}) {
|
||||
return <div className={cn("mb-4 lg:mb-6", className)}>{children}</div>;
|
||||
return <div className={cn("mb-4 md:mt-2", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function PageSection({ children }: { children?: React.ReactNode }) {
|
||||
return <div className="space-y-4 md:space-y-6">{children}</div>;
|
||||
}
|
||||
|
||||
export function PageSectionTitle({ children }: { children?: React.ReactNode }) {
|
||||
return <h2 className="text-muted-foreground text-sm">{children}</h2>;
|
||||
}
|
||||
|
||||
export function PageContent({
|
||||
|
@ -64,5 +68,5 @@ export function PageContent({
|
|||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("lg:grow", className)}>{children}</div>;
|
||||
return <div className={cn("md:grow", className)}>{children}</div>;
|
||||
}
|
||||
|
|
46
apps/web/src/app/components/squircle.tsx
Normal file
46
apps/web/src/app/components/squircle.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cn } from "@rallly/ui";
|
||||
|
||||
export function Squircle({
|
||||
children,
|
||||
asChild,
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
asChild?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
return (
|
||||
<>
|
||||
<Comp
|
||||
className={cn("relative", className)}
|
||||
style={{
|
||||
clipPath: `url(#squircleClip)`,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SquircleClipPath() {
|
||||
return (
|
||||
<svg
|
||||
width="0"
|
||||
height="0"
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="squircleClip" clipPathUnits="objectBoundingBox">
|
||||
<path d="M 0,0.5 C 0,0 0,0 0.5,0 S 1,0 1,0.5 1,1 0.5,1 0,1 0,0.5" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
|||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
export function ResponsiveMenuItem({
|
||||
export function TabMenuItem({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
|
@ -29,10 +29,6 @@ export function ResponsiveMenuItem({
|
|||
);
|
||||
}
|
||||
|
||||
export function ResponsiveMenu({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ul className="scrollbar-none -mx-3 flex gap-2.5 overflow-x-auto px-3">
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
export function TabMenu({ children }: { children: React.ReactNode }) {
|
||||
return <ul className="flex gap-2.5">{children}</ul>;
|
||||
}
|
|
@ -136,7 +136,7 @@ export const CreatePoll: React.FunctionComponent = () => {
|
|||
className="w-full"
|
||||
variant="primary"
|
||||
>
|
||||
<Trans i18nKey="createPoll" />
|
||||
<Trans i18nKey="createPoll" defaults="Create poll" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
||||
import { Card, CardContent, CardDescription, CardTitle } from "@rallly/ui/card";
|
||||
import { Card, CardContent, CardDescription } from "@rallly/ui/card";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import dayjs from "dayjs";
|
||||
import { DotIcon, MapPinIcon, PauseIcon } from "lucide-react";
|
||||
|
@ -22,9 +22,9 @@ export function EventCard() {
|
|||
<CardContent>
|
||||
<div className="flex flex-col items-start gap-4 lg:flex-row lg:justify-between">
|
||||
<div>
|
||||
<CardTitle data-testid="poll-title" className="text-lg">
|
||||
<h1 data-testid="poll-title" className="text-lg font-semibold">
|
||||
{poll.title}
|
||||
</CardTitle>
|
||||
</h1>
|
||||
<CardDescription>
|
||||
<span className="flex items-center gap-0.5 whitespace-nowrap text-sm text-gray-500">
|
||||
<span>
|
||||
|
|
|
@ -4,15 +4,16 @@ import { Icon } from "@rallly/ui/icon";
|
|||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowUpRight,
|
||||
ListIcon,
|
||||
LogInIcon,
|
||||
LogOutIcon,
|
||||
ShieldCloseIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card";
|
||||
import Loader from "@/app/[locale]/poll/[urlId]/skeleton";
|
||||
import { LogoutButton } from "@/app/components/logout-button";
|
||||
import { InviteDialog } from "@/components/invite-dialog";
|
||||
|
@ -47,16 +48,15 @@ const Layout = ({ children }: React.PropsWithChildren) => {
|
|||
const poll = usePoll();
|
||||
const pollLink = `/poll/${poll.id}`;
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100">
|
||||
<div className="sticky top-0 z-30 flex flex-col justify-between gap-x-4 gap-y-2.5 border-b bg-gray-100 p-3 sm:flex-row lg:items-center lg:px-5">
|
||||
<div className="flex min-w-0 items-center gap-x-4">
|
||||
<div className="flex min-w-0 items-center gap-x-2.5">
|
||||
{pathname === pollLink ? (
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href="/polls">
|
||||
<Icon>
|
||||
<ListIcon />
|
||||
<XIcon />
|
||||
</Icon>
|
||||
</Link>
|
||||
</Button>
|
||||
|
@ -69,7 +69,8 @@ const Layout = ({ children }: React.PropsWithChildren) => {
|
|||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<h1 className="truncate text-sm font-medium">{poll.title}</h1>
|
||||
<GroupPollIcon size="xs" />
|
||||
<h1 className="truncate text-sm font-semibold">{poll.title}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<AdminControls />
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { PollStatus } from "@rallly/database";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { Badge } from "@rallly/ui/badge";
|
||||
import { CalendarCheckIcon, PauseIcon, RadioIcon } from "lucide-react";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
|
@ -17,11 +15,11 @@ export const PollStatusLabel = ({
|
|||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-x-1.5 text-sm font-medium text-gray-800",
|
||||
"inline-flex items-center gap-x-1.5 text-sm font-medium text-pink-600",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<RadioIcon className="inline-block size-4 opacity-75" />
|
||||
<span className="size-1.5 rounded-full bg-pink-600" />
|
||||
<Trans i18nKey="pollStatusOpen" defaults="Live" />
|
||||
</span>
|
||||
);
|
||||
|
@ -29,11 +27,12 @@ export const PollStatusLabel = ({
|
|||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground inline-flex items-center gap-x-1.5 text-sm font-medium",
|
||||
"inline-flex items-center gap-x-1.5 rounded-full text-sm font-medium text-gray-500",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<PauseIcon className="inline-block size-4 opacity-75" />
|
||||
<span className="size-1.5 rounded-full bg-gray-600" />
|
||||
|
||||
<Trans i18nKey="pollStatusPaused" defaults="Paused" />
|
||||
</span>
|
||||
);
|
||||
|
@ -41,11 +40,12 @@ export const PollStatusLabel = ({
|
|||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-primary-50 inline-flex items-center gap-x-1.5 text-sm font-medium",
|
||||
"inline-flex items-center gap-x-1.5 rounded-full text-sm font-medium text-green-600",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CalendarCheckIcon className="inline-block size-4 opacity-75" />
|
||||
<span className="size-1.5 rounded-full bg-green-600" />
|
||||
|
||||
<Trans i18nKey="pollStatusFinalized" defaults="Finalized" />
|
||||
</span>
|
||||
);
|
||||
|
@ -53,18 +53,5 @@ export const PollStatusLabel = ({
|
|||
};
|
||||
|
||||
export const PollStatusBadge = ({ status }: { status: PollStatus }) => {
|
||||
return (
|
||||
<Badge
|
||||
size="lg"
|
||||
variant={
|
||||
status === "finalized"
|
||||
? "primary"
|
||||
: status === "paused"
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
<PollStatusLabel status={status} />
|
||||
</Badge>
|
||||
);
|
||||
return <PollStatusLabel status={status} />;
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { generateGradient } from "@/utils/color-hash";
|
|||
export function RandomGradientBar({ seed }: { seed?: string }) {
|
||||
return (
|
||||
<div
|
||||
className="-mx-px -mt-px h-2 rounded-t-md"
|
||||
className="-mx-px -mt-px h-2 sm:rounded-t-md"
|
||||
style={{ background: generateGradient(seed ?? "") }}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -41,7 +41,7 @@ export const UserAvatar = ({
|
|||
"size-5 text-[10px]": size === "xs",
|
||||
"size-6 text-sm": size === "sm",
|
||||
"size-8 text-base": size === "md",
|
||||
"size-14 text-2xl": size === "lg",
|
||||
"size-10 text-lg": size === "lg",
|
||||
},
|
||||
!name
|
||||
? "bg-gray-200"
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
* See: https://github.com/lukevella/rallly/issues/949
|
||||
*/
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import { PrismaClient } from "@rallly/database";
|
||||
import { ExtendedPrismaClient, PrismaClient } from "@rallly/database";
|
||||
import { Adapter, AdapterAccount } from "next-auth/adapters";
|
||||
|
||||
export function CustomPrismaAdapter(client: PrismaClient): Adapter {
|
||||
export function CustomPrismaAdapter(client: ExtendedPrismaClient): Adapter {
|
||||
return {
|
||||
...PrismaAdapter(client),
|
||||
...PrismaAdapter(client as PrismaClient),
|
||||
linkAccount: (data) => {
|
||||
return client.account.create({
|
||||
data: {
|
||||
|
|
|
@ -74,7 +74,7 @@ test.describe.serial(() => {
|
|||
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await page.waitForURL("/polls");
|
||||
await page.waitForURL("/");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -121,7 +121,7 @@ test.describe.serial(() => {
|
|||
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await page.waitForURL("/polls");
|
||||
await page.waitForURL("/");
|
||||
|
||||
await expect(page.getByText("Test User")).toBeVisible();
|
||||
});
|
||||
|
@ -141,7 +141,7 @@ test.describe.serial(() => {
|
|||
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await page.waitForURL("/polls");
|
||||
await page.waitForURL("/");
|
||||
|
||||
await expect(page.getByText("Test User")).toBeVisible();
|
||||
});
|
||||
|
@ -161,7 +161,7 @@ test.describe.serial(() => {
|
|||
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await page.waitForURL("/polls");
|
||||
await page.waitForURL("/");
|
||||
|
||||
await expect(page.getByText("Test User")).toBeVisible();
|
||||
});
|
||||
|
|
|
@ -22,12 +22,6 @@ test.describe.serial(() => {
|
|||
await newPollPage.createPollAndCloseDialog();
|
||||
|
||||
await expect(page.getByTestId("poll-title")).toHaveText("Monthly Meetup");
|
||||
|
||||
// const { email } = await mailServer.captureOne("john.doe@example.com", {
|
||||
// wait: 5000,
|
||||
// });
|
||||
|
||||
// expect(email.headers.subject).toBe("Let's find a date for Monthly Meetup");
|
||||
});
|
||||
|
||||
// delete the poll we just created
|
||||
|
|
|
@ -15,5 +15,10 @@
|
|||
".next/types/**/*.ts",
|
||||
"vitest.config.mts",
|
||||
],
|
||||
"exclude": ["node_modules", ".next/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next/**/*",
|
||||
"playwright-report",
|
||||
"test-results",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"dev": "dotenv -c development -- turbo dev --filter=@rallly/web",
|
||||
"dev:emails": "turbo dev --filter=@rallly/emails",
|
||||
"dev:landing": "dotenv -c development turbo dev --filter=@rallly/landing",
|
||||
"start": "dotenv -c -- turbo run start --filter=@rallly/web",
|
||||
"start": "turbo run start --filter=@rallly/web",
|
||||
"build": "dotenv -c -- turbo run build --filter=@rallly/web",
|
||||
"build:test": "turbo build:test",
|
||||
"docs:dev": "turbo dev --filter=@rallly/docs...",
|
||||
|
@ -32,10 +32,10 @@
|
|||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.3.1",
|
||||
"@prisma/client": "^5.15.0",
|
||||
"@sentry/nextjs": "^7.77.0",
|
||||
"framer-motion": "^10.16.4",
|
||||
"next": "^14.0.4",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"typescript": "^5.2.2",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"@rallly/database": "*",
|
||||
"@rallly/features": "*",
|
||||
"@rallly/emails": "*",
|
||||
"@rallly/utils": "*",
|
||||
"@trpc/server": "^10.13.0",
|
||||
|
|
17
packages/backend/trpc/routers/dashboard.ts
Normal file
17
packages/backend/trpc/routers/dashboard.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
|
||||
import { possiblyPublicProcedure, router } from "../trpc";
|
||||
|
||||
export const dashboard = router({
|
||||
info: possiblyPublicProcedure.query(async ({ ctx }) => {
|
||||
const activePollCount = await prisma.poll.count({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
status: "live",
|
||||
deleted: false, // TODO (Luke Vella) [2024-06-16]: We should add deleted/cancelled to the status enum
|
||||
},
|
||||
});
|
||||
|
||||
return { activePollCount };
|
||||
}),
|
||||
});
|
|
@ -1,13 +1,26 @@
|
|||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { mergeRouters, router } from "../trpc";
|
||||
import { auth } from "./auth";
|
||||
import { dashboard } from "./dashboard";
|
||||
import { polls } from "./polls";
|
||||
import { scheduledEvents } from "./scheduled-events";
|
||||
import { user } from "./user";
|
||||
|
||||
dayjs.extend(toArray); // used for creating ics
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(utc);
|
||||
|
||||
export const appRouter = mergeRouters(
|
||||
router({
|
||||
scheduledEvents,
|
||||
auth,
|
||||
polls,
|
||||
user,
|
||||
dashboard,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { PollStatus, prisma } from "@rallly/database";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import * as ics from "ics";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -19,10 +16,6 @@ import {
|
|||
import { comments } from "./polls/comments";
|
||||
import { participants } from "./polls/participants";
|
||||
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(utc);
|
||||
|
||||
const getPollIdFromAdminUrlId = async (urlId: string) => {
|
||||
const res = await prisma.poll.findUnique({
|
||||
select: {
|
||||
|
@ -43,6 +36,64 @@ const getPollIdFromAdminUrlId = async (urlId: string) => {
|
|||
export const polls = router({
|
||||
participants,
|
||||
comments,
|
||||
getCountByStatus: possiblyPublicProcedure.query(async ({ ctx }) => {
|
||||
const res = await prisma.poll.groupBy({
|
||||
by: ["status"],
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
deleted: false,
|
||||
},
|
||||
_count: {
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
return res.reduce(
|
||||
(acc, { status, _count }) => {
|
||||
acc[status] = _count.status;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<PollStatus, number>,
|
||||
);
|
||||
}),
|
||||
list: possiblyPublicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
status: z.enum(["all", "live", "paused", "finalized"]),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await prisma.poll.findMany({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
status: input.status === "all" ? undefined : input.status,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: "desc",
|
||||
},
|
||||
{
|
||||
title: "asc",
|
||||
},
|
||||
],
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
location: true,
|
||||
timeZone: true,
|
||||
createdAt: true,
|
||||
status: true,
|
||||
userId: true,
|
||||
participants: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
// START LEGACY ROUTES
|
||||
create: possiblyPublicProcedure
|
||||
.input(
|
||||
|
|
33
packages/backend/trpc/routers/scheduled-events.ts
Normal file
33
packages/backend/trpc/routers/scheduled-events.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { listScheduledEvents } from "@rallly/features/scheduled-events/api";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { z } from "zod";
|
||||
|
||||
import { possiblyPublicProcedure, router } from "../trpc";
|
||||
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(utc);
|
||||
|
||||
export const scheduledEvents = router({
|
||||
list: possiblyPublicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
period: z.enum(["upcoming", "past"]).default("upcoming"),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const events = await listScheduledEvents({
|
||||
userId: ctx.user.id,
|
||||
period: input.period,
|
||||
});
|
||||
|
||||
return events.map(({ poll, ...event }) => ({
|
||||
...event,
|
||||
timeZone: poll?.timeZone || null,
|
||||
participants: poll?.participants ?? [],
|
||||
}));
|
||||
}),
|
||||
});
|
|
@ -1,13 +1,31 @@
|
|||
import { PrismaClient } from "@rallly/database";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export * from "@prisma/client";
|
||||
export type * from "@prisma/client";
|
||||
|
||||
declare global {
|
||||
// allow global `var` declarations
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient().$extends({
|
||||
query: {
|
||||
poll: {
|
||||
findMany: ({ args, query }) => {
|
||||
if (!args.where?.deleted) {
|
||||
args.where = { ...args.where, deleted: false };
|
||||
}
|
||||
|
||||
export const prisma = global.prisma || new PrismaClient();
|
||||
return query(args);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
|
||||
export type ExtendedPrismaClient = ReturnType<typeof prismaClientSingleton>;
|
||||
|
||||
declare const globalThis: {
|
||||
prismaGlobal: ExtendedPrismaClient;
|
||||
} & typeof global;
|
||||
|
||||
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
|
||||
|
||||
export { prisma };
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
|
||||
|
|
|
@ -9,13 +9,12 @@
|
|||
"db:migrate": "prisma migrate dev",
|
||||
"db:seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"exports": "./index.ts",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@rallly/tsconfig": "*",
|
||||
"@types/node": "^18.15.10",
|
||||
"prisma": "^5.3.1",
|
||||
"prisma": "^5.15.0",
|
||||
"tsx": "^4.6.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ model Event {
|
|||
duration Int @default(0) @map("duration_minutes")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
Poll Poll?
|
||||
poll Poll?
|
||||
|
||||
@@index([userId], type: Hash)
|
||||
@@map("events")
|
||||
|
|
|
@ -11,7 +11,7 @@ const randInt = (max = 1, floor = 0) => {
|
|||
async function createPollsForUser(userId: string) {
|
||||
// Create some polls
|
||||
const polls = await Promise.all(
|
||||
Array.from({ length: 20 }).map(async (_, i) => {
|
||||
Array.from({ length: 5 }).map(async (_, i) => {
|
||||
// create some polls with no duration (all day) and some with a random duration.
|
||||
const duration = i % 2 === 0 ? 60 * randInt(8, 1) : 0;
|
||||
let cursor = dayjs().add(randInt(30), "day").second(0).minute(0);
|
||||
|
@ -25,7 +25,7 @@ async function createPollsForUser(userId: string) {
|
|||
},
|
||||
data: {
|
||||
id: faker.random.alpha(10),
|
||||
title: `${faker.animal.cat()} meetup - ${faker.date.month()}`,
|
||||
title: `${faker.animal.cat()} Meetup ${faker.date.month()}`,
|
||||
description: faker.lorem.paragraph(),
|
||||
location: faker.address.streetAddress(),
|
||||
deadline: faker.date.future(),
|
||||
|
@ -34,7 +34,7 @@ async function createPollsForUser(userId: string) {
|
|||
id: userId,
|
||||
},
|
||||
},
|
||||
timeZone: duration !== 0 ? "America/New_York" : undefined,
|
||||
timeZone: duration !== 0 ? "Europe/London" : undefined,
|
||||
options: {
|
||||
create: Array.from({ length: numberOfOptions }).map(() => {
|
||||
const startTime = cursor.toDate();
|
||||
|
|
3
packages/features/index.ts
Normal file
3
packages/features/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function () {
|
||||
return null;
|
||||
}
|
6
packages/features/package.json
Normal file
6
packages/features/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@rallly/features",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"version": "0.0.0"
|
||||
}
|
49
packages/features/scheduled-events/api.ts
Normal file
49
packages/features/scheduled-events/api.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
|
||||
export type EventPeriod = "upcoming" | "past";
|
||||
|
||||
/**
|
||||
* List upcoming events for a user grouped by day
|
||||
* @param userId
|
||||
*/
|
||||
export async function listScheduledEvents({
|
||||
userId,
|
||||
period,
|
||||
}: {
|
||||
userId: string;
|
||||
period: EventPeriod;
|
||||
}) {
|
||||
const events = await prisma.event.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
start: true,
|
||||
duration: true,
|
||||
poll: {
|
||||
select: {
|
||||
timeZone: true,
|
||||
participants: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
userId,
|
||||
start: period === "upcoming" ? { gte: new Date() } : { lt: new Date() },
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
start: "desc",
|
||||
},
|
||||
{
|
||||
title: "asc",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
8
packages/features/tsconfig.json
Normal file
8
packages/features/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@rallly/tsconfig/next.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
}
|
|
@ -6,6 +6,6 @@
|
|||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"lucide-react": "^0.338.0"
|
||||
"lucide-react": "^0.387.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,14 +27,14 @@ module.exports = {
|
|||
secondary: {
|
||||
background: colors.gray["100"],
|
||||
DEFAULT: colors.gray["100"],
|
||||
foreground: colors.gray["800"],
|
||||
foreground: colors.gray["700"],
|
||||
},
|
||||
gray: colors.gray,
|
||||
border: colors.gray["200"],
|
||||
input: {
|
||||
DEFAULT: colors.gray["200"],
|
||||
background: colors.white,
|
||||
foreground: colors.gray["800"],
|
||||
foreground: colors.gray["700"],
|
||||
},
|
||||
ring: {
|
||||
DEFAULT: colors.gray["300"],
|
||||
|
@ -45,7 +45,7 @@ module.exports = {
|
|||
foreground: colors.rose["50"],
|
||||
},
|
||||
background: colors.white,
|
||||
foreground: colors.gray["800"],
|
||||
foreground: colors.gray["700"],
|
||||
accent: {
|
||||
DEFAULT: colors.gray["100"],
|
||||
},
|
||||
|
@ -56,12 +56,12 @@ module.exports = {
|
|||
},
|
||||
popover: {
|
||||
DEFAULT: colors.white,
|
||||
foreground: colors.gray["800"],
|
||||
foreground: colors.gray["700"],
|
||||
},
|
||||
card: {
|
||||
DEFAULT: colors.white,
|
||||
background: colors.white,
|
||||
foreground: colors.gray["800"],
|
||||
foreground: colors.gray["700"],
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
|
|
|
@ -16,7 +16,7 @@ const badgeVariants = cva(
|
|||
green: "border-transparent bg-green-500 text-green-50",
|
||||
},
|
||||
size: {
|
||||
md: "h-5 min-w-5 text-xs px-1.5",
|
||||
md: "h-6 min-w-5 text-xs px-2",
|
||||
lg: "h-7 text-sm min-w-7 px-2.5",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,27 +7,27 @@ import { cn } from "./lib/utils";
|
|||
|
||||
const buttonVariants = cva(
|
||||
cn(
|
||||
"inline-flex border font-medium disabled:pointer-events-none select-none disabled:opacity-50 items-center justify-center whitespace-nowrap rounded-md border",
|
||||
"focus-visible:ring-offset-input-background focus-visible:ring-offset-1 focus-visible:ring-2 focus-visible:ring-gray-200",
|
||||
"active:shadow-none",
|
||||
"inline-flex border font-medium disabled:pointer-events-none select-none disabled:opacity-50 items-center justify-center whitespace-nowrap border",
|
||||
"focus-visible:ring-offset-input-background",
|
||||
"focus:shadow-none",
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
"border-primary-700 shadow-sm bg-primary disabled:bg-gray-400 disabled:border-transparent text-primary-foreground shadow-sm hover:bg-primary-500 active:bg-primary-700",
|
||||
"border-primary-700 bg-primary disabled:bg-gray-400 disabled:border-transparent text-primary-foreground shadow-sm focus:bg-primary-500",
|
||||
destructive:
|
||||
"bg-destructive shadow-sm text-destructive-foreground focus-visible:ring-offset-1 active:bg-destructive border-destructive hover:bg-destructive/90",
|
||||
default:
|
||||
"rounded-md px-3.5 py-2.5 data-[state=open]:shadow-none data-[state=open]:bg-gray-100 active:bg-gray-200 hover:bg-gray-100 bg-gray-50",
|
||||
"ring-1 ring-inset ring-white/25 data-[state=open]:bg-gray-100 focus:border-gray-300 focus:bg-gray-200 hover:bg-gray-100 bg-gray-50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"border-transparent bg-transparent hover:bg-gray-200 active:bg-gray-300",
|
||||
"border-transparent bg-transparent text-gray-800 focus:border-gray-300 focus:bg-gray-200",
|
||||
link: "underline-offset-4 border-transparent hover:underline text-primary",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2.5 gap-x-2.5 text-sm",
|
||||
default: "h-9 px-2.5 pr-3 gap-x-2 text-sm rounded-md",
|
||||
sm: "h-7 text-sm px-1.5 gap-x-1.5 rounded-md",
|
||||
lg: "h-11 text-base gap-x-3 px-4 rounded-md",
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-card text-card-foreground overflow-hidden rounded-lg border shadow-sm",
|
||||
"bg-card text-card-foreground overflow-hidden rounded-lg border-x border-y shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
@ -39,7 +39,7 @@ const CardTitle = React.forwardRef<
|
|||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center gap-x-2.5 text-sm font-semibold sm:text-base",
|
||||
"flex items-center gap-x-2.5 text-base font-semibold",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
@ -53,7 +53,7 @@ const CardDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-muted-foreground mt-1 text-sm", className)}
|
||||
className={cn("text-muted-foreground mt-0.5 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -12,9 +12,8 @@ const Checkbox = React.forwardRef<
|
|||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=checked]:bg-primary data-[state=checked]:border-primary-600 data-[state=checked]:focus:ring-primary-200 data-[state=checked]:focus:ring-2",
|
||||
"focus-visible:ring-gray-100",
|
||||
"peer inline-flex h-5 w-5 shrink-0 items-center justify-center rounded border border-gray-200 bg-gray-50 ring-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"data-[state=checked]:bg-primary data-[state=checked]:border-primary-600",
|
||||
"peer inline-flex size-5 shrink-0 items-center justify-center rounded border border-gray-200 bg-gray-50 ring-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -33,7 +33,7 @@ export function Icon({ children, size, variant }: IconProps) {
|
|||
<Slot
|
||||
className={cn(
|
||||
iconVariants({ size, variant }),
|
||||
"group-[.bg-primary]:text-primary-100 group-[.bg-destructive]:text-destructive-foreground group shrink-0",
|
||||
"group-[.bg-primary]:text-primary-50 group-[.bg-destructive]:text-destructive-foreground group shrink-0",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -14,7 +14,7 @@ export type InputProps = Omit<
|
|||
const inputVariants = cva(
|
||||
cn(
|
||||
"w-full focus-visible:border-primary-400 focus-visible:ring-offset-1 focus-visible:outline-none focus-visible:ring-primary-200 focus-visible:ring-1",
|
||||
"border-input placeholder:text-muted-foreground h-9 rounded border bg-gray-50 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-input placeholder:text-muted-foreground h-9 rounded-md border bg-white file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50",
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
|
|
34
packages/ui/src/radio-pills.tsx
Normal file
34
packages/ui/src/radio-pills.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
"use client";
|
||||
|
||||
import * as Primitive from "@radix-ui/react-radio-group";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const RadioPills = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitive.Root
|
||||
ref={ref}
|
||||
className={cn("display flex items-center gap-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
const RadioPillsItem = React.forwardRef<
|
||||
React.ElementRef<typeof Primitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-muted-foreground data-[state=checked]:text-primary data-[state=checked]:border-primary data-[state=unchecked]:hover:text-foreground h-8 rounded-full border px-3 text-sm font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
RadioPillsItem.displayName = Primitive.Item.displayName;
|
||||
|
||||
export { RadioPills as RadioCards, RadioPillsItem as RadioCardsItem };
|
|
@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
|
|||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex items-center justify-center rounded-md border",
|
||||
"bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-md border",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
|
|||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:ring-1 data-[state=active]:ring-gray-200",
|
||||
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex h-full items-center justify-center whitespace-nowrap rounded px-3 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:ring-1 data-[state=active]:ring-gray-200",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground flex min-h-[80px] rounded border bg-gray-50 px-2 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-input placeholder:text-muted-foreground flex min-h-[80px] rounded-md border bg-white px-2 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"focus-visible:ring-offset-input-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1",
|
||||
"focus-visible:border-primary-400 focus-visible:ring-primary-100",
|
||||
className,
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
"extends": "@rallly/tsconfig/next.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@rallly/ui/*": ["./src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
|
|
28
turbo.json
28
turbo.json
|
@ -3,7 +3,6 @@
|
|||
"globalDependencies": [".env"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
},
|
||||
"build:test": {
|
||||
|
@ -28,11 +27,11 @@
|
|||
"cache": true
|
||||
},
|
||||
"test:integration": {
|
||||
"dependsOn": ["@rallly/database#db:generate"],
|
||||
"inputs": ["playwright.config.ts", ".env.test", "src/**/*", "tests/**/*"],
|
||||
"cache": true
|
||||
},
|
||||
"db:generate": {
|
||||
"cache": true
|
||||
"cache": false
|
||||
},
|
||||
"lint": {
|
||||
"outputs": []
|
||||
|
@ -41,7 +40,6 @@
|
|||
"outputs": []
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": ["@rallly/database#db:generate"],
|
||||
"cache": false
|
||||
},
|
||||
"start": {
|
||||
|
@ -68,32 +66,14 @@
|
|||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_REGION",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"DATABASE_URL",
|
||||
"DISABLE_LANDING_PAGE",
|
||||
"EMAIL_PROVIDER",
|
||||
"MAINTENANCE_MODE",
|
||||
"MICROSOFT_CLIENT_ID",
|
||||
"MICROSOFT_CLIENT_SECRET",
|
||||
"MICROSOFT_TENANT_ID",
|
||||
"NEXT_PUBLIC_ABOUT_PAGE_URL",
|
||||
"NEXT_PUBLIC_APP_BASE_URL",
|
||||
"NEXT_PUBLIC_APP_VERSION",
|
||||
"NEXT_PUBLIC_BASE_URL",
|
||||
"NEXT_PUBLIC_BETA",
|
||||
"NEXT_PUBLIC_CRISP_WEBSITE_ID",
|
||||
"NEXT_PUBLIC_ENABLE_ANALYTICS",
|
||||
"NEXT_PUBLIC_ENABLE_FINALIZATION",
|
||||
"NEXT_PUBLIC_LANDING_PAGE_URL",
|
||||
"NEXT_PUBLIC_MAINTENANCE_MODE",
|
||||
"NEXT_PUBLIC_PADDLE_SANDBOX",
|
||||
"NEXT_PUBLIC_PADDLE_VENDOR_ID",
|
||||
"NEXT_PUBLIC_POSTHOG_API_HOST",
|
||||
"NEXT_PUBLIC_POSTHOG_API_KEY",
|
||||
"NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY",
|
||||
"NEXT_PUBLIC_PRO_PLAN_ID_YEARLY",
|
||||
"NEXT_PUBLIC_SELF_HOSTED",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"NEXT_PUBLIC_SHORT_BASE_URL",
|
||||
"NEXT_PUBLIC_VERCEL_URL",
|
||||
"NEXT_PUBLIC_*",
|
||||
"NODE_ENV",
|
||||
"NOREPLY_EMAIL",
|
||||
"NOREPLY_EMAIL_NAME",
|
||||
|
|
207
yarn.lock
207
yarn.lock
|
@ -2083,16 +2083,16 @@
|
|||
dependencies:
|
||||
webpack-bundle-analyzer "4.3.0"
|
||||
|
||||
"@next/env@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
|
||||
integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
|
||||
|
||||
"@next/env@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.5-canary.46.tgz#b9b597baaba77a2836eaf836712a6e0afed1ca2d"
|
||||
integrity sha512-dvNzrArTfe3VY1VIscpb3E2e7SZ1qwFe82WGzpOVbxilT3JcsnVGYF/uq8Jj1qKWPI5C/aePNXwA97JRNAXpRQ==
|
||||
|
||||
"@next/env@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.4.tgz#5546813dc4f809884a37d257b254a5ce1b0248d7"
|
||||
integrity sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==
|
||||
|
||||
"@next/eslint-plugin-next@14.0.1":
|
||||
version "14.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.1.tgz#6e587b76588a02d77267945b5d1f059a6c8fd9ca"
|
||||
|
@ -2100,96 +2100,96 @@
|
|||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/swc-darwin-arm64@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
|
||||
integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
|
||||
|
||||
"@next/swc-darwin-arm64@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.5-canary.46.tgz#94c67fa212614892f94db120c92a9f4207da13b8"
|
||||
integrity sha512-7Bq9rjWl4sq70Zkn6h6mn8/tgYTH2SQ8lIm8b/j1MAnTiJYyVBLapu//gT/cgtqx6y8SwSc2JNviBue35zeCNw==
|
||||
|
||||
"@next/swc-darwin-x64@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
|
||||
integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
|
||||
"@next/swc-darwin-arm64@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz#da9f04c34a3d5f0b8401ed745768420e4a604036"
|
||||
integrity sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==
|
||||
|
||||
"@next/swc-darwin-x64@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.5-canary.46.tgz#25e2a5acfc5b20d3a25ad6adcfbfc91aaa44d79f"
|
||||
integrity sha512-3oI8rDVBZsfkTdqXwtRjxA85o0RIjZv9uuOLohfaIuFP3oZnCM0dRZREP2umYcFQRxdavW+TDJzYcqzKxYTujA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
|
||||
integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
|
||||
"@next/swc-darwin-x64@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz#46dedb29ec5503bf171a72a3ecb8aac6e738e9d6"
|
||||
integrity sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.5-canary.46.tgz#00fad5be6cada895e513d81427c462c92abdae3f"
|
||||
integrity sha512-gXSS328bUWxBwQfeDFROOzFSzzoyX1075JxOeArLl63sV59cbnRrwHHhD4CWG1bYYzcHxHfVugZgvyCucaHCIw==
|
||||
|
||||
"@next/swc-linux-arm64-musl@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
|
||||
integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
|
||||
"@next/swc-linux-arm64-gnu@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz#c9697ab9eb422bd1d7ffd0eb0779cc2aefa9d4a1"
|
||||
integrity sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==
|
||||
|
||||
"@next/swc-linux-arm64-musl@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.5-canary.46.tgz#a931a1312d3f5e66ea59c4b23e0ae90721f8e252"
|
||||
integrity sha512-7QkBRKlDsjaWGbfIKh6qJK0HiHJISNGoKpwFTcnZvlhAEaydS5Hmu0zh64kbLRlzwXtkpj6/iCwjrWnHes59aA==
|
||||
|
||||
"@next/swc-linux-x64-gnu@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
|
||||
integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
|
||||
"@next/swc-linux-arm64-musl@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz#cbbceb2008571c743b5a310a488d2e166d200a75"
|
||||
integrity sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==
|
||||
|
||||
"@next/swc-linux-x64-gnu@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.5-canary.46.tgz#32bf69fa93975ca3fef141121eaa8a1a67086694"
|
||||
integrity sha512-DS5wTjw3FtcLFVzRxLMJgmDNMoeaXp5qBdKUSBrKTq4zQnqUi99CGz2461DlUSxJCWPUgAVo23MdoQD6Siuk7A==
|
||||
|
||||
"@next/swc-linux-x64-musl@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
|
||||
integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
|
||||
"@next/swc-linux-x64-gnu@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz#d79184223f857bacffb92f643cb2943a43632568"
|
||||
integrity sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==
|
||||
|
||||
"@next/swc-linux-x64-musl@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.5-canary.46.tgz#59f221d83096b0362849fabbcda1fdc1671cf6b1"
|
||||
integrity sha512-d409ur5JGj6HFp8DBu5M2oTh5EddDcrT+vjewQkAq/A7MZoAMAOH74xOFouEnJs0/dQ71XvH9Lw+1gJSnElcyQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
|
||||
integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
|
||||
"@next/swc-linux-x64-musl@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz#6b6c3e5ac02ca5e63394d280ec8ee607491902df"
|
||||
integrity sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.5-canary.46.tgz#465d24227cd1b8840b85ee488327725e478da221"
|
||||
integrity sha512-goyh/RCFtivflIOvbwircMxTSObETufm3pcxtI8rIz9+pg/M2MmK8/z48EZybkEcPKl41xu4s1iqXThy/jDPng==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
|
||||
integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
|
||||
"@next/swc-win32-arm64-msvc@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz#dbad3906e870dba84c5883d9d4c4838472e0697f"
|
||||
integrity sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.5-canary.46.tgz#0a65de42dcb8a8293ee0f8e3082d4d8c326f3d12"
|
||||
integrity sha512-SEnrOZ7ASXdd/GBq2x0IfpSbfamv1rZfcDeZZLF7kzu0pY7jDQwcW8zTKwwC8JH5CLGLfI3wD6wUYrA+PgJSCw==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.0.4":
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
|
||||
integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
|
||||
"@next/swc-win32-ia32-msvc@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz#6074529b91ba49132922ce89a2e16d25d2ec235d"
|
||||
integrity sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.0.5-canary.46":
|
||||
version "14.0.5-canary.46"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.5-canary.46.tgz#e19326097b306c58eb47984acf7f7eca4485b604"
|
||||
integrity sha512-NK1EJLyeUxgX9IHSxO0kN1Nk8VsaDfjHVYL4p9fM24e/9rG8jPcxquIQJ4Wy+ZdqxaVivqQ2eHrJYUpXpfOXmw==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.2.4":
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz#e65a1c6539a671f97bb86d5183d6e3a1733c29c7"
|
||||
integrity sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
|
@ -2270,22 +2270,46 @@
|
|||
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz"
|
||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||
|
||||
"@prisma/client@^5.3.1":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz"
|
||||
integrity sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==
|
||||
"@prisma/client@^5.15.0":
|
||||
version "5.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.15.0.tgz#a9443ace9b8a8d57aff70647168e95f2f55c5dc9"
|
||||
integrity sha512-wPTeTjbd2Q0abOeffN7zCDCbkp9C9cF+e9HPiI64lmpehyq2TepgXE+sY7FXr7Rhbb21prLMnhXX27/E11V09w==
|
||||
|
||||
"@prisma/debug@5.15.0":
|
||||
version "5.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.15.0.tgz#a4c1d8dbca9cf29aab1c82a56a65224ed3e05f13"
|
||||
integrity sha512-QpEAOjieLPc/4sMny/WrWqtpIAmBYsgqwWlWwIctqZO0AbhQ9QcT6x2Ut3ojbDo/pFRCCA1Z1+xm2MUy7fAkZA==
|
||||
|
||||
"@prisma/engines-version@5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022":
|
||||
version "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022.tgz#4469a372b74088db05c0fc8cff65f229b804fa51"
|
||||
integrity sha512-3BEgZ41Qb4oWHz9kZNofToRvNeS4LZYaT9pienR1gWkjhky6t6K1NyeWNBkqSj2llgraUNbgMOCQPY4f7Qp5wA==
|
||||
|
||||
"@prisma/engines@5.15.0":
|
||||
version "5.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.15.0.tgz#bddf1973b5b0d2ebed473ed445b1a7c8dd23300b"
|
||||
integrity sha512-hXL5Sn9hh/ZpRKWiyPA5GbvF3laqBHKt6Vo70hYqqOhh5e0ZXDzHcdmxNvOefEFeqxra2DMz2hNbFoPvqrVe1w==
|
||||
dependencies:
|
||||
"@prisma/engines-version" "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59"
|
||||
"@prisma/debug" "5.15.0"
|
||||
"@prisma/engines-version" "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022"
|
||||
"@prisma/fetch-engine" "5.15.0"
|
||||
"@prisma/get-platform" "5.15.0"
|
||||
|
||||
"@prisma/engines-version@5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59":
|
||||
version "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59"
|
||||
resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz"
|
||||
integrity sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w==
|
||||
"@prisma/fetch-engine@5.15.0":
|
||||
version "5.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.15.0.tgz#f5bafd6aed3f58c41b5d0d6f832d652aa5d4cde7"
|
||||
integrity sha512-z6AY5yyXxc20Klj7wwnfGP0iIUkVKzybqapT02zLYR/nf9ynaeN8bq73WRmi1TkLYn+DJ5Qy+JGu7hBf1pE78A==
|
||||
dependencies:
|
||||
"@prisma/debug" "5.15.0"
|
||||
"@prisma/engines-version" "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022"
|
||||
"@prisma/get-platform" "5.15.0"
|
||||
|
||||
"@prisma/engines@5.3.1":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz"
|
||||
integrity sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==
|
||||
"@prisma/get-platform@5.15.0":
|
||||
version "5.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.15.0.tgz#d39fbe8458432f76afeb6c9199bffae73db4f5cc"
|
||||
integrity sha512-1GULDkW4+/VQb73vihxCBSc4Chc2x88MA+O40tcZFjmBzG4/fF44PaXFxUqKSFltxU9L9GIMLhh0Gfkk/pUbtg==
|
||||
dependencies:
|
||||
"@prisma/debug" "5.15.0"
|
||||
|
||||
"@radix-ui/colors@1.0.1":
|
||||
version "1.0.1"
|
||||
|
@ -4101,6 +4125,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.2.tgz#bf06d0770e47c6f1102270b744e17b934586985e"
|
||||
integrity sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
|
||||
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
|
||||
|
||||
"@swc/helpers@0.5.2":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz"
|
||||
|
@ -4108,6 +4137,14 @@
|
|||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@swc/helpers@0.5.5":
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.5.tgz#12689df71bfc9b21c4f4ca00ae55f2f16c8b77c0"
|
||||
integrity sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@swc/types@^0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a"
|
||||
|
@ -5593,6 +5630,11 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.300014
|
|||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz"
|
||||
integrity sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==
|
||||
|
||||
caniuse-lite@^1.0.30001579:
|
||||
version "1.0.30001633"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz#45a4ade9fb9ec80a06537a6271ac1e0afadcb324"
|
||||
integrity sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"
|
||||
|
@ -8894,16 +8936,16 @@ lru-cache@^6.0.0:
|
|||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
||||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
||||
|
||||
lucide-react@^0.338.0:
|
||||
version "0.338.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.338.0.tgz#b7f3be2fddf9988fe28f0ee72b6f955cc36ae011"
|
||||
integrity sha512-Uq+vcn/gp6l01GpDH8SxY6eAvO6Ur2bSU39NxEEJt35OotnVCH5q26TZEVPtJf23gTAncXd3DJQqcezIm6HA7w==
|
||||
|
||||
lucide-react@^0.367.0:
|
||||
version "0.367.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.367.0.tgz#dfec6b46442a647506b9a4e3878829e36c2083ce"
|
||||
integrity sha512-3FWiBaJiqMrx5a1sjH3CVdPqWnw/Z/PTVeeTDmOeILSs+8Ah+VhCd4FQMeHo6Z0WxHcm9piIOtilQwvceiCCKQ==
|
||||
|
||||
lucide-react@^0.387.0:
|
||||
version "0.387.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.387.0.tgz#1939eb2d5ab4a924d617667e76e777eb1eb403e6"
|
||||
integrity sha512-NyB4oJZ0pzLHT/QgMpgCPbez6yqvz8QPBocMJBXQCInPpXcQVCUpcU1CDlRG8mT2j0KqodLQYp+F5zn8U86sXg==
|
||||
|
||||
luxon@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz"
|
||||
|
@ -9530,29 +9572,28 @@ next@14.0.5-canary.46:
|
|||
"@next/swc-win32-ia32-msvc" "14.0.5-canary.46"
|
||||
"@next/swc-win32-x64-msvc" "14.0.5-canary.46"
|
||||
|
||||
next@^14.0.4:
|
||||
version "14.0.4"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
|
||||
integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
|
||||
next@^14.2.4:
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.4.tgz#ef66c39c71e2d8ad0a3caa0383c8933f4663e4d1"
|
||||
integrity sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==
|
||||
dependencies:
|
||||
"@next/env" "14.0.4"
|
||||
"@swc/helpers" "0.5.2"
|
||||
"@next/env" "14.2.4"
|
||||
"@swc/helpers" "0.5.5"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001406"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
graceful-fs "^4.2.11"
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.1"
|
||||
watchpack "2.4.0"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "14.0.4"
|
||||
"@next/swc-darwin-x64" "14.0.4"
|
||||
"@next/swc-linux-arm64-gnu" "14.0.4"
|
||||
"@next/swc-linux-arm64-musl" "14.0.4"
|
||||
"@next/swc-linux-x64-gnu" "14.0.4"
|
||||
"@next/swc-linux-x64-musl" "14.0.4"
|
||||
"@next/swc-win32-arm64-msvc" "14.0.4"
|
||||
"@next/swc-win32-ia32-msvc" "14.0.4"
|
||||
"@next/swc-win32-x64-msvc" "14.0.4"
|
||||
"@next/swc-darwin-arm64" "14.2.4"
|
||||
"@next/swc-darwin-x64" "14.2.4"
|
||||
"@next/swc-linux-arm64-gnu" "14.2.4"
|
||||
"@next/swc-linux-arm64-musl" "14.2.4"
|
||||
"@next/swc-linux-x64-gnu" "14.2.4"
|
||||
"@next/swc-linux-x64-musl" "14.2.4"
|
||||
"@next/swc-win32-arm64-msvc" "14.2.4"
|
||||
"@next/swc-win32-ia32-msvc" "14.2.4"
|
||||
"@next/swc-win32-x64-msvc" "14.2.4"
|
||||
|
||||
nice-try@^1.0.4:
|
||||
version "1.0.5"
|
||||
|
@ -10302,12 +10343,12 @@ prism-react-renderer@2.1.0:
|
|||
"@types/prismjs" "^1.26.0"
|
||||
clsx "^1.2.1"
|
||||
|
||||
prisma@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.npmjs.org/prisma/-/prisma-5.3.1.tgz"
|
||||
integrity sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==
|
||||
prisma@^5.15.0:
|
||||
version "5.15.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.15.0.tgz#887c295caa1b81b8849d94a2751cc0e0994f86d1"
|
||||
integrity sha512-JA81ACQSCi3a7NUOgonOIkdx8PAVkO+HbUOxmd00Yb8DgIIEpr2V9+Qe/j6MLxIgWtE/OtVQ54rVjfYRbZsCfw==
|
||||
dependencies:
|
||||
"@prisma/engines" "5.3.1"
|
||||
"@prisma/engines" "5.15.0"
|
||||
|
||||
prismjs@1.29.0:
|
||||
version "1.29.0"
|
||||
|
@ -12660,7 +12701,7 @@ warning@^4.0.3:
|
|||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
watchpack@2.4.0, watchpack@^2.4.0:
|
||||
watchpack@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz"
|
||||
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue