New and Improved Screens (#1151)

This commit is contained in:
Luke Vella 2024-06-19 11:14:18 +01:00 committed by GitHub
parent 5461c57228
commit 997a1eec78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 1517 additions and 743 deletions

View file

@ -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
View file

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

View file

@ -46,11 +46,6 @@ const nextConfig = {
destination: "/settings/profile",
permanent: true,
},
{
source: "/",
destination: "/polls",
permanent: false,
},
];
},
};

View file

@ -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",

View file

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

View file

@ -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"
}

View 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>;
}

View 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>
);
}

View 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>
);
}

View file

@ -0,0 +1,3 @@
export default function Layout({ children }: { children?: React.ReactNode }) {
return <div>{children}</div>;
}

View file

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

View 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} />;
}

View file

@ -0,0 +1,8 @@
export type ScheduledEvent = {
id: string;
title: string;
start: Date;
duration: number;
timeZone: string | null;
participants: { name: string }[];
};

View 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} />;
}

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -1,3 +0,0 @@
export default function Loader() {
return null;
}

View file

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

View file

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

View file

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

View file

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

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

View 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>
);
}

View file

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

View file

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

View file

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

View file

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

View 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>
);
}

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

View file

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

View 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>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,5 +15,10 @@
".next/types/**/*.ts",
"vitest.config.mts",
],
"exclude": ["node_modules", ".next/**/*"],
"exclude": [
"node_modules",
".next/**/*",
"playwright-report",
"test-results",
],
}

View file

@ -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",

View file

@ -10,6 +10,7 @@
"types": "src/index.ts",
"dependencies": {
"@rallly/database": "*",
"@rallly/features": "*",
"@rallly/emails": "*",
"@rallly/utils": "*",
"@trpc/server": "^10.13.0",

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

View file

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

View file

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

View 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 ?? [],
}));
}),
});

View file

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

View file

@ -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"
}
}

View file

@ -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")

View file

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

View file

@ -0,0 +1,3 @@
export default function () {
return null;
}

View file

@ -0,0 +1,6 @@
{
"name": "@rallly/features",
"private": true,
"main": "index.ts",
"version": "0.0.0"
}

View 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;
}

View file

@ -0,0 +1,8 @@
{
"extends": "@rallly/tsconfig/next.json",
"compilerOptions": {
"baseUrl": ".",
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
}

View file

@ -6,6 +6,6 @@
"types": "src/index.ts",
"dependencies": {
"@heroicons/react": "^1.0.6",
"lucide-react": "^0.338.0"
"lucide-react": "^0.387.0"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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 };

View file

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

View file

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

View file

@ -2,9 +2,6 @@
"extends": "@rallly/tsconfig/next.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@rallly/ui/*": ["./src/*"],
},
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],

View file

@ -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
View file

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