mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-18 03:16:21 +02:00
✨ Update admin layout and pages (#976)
This commit is contained in:
parent
0ba7e9ce91
commit
a1bac0c986
81 changed files with 2053 additions and 1260 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
|
|
|
@ -52,13 +52,12 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
sentry: {
|
|
||||||
hideSourceMaps: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sentryWebpackPluginOptions = {
|
const sentryWebpackPluginOptions = {
|
||||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
org: "stack-snap",
|
||||||
|
project: "rallly",
|
||||||
|
// Additional config ocptions for the Sentry Webpack plugin. Keep in mind that
|
||||||
// the following options are set automatically, and overriding them is not
|
// the following options are set automatically, and overriding them is not
|
||||||
// recommended:
|
// recommended:
|
||||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||||
|
@ -70,8 +69,9 @@ const sentryWebpackPluginOptions = {
|
||||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const withBundleAnalyzerConfig = withBundleAnalyzer(nextConfig);
|
||||||
// Make sure adding Sentry options is the last code to run before exporting, to
|
// Make sure adding Sentry options is the last code to run before exporting, to
|
||||||
// ensure that your source maps include changes from all other Webpack plugins
|
// ensure that your source maps include changes from all other Webpack plugins
|
||||||
module.exports = withSentryConfig(
|
module.exports = process.env.SENTRY_AUTH_TOKEN
|
||||||
withBundleAnalyzer(nextConfig, sentryWebpackPluginOptions),
|
? withSentryConfig(withBundleAnalyzerConfig, sentryWebpackPluginOptions)
|
||||||
);
|
: withBundleAnalyzerConfig;
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
"iron-session": "^6.3.1",
|
"iron-session": "^6.3.1",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"next-auth": "^4.24.5",
|
"next-auth": "^4.24.5",
|
||||||
|
|
|
@ -112,9 +112,6 @@
|
||||||
"dates": "Dates",
|
"dates": "Dates",
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
"useLocaleDefaults": "Use locale defaults",
|
"useLocaleDefaults": "Use locale defaults",
|
||||||
"inviteParticipantsDescription": "Copy and share this 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.",
|
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"guestPollAlertDescription": "<0>Create an account</0> or <1>login</1> to claim this poll.",
|
"guestPollAlertDescription": "<0>Create an account</0> or <1>login</1> to claim this poll.",
|
||||||
|
@ -135,7 +132,6 @@
|
||||||
"permissionDenied": "Unauthorized",
|
"permissionDenied": "Unauthorized",
|
||||||
"permissionDeniedDescription": "If you are the poll creator, please login to access your poll",
|
"permissionDeniedDescription": "If you are the poll creator, please login to access your poll",
|
||||||
"loginDifferent": "Switch user",
|
"loginDifferent": "Switch user",
|
||||||
"share": "Share",
|
|
||||||
"timeShownIn": "Times shown in {timeZone}",
|
"timeShownIn": "Times shown in {timeZone}",
|
||||||
"editDetailsDescription": "Change the details of your event.",
|
"editDetailsDescription": "Change the details of your event.",
|
||||||
"finalizeDescription": "Select a final date for your event.",
|
"finalizeDescription": "Select a final date for your event.",
|
||||||
|
@ -210,14 +206,7 @@
|
||||||
"earlyAccess": "Get early access to new features",
|
"earlyAccess": "Get early access to new features",
|
||||||
"earlyAdopterDescription": "As an early adopter, you'll lock in your subscription rate and won't be affected by future price increases.",
|
"earlyAdopterDescription": "As an early adopter, you'll lock in your subscription rate and won't be affected by future price increases.",
|
||||||
"upgradeNowSaveLater": "Upgrade now, save later",
|
"upgradeNowSaveLater": "Upgrade now, save later",
|
||||||
"savePercent": "Save {percent}%",
|
|
||||||
"priceIncreaseSoon": "Price increase soon.",
|
|
||||||
"lockPrice": "Upgrade today to keep this price forever.",
|
|
||||||
"features": "Get access to all current and future Pro features!",
|
|
||||||
"noAds": "No ads",
|
|
||||||
"supportProject": "Support this project",
|
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"pleaseUpgrade": "Please upgrade to Pro to use this feature",
|
|
||||||
"pollSettingsDescription": "Customize the behaviour of your poll",
|
"pollSettingsDescription": "Customize the behaviour of your poll",
|
||||||
"requireParticipantEmailLabel": "Make email address required for participants",
|
"requireParticipantEmailLabel": "Make email address required for participants",
|
||||||
"hideParticipantsLabel": "Hide participant list from other participants",
|
"hideParticipantsLabel": "Hide participant list from other participants",
|
||||||
|
@ -226,8 +215,28 @@
|
||||||
"authErrorDescription": "There was an error logging you in. Please try again.",
|
"authErrorDescription": "There was an error logging you in. Please try again.",
|
||||||
"authErrorCta": "Go to login page",
|
"authErrorCta": "Go to login page",
|
||||||
"continueAs": "Continue as",
|
"continueAs": "Continue as",
|
||||||
"finalizeFeature": "Finalize",
|
|
||||||
"duplicateFeature": "Duplicate",
|
|
||||||
"pageMovedDescription": "Redirecting to <a>{newUrl}</a>",
|
"pageMovedDescription": "Redirecting to <a>{newUrl}</a>",
|
||||||
"notRegistered": "Don't have an account? <a>Register</a>"
|
"notRegistered": "Don't have an account? <a>Register</a>",
|
||||||
|
"comingSoon": "Coming Soon",
|
||||||
|
"integrations": "Integrations",
|
||||||
|
"contacts": "Contacts",
|
||||||
|
"unlockFeatures": "Unlock all Pro features.",
|
||||||
|
"back": "Back",
|
||||||
|
"pollStatusAll": "All",
|
||||||
|
"pollStatusLive": "Live",
|
||||||
|
"pollStatusFinalized": "Finalized",
|
||||||
|
"pending": "Pending",
|
||||||
|
"xMore": "{count} more",
|
||||||
|
"share": "Share",
|
||||||
|
"pageXOfY": "Page {currentPage} of {pageCount}",
|
||||||
|
"noParticipants": "No participants",
|
||||||
|
"userId": "User ID",
|
||||||
|
"aboutGuest": "Guest User",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
"use client";
|
import { cn } from "@rallly/ui";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { MenuIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
import { Sidebar } from "@/app/[locale]/(admin)/sidebar";
|
||||||
|
import { LogoLink } from "@/app/components/logo-link";
|
||||||
|
import { CurrentUserAvatar } from "@/components/user";
|
||||||
import { isSelfHosted } from "@/utils/constants";
|
import { isSelfHosted } from "@/utils/constants";
|
||||||
|
|
||||||
const Auth = ({ children }: { children: React.ReactNode }) => {
|
const Auth = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
@ -22,13 +27,57 @@ const Auth = ({ children }: { children: React.ReactNode }) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
function MobileNavigation() {
|
||||||
|
return (
|
||||||
|
<div className="lg:hidden shadow-sm bg-gray-100 border-b flex items-center justify-between px-4 py-3">
|
||||||
|
<LogoLink />
|
||||||
|
<div className="flex gap-x-2.5 justify-end">
|
||||||
|
<Link
|
||||||
|
href="/settings/profile"
|
||||||
|
className="inline-flex items-center w-7 h-9"
|
||||||
|
>
|
||||||
|
<CurrentUserAvatar size="sm" />
|
||||||
|
</Link>
|
||||||
|
<Button asChild variant="ghost">
|
||||||
|
<Link href="/menu">
|
||||||
|
<MenuIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Layout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
function SidebarLayout() {
|
||||||
|
return (
|
||||||
|
<div className="lg:flex h-full bg-gray-50">
|
||||||
|
<MobileNavigation />
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"hidden lg:flex lg:w-72 bg-gray-100 shrink-0 flex-col gap-y-5 overflow-y-auto border-r lg:px-6 lg:py-4 px-5 py-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<LogoLink />
|
||||||
|
</div>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
<div className={cn("grow overflow-auto bg-gray-50")}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isSelfHosted) {
|
if (isSelfHosted) {
|
||||||
return (
|
return (
|
||||||
<Auth>
|
<Auth>
|
||||||
<StandardLayout>{children}</StandardLayout>
|
<SidebarLayout />
|
||||||
</Auth>
|
</Auth>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <StandardLayout>{children}</StandardLayout>;
|
return <SidebarLayout />;
|
||||||
}
|
}
|
||||||
|
|
42
apps/web/src/app/[locale]/(admin)/menu-item.tsx
Normal file
42
apps/web/src/app/[locale]/(admin)/menu-item.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
import { Link } from "lucide-react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
import { IconComponent } from "@/types";
|
||||||
|
|
||||||
|
export function MenuItem({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
icon: Icon,
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
icon: IconComponent;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isCurrent = pathname === href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={cn(
|
||||||
|
isCurrent
|
||||||
|
? "bg-gray-200 text-indigo-600"
|
||||||
|
: "text-gray-700 hover:text-primary",
|
||||||
|
"group flex items-center gap-x-3 rounded-md py-2 px-3 text-sm leading-6 font-semibold",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
isCurrent
|
||||||
|
? "text-indigo-600"
|
||||||
|
: "text-gray-400 group-hover:text-indigo-600",
|
||||||
|
"h-5 w-5 shrink-0",
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
3
apps/web/src/app/[locale]/(admin)/new/loading.tsx
Normal file
3
apps/web/src/app/[locale]/(admin)/new/loading.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Loading() {
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -1,8 +1,37 @@
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Trans } from "react-i18next/TransWithoutContext";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PageContainer,
|
||||||
|
PageContent,
|
||||||
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
} from "@/app/components/page-layout";
|
||||||
import { getTranslation } from "@/app/i18n";
|
import { getTranslation } from "@/app/i18n";
|
||||||
import { CreatePoll } from "@/components/create-poll";
|
import { CreatePoll } from "@/components/create-poll";
|
||||||
|
|
||||||
export default function Page() {
|
export default async function Page({ params }: { params: { locale: string } }) {
|
||||||
return <CreatePoll />;
|
const { t } = await getTranslation(params.locale);
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader>
|
||||||
|
<div className="flex justify-between items-center gap-x-4">
|
||||||
|
<PageTitle>
|
||||||
|
<Trans t={t} i18nKey="polls" />
|
||||||
|
</PageTitle>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/polls">
|
||||||
|
<Trans t={t} i18nKey="cancel" defaults="Cancel" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<CreatePoll />
|
||||||
|
</PageContent>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
|
|
|
@ -1,9 +1,44 @@
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { PenBoxIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Trans } from "react-i18next/TransWithoutContext";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PageContainer,
|
||||||
|
PageContent,
|
||||||
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
} from "@/app/components/page-layout";
|
||||||
import { getTranslation } from "@/app/i18n";
|
import { getTranslation } from "@/app/i18n";
|
||||||
|
|
||||||
import { PollsPage } from "./polls-page";
|
import { PollsList } from "./polls-list";
|
||||||
|
|
||||||
export default function Page() {
|
export default async function Page({ params }: { params: { locale: string } }) {
|
||||||
return <PollsPage />;
|
const { t } = await getTranslation(params.locale);
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader>
|
||||||
|
<div className="flex justify-between items-center gap-x-4">
|
||||||
|
<PageTitle>
|
||||||
|
<Trans t={t} i18nKey="polls" />
|
||||||
|
</PageTitle>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/new">
|
||||||
|
<PenBoxIcon className="w-4 text-muted-foreground h-4" />
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
<Trans t={t} i18nKey="newPoll" />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<PollsList />
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
|
|
55
apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx
Normal file
55
apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
import { Trans } from "@/components/trans";
|
||||||
|
|
||||||
|
function PollFolder({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname() ?? "";
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const query = searchParams?.has("status")
|
||||||
|
? `?${searchParams?.toString()}`
|
||||||
|
: "";
|
||||||
|
const currentUrl = pathname + query;
|
||||||
|
const isActive = href === currentUrl;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
className={cn(
|
||||||
|
isActive
|
||||||
|
? "bg-gray-100"
|
||||||
|
: "shadow-sm text-muted-foreground hover:bg-gray-100 active:bg-gray-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link href={href}>{children}</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PollFolders() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<PollFolder href="/polls">
|
||||||
|
<Trans i18nKey="pollStatusAll" defaults="All" />
|
||||||
|
</PollFolder>
|
||||||
|
<PollFolder href="/polls?status=live">
|
||||||
|
<Trans i18nKey="pollStatusLive" defaults="Live" />
|
||||||
|
</PollFolder>
|
||||||
|
<PollFolder href="/polls?status=paused">
|
||||||
|
<Trans i18nKey="pollStatusPaused" defaults="Paused" />
|
||||||
|
</PollFolder>
|
||||||
|
<PollFolder href="/polls?status=finalized">
|
||||||
|
<Trans i18nKey="pollStatusFinalized" defaults="Finalized" />
|
||||||
|
</PollFolder>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
211
apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx
Normal file
211
apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
"use client";
|
||||||
|
import { PollStatus } from "@rallly/database";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||||
|
import { createColumnHelper, PaginationState } from "@tanstack/react-table";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { ArrowRightIcon, InboxIcon, PlusIcon, UsersIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { PollStatusBadge } from "@/components/poll-status";
|
||||||
|
import { Table } from "@/components/table";
|
||||||
|
import { Trans } from "@/components/trans";
|
||||||
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
|
const EmptyState = () => {
|
||||||
|
return (
|
||||||
|
<div className="py-24">
|
||||||
|
<div className="mx-auto max-w-md rounded-md border-2 w-full border-dashed border-gray-300 p-8 text-center">
|
||||||
|
<div className="mb-4">
|
||||||
|
<InboxIcon className="inline-block h-10 w-10 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold">
|
||||||
|
<Trans i18nKey="noPolls" defaults="No polls" />
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
<Trans
|
||||||
|
i18nKey="noPollsDescription"
|
||||||
|
defaults="Get started by creating a new poll."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button variant="primary" asChild={true}>
|
||||||
|
<Link href="/new">
|
||||||
|
<PlusIcon className="h-5 w-5" />
|
||||||
|
<Trans defaults="New Poll" i18nKey="newPoll" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Column = {
|
||||||
|
id: string;
|
||||||
|
status: PollStatus;
|
||||||
|
title: string;
|
||||||
|
createdAt: Date;
|
||||||
|
participants: { name: string }[];
|
||||||
|
timeZone: string | null;
|
||||||
|
event: {
|
||||||
|
start: Date;
|
||||||
|
duration: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Column>();
|
||||||
|
|
||||||
|
export function PollsList() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const pagination = React.useMemo<PaginationState>(
|
||||||
|
() => ({
|
||||||
|
pageIndex: (Number(searchParams?.get("page")) || 1) - 1,
|
||||||
|
pageSize: Number(searchParams?.get("pageSize")) || 10,
|
||||||
|
}),
|
||||||
|
[searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = trpc.polls.paginatedList.useQuery({ pagination });
|
||||||
|
const { adjustTimeZone } = useDayjs();
|
||||||
|
const columns = React.useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "title",
|
||||||
|
header: () => null,
|
||||||
|
size: 5000,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Link className="group block" href={`/poll/${row.original.id}`}>
|
||||||
|
<div className="flex items-center gap-x-2 mb-1 min-w-0">
|
||||||
|
<h3 className="font-semibold truncate text-gray-600 group-hover:text-gray-900">
|
||||||
|
{row.original.title}
|
||||||
|
</h3>
|
||||||
|
<ArrowRightIcon className="h-4 w-4 opacity-0 transition-all group-focus:translate-x-2 group-hover:opacity-100" />
|
||||||
|
</div>
|
||||||
|
{row.original.event ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{row.original.event.duration === 0
|
||||||
|
? adjustTimeZone(
|
||||||
|
row.original.event.start,
|
||||||
|
!row.original.timeZone,
|
||||||
|
).format("LL")
|
||||||
|
: `${adjustTimeZone(
|
||||||
|
row.original.event.start,
|
||||||
|
!row.original.timeZone,
|
||||||
|
).format("LL LT")} - ${adjustTimeZone(
|
||||||
|
dayjs(row.original.event.start).add(
|
||||||
|
row.original.event.duration,
|
||||||
|
"minutes",
|
||||||
|
),
|
||||||
|
!row.original.timeZone,
|
||||||
|
).format("LT")}`}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
<Trans i18nKey="pending" defaults="Pending" />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("status", {
|
||||||
|
header: () => null,
|
||||||
|
size: 200,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PollStatusBadge status={row.getValue("status")} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("createdAt", {
|
||||||
|
header: () => null,
|
||||||
|
size: 1000,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { createdAt } = row.original;
|
||||||
|
return (
|
||||||
|
<p className="text-sm whitespace-nowrap text-muted-foreground">
|
||||||
|
<time dateTime={createdAt.toDateString()}>
|
||||||
|
<Trans
|
||||||
|
i18nKey="createdTime"
|
||||||
|
values={{ relativeTime: dayjs(createdAt).fromNow() }}
|
||||||
|
/>
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("participants", {
|
||||||
|
header: () => null,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Tooltip delayDuration={100}>
|
||||||
|
<TooltipTrigger className="flex items-center text-muted-foreground gap-x-2">
|
||||||
|
<UsersIcon className="h-4 w-4" />
|
||||||
|
<span className="text-sm">
|
||||||
|
{row.original.participants.length}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{row.original.participants.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{row.original.participants
|
||||||
|
.slice(0, 10)
|
||||||
|
.map((participant, i) => (
|
||||||
|
<p key={i}>{participant.name}</p>
|
||||||
|
))}
|
||||||
|
{row.original.participants.length > 10 ? (
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="xMore"
|
||||||
|
defaults="{count} more"
|
||||||
|
values={{
|
||||||
|
count: row.original.participants.length - 5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Trans i18nKey="noParticipants" defaults="No participants" />
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[adjustTimeZone],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
if (data.total === 0) return <EmptyState />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
layout="auto"
|
||||||
|
paginationState={pagination}
|
||||||
|
data={data.rows as Column[]}
|
||||||
|
pageCount={Math.ceil(data.total / pagination.pageSize)}
|
||||||
|
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.push(`${pathname}?${current.toString()}`);
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,174 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import {
|
|
||||||
InboxIcon,
|
|
||||||
PauseCircleIcon,
|
|
||||||
PlusIcon,
|
|
||||||
RadioIcon,
|
|
||||||
VoteIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
|
||||||
import { DateIcon } from "@/components/date-icon";
|
|
||||||
import {
|
|
||||||
TopBar,
|
|
||||||
TopBarTitle,
|
|
||||||
} from "@/components/layouts/standard-layout/top-bar";
|
|
||||||
import { ParticipantAvatarBar } from "@/components/participant-avatar-bar";
|
|
||||||
import { PollStatusBadge } from "@/components/poll-status";
|
|
||||||
import { Skeleton } from "@/components/skeleton";
|
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
|
||||||
import { trpc } from "@/utils/trpc/client";
|
|
||||||
|
|
||||||
const EmptyState = () => {
|
|
||||||
return (
|
|
||||||
<div className="p-8 lg:p-36">
|
|
||||||
<div className="mx-auto max-w-lg rounded-md border-2 border-dashed border-gray-300 p-8 text-center text-gray-600">
|
|
||||||
<div className="mb-4">
|
|
||||||
<InboxIcon className="inline-block h-10 w-10 text-gray-500" />
|
|
||||||
</div>
|
|
||||||
<h3>
|
|
||||||
<Trans i18nKey="noPolls" defaults="No polls" />
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
<Trans
|
|
||||||
i18nKey="noPollsDescription"
|
|
||||||
defaults="Get started by creating a new poll."
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<Button variant="primary" asChild={true}>
|
|
||||||
<Link href="/new">
|
|
||||||
<PlusIcon className="h-5 w-5" />
|
|
||||||
<Trans defaults="New Poll" i18nKey="newPoll" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function PollsPage() {
|
|
||||||
const { data } = trpc.polls.list.useQuery();
|
|
||||||
const { adjustTimeZone } = useDayjs();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TopBar className="flex items-center justify-between gap-4">
|
|
||||||
<TopBarTitle title={<Trans i18nKey="polls" />} icon={VoteIcon} />
|
|
||||||
<div>
|
|
||||||
<Button variant="primary" asChild={true}>
|
|
||||||
<Link href="/new">
|
|
||||||
<PlusIcon className="-ml-0.5 h-5 w-5" />
|
|
||||||
<Trans defaults="New Poll" i18nKey="newPoll" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TopBar>
|
|
||||||
<div>
|
|
||||||
<Container className="mx-auto p-3 sm:p-8">
|
|
||||||
{data ? (
|
|
||||||
data.length > 0 ? (
|
|
||||||
<div className="mx-auto grid max-w-3xl gap-3 sm:gap-4">
|
|
||||||
{data.map((poll) => {
|
|
||||||
const { title, id: pollId, createdAt, status } = poll;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={poll.id}
|
|
||||||
className="flex overflow-hidden rounded-md border shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="flex grow flex-col-reverse justify-between gap-x-4 gap-y-4 bg-white p-4 sm:flex-row sm:items-start sm:px-6">
|
|
||||||
<div className="flex gap-x-4">
|
|
||||||
<div className="sm:-ml-2">
|
|
||||||
{poll.event ? (
|
|
||||||
<DateIcon
|
|
||||||
date={adjustTimeZone(
|
|
||||||
poll.event.start,
|
|
||||||
!poll.timeZone,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="inline-flex h-14 w-14 items-center justify-center rounded-md border bg-gray-50 text-gray-400">
|
|
||||||
{status === "live" ? (
|
|
||||||
<RadioIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<PauseCircleIcon className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
{poll.event
|
|
||||||
? poll.event.duration > 0
|
|
||||||
? `${adjustTimeZone(
|
|
||||||
poll.event.start,
|
|
||||||
!poll.timeZone,
|
|
||||||
).format("LL LT")} - ${adjustTimeZone(
|
|
||||||
dayjs(poll.event.start).add(
|
|
||||||
poll.event.duration,
|
|
||||||
"minutes",
|
|
||||||
),
|
|
||||||
!poll.timeZone,
|
|
||||||
).format("LT")}`
|
|
||||||
: adjustTimeZone(
|
|
||||||
poll.event.start,
|
|
||||||
!poll.timeZone,
|
|
||||||
).format("LL")
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Link
|
|
||||||
href={`/poll/${pollId}`}
|
|
||||||
className="text-lg font-semibold tracking-tight hover:underline"
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
<Trans
|
|
||||||
i18nKey="createdTime"
|
|
||||||
defaults="Created {relativeTime}"
|
|
||||||
values={{
|
|
||||||
relativeTime: dayjs(createdAt).fromNow(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{poll.participants.length > 0 ? (
|
|
||||||
<div className="mt-4">
|
|
||||||
<ParticipantAvatarBar
|
|
||||||
participants={poll.participants}
|
|
||||||
max={5}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<PollStatusBadge status={status} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EmptyState />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="mx-auto grid max-w-3xl gap-3 sm:gap-4">
|
|
||||||
<Skeleton className="h-24 w-full" />
|
|
||||||
<Skeleton className="h-24 w-full" />
|
|
||||||
<Skeleton className="h-24 w-full" />
|
|
||||||
<Skeleton className="h-24 w-full" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -13,7 +13,6 @@ import { BillingPlans } from "@/components/billing/billing-plans";
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
SettingsContent,
|
SettingsContent,
|
||||||
SettingsHeader,
|
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "@/components/settings/settings";
|
} from "@/components/settings/settings";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -239,9 +238,6 @@ export function BillingPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Settings>
|
<Settings>
|
||||||
<SettingsHeader>
|
|
||||||
<Trans i18nKey="billing" />
|
|
||||||
</SettingsHeader>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("billing")}</title>
|
<title>{t("billing")}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
@ -257,6 +253,7 @@ export function BillingPage() {
|
||||||
>
|
>
|
||||||
<SubscriptionStatus />
|
<SubscriptionStatus />
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
<hr />
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={<Trans i18nKey="support" defaults="Support" />}
|
title={<Trans i18nKey="support" defaults="Support" />}
|
||||||
description={
|
description={
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Loading() {
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -1,10 +1,61 @@
|
||||||
"use client";
|
import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react";
|
||||||
import { ProfileLayout } from "@/components/layouts/profile-layout";
|
import React from "react";
|
||||||
|
import { Trans } from "react-i18next/TransWithoutContext";
|
||||||
|
|
||||||
export default function SettingsLayout({
|
import {
|
||||||
|
PageContainer,
|
||||||
|
PageContent,
|
||||||
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
} from "@/app/components/page-layout";
|
||||||
|
import { getTranslation } from "@/app/i18n";
|
||||||
|
import { isSelfHosted } from "@/utils/constants";
|
||||||
|
|
||||||
|
import { SettingsMenu } from "./menu-item";
|
||||||
|
|
||||||
|
export default async function ProfileLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
params,
|
||||||
children: React.ReactNode;
|
}: React.PropsWithChildren<{
|
||||||
}) {
|
params: { locale: string };
|
||||||
return <ProfileLayout>{children}</ProfileLayout>;
|
}>) {
|
||||||
|
const { t } = await getTranslation(params.locale);
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
title: t("profile"),
|
||||||
|
href: "/settings/profile",
|
||||||
|
icon: UserIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("preferences"),
|
||||||
|
href: "/settings/preferences",
|
||||||
|
icon: Settings2Icon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isSelfHosted) {
|
||||||
|
menuItems.push({
|
||||||
|
title: t("billing"),
|
||||||
|
href: "/settings/billing",
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader>
|
||||||
|
<div className="flex items-center justify-between gap-x-4">
|
||||||
|
<PageTitle>
|
||||||
|
<Trans t={t} i18nKey="settings" />
|
||||||
|
</PageTitle>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<SettingsMenu />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-4xl">{children}</div>
|
||||||
|
</PageContent>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
102
apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx
Normal file
102
apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@rallly/ui/select";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { isSelfHosted } from "@/utils/constants";
|
||||||
|
|
||||||
|
export function MenuItem(props: { href: string; children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={clsx(
|
||||||
|
"flex min-w-0 items-center gap-x-2 px-3 py-2 text-sm font-medium",
|
||||||
|
pathname === props.href
|
||||||
|
? "bg-gray-200"
|
||||||
|
: "text-gray-500 hover:text-gray-800",
|
||||||
|
)}
|
||||||
|
href={props.href}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsMenu() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const menuItems = React.useMemo(() => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: t("profile"),
|
||||||
|
href: "/settings/profile",
|
||||||
|
icon: UserIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("preferences"),
|
||||||
|
href: "/settings/preferences",
|
||||||
|
icon: Settings2Icon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (!isSelfHosted) {
|
||||||
|
items.push({
|
||||||
|
title: t("billing"),
|
||||||
|
href: "/settings/billing",
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => menuItems.find((item) => item.href === pathname),
|
||||||
|
[menuItems, pathname],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="hidden lg:inline-flex mb-4 border rounded-md p-0.5 gap-x-2">
|
||||||
|
{menuItems.map((item, i) => (
|
||||||
|
<MenuItem key={i} href={item.href}>
|
||||||
|
<item.icon className="h-4 w-4" />
|
||||||
|
{item.title}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={value?.title}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const item = menuItems.find((item) => item.title === value);
|
||||||
|
if (item) {
|
||||||
|
router.push(item.href);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="lg:hidden">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{menuItems.map((item, i) => (
|
||||||
|
<SelectItem key={i} value={item.title}>
|
||||||
|
<div className="flex items-center gap-x-2.5">
|
||||||
|
<item.icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="font-medium">{item.title}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Loading() {
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import { LanguagePreference } from "@/components/settings/language-preference";
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
SettingsContent,
|
SettingsContent,
|
||||||
SettingsHeader,
|
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "@/components/settings/settings";
|
} from "@/components/settings/settings";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -17,9 +16,6 @@ export function PreferencesPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Settings>
|
<Settings>
|
||||||
<SettingsHeader>
|
|
||||||
<Trans i18nKey="preferences" />
|
|
||||||
</SettingsHeader>
|
|
||||||
<SettingsContent>
|
<SettingsContent>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("settings")}</title>
|
<title>{t("settings")}</title>
|
||||||
|
@ -35,6 +31,7 @@ export function PreferencesPage() {
|
||||||
>
|
>
|
||||||
<LanguagePreference />
|
<LanguagePreference />
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
<hr />
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={<Trans i18nKey="dateAndTime" defaults="Date & Time" />}
|
title={<Trans i18nKey="dateAndTime" defaults="Date & Time" />}
|
||||||
description={
|
description={
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Loading() {
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -1,13 +1,19 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
||||||
|
import { Label } from "@rallly/ui/label";
|
||||||
|
import { InfoIcon, LogOutIcon, UserXIcon } from "lucide-react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import Link from "next/link";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import { LogoutButton } from "@/app/components/logout-button";
|
||||||
import { ProfileSettings } from "@/components/settings/profile-settings";
|
import { ProfileSettings } from "@/components/settings/profile-settings";
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
SettingsHeader,
|
SettingsContent,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "@/components/settings/settings";
|
} from "@/components/settings/settings";
|
||||||
|
import { TextInput } from "@/components/text-input";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
|
|
||||||
|
@ -15,17 +21,49 @@ export const ProfilePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
if (user.isGuest) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Settings>
|
<Settings>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("profile")}</title>
|
<title>{t("profile")}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SettingsHeader>
|
{user.isGuest ? (
|
||||||
<Trans i18nKey="profile" />
|
<SettingsContent>
|
||||||
</SettingsHeader>
|
<SettingsSection
|
||||||
|
title={<Trans i18nKey="profile" />}
|
||||||
|
description={<Trans i18nKey="profileDescription" />}
|
||||||
|
>
|
||||||
|
<Label className="mb-2.5">
|
||||||
|
<Trans i18nKey="userId" defaults="User ID" />
|
||||||
|
</Label>
|
||||||
|
<TextInput
|
||||||
|
className="w-full"
|
||||||
|
value={user.id.substring(0, 10)}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Alert className="mt-4" icon={InfoIcon}>
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans i18nKey="aboutGuest" defaults="Guest User" />
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans
|
||||||
|
i18nKey="aboutGuestDescription"
|
||||||
|
defaults="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."
|
||||||
|
components={[
|
||||||
|
<Link className="text-link" key={0} href="/login" />,
|
||||||
|
<Link className="text-link" key={1} href="/register" />,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<LogoutButton className="mt-6" variant="destructive">
|
||||||
|
<UserXIcon className="h-4 w-4" />
|
||||||
|
<Trans i18nKey="forgetMe" />
|
||||||
|
</LogoutButton>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsContent>
|
||||||
|
) : (
|
||||||
|
<SettingsContent>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={<Trans i18nKey="profile" defaults="Profile" />}
|
title={<Trans i18nKey="profile" defaults="Profile" />}
|
||||||
description={
|
description={
|
||||||
|
@ -37,32 +75,24 @@ export const ProfilePage = () => {
|
||||||
>
|
>
|
||||||
<ProfileSettings />
|
<ProfileSettings />
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
{/* <SettingsSection
|
<hr />
|
||||||
title={<Trans defaults="Email" i18nKey="settings_profile_email" />}
|
|
||||||
|
<SettingsSection
|
||||||
|
title={<Trans i18nKey="logout" />}
|
||||||
description={
|
description={
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="settings_profile_emailDescription"
|
i18nKey="logoutDescription"
|
||||||
defaults="Change your email address"
|
defaults="Sign out of your existing session"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChangeEmailForm />
|
<LogoutButton>
|
||||||
</SettingsSection> */}
|
<LogOutIcon className="h-4 w-4" />
|
||||||
{/* <SettingsSection
|
<Trans i18nKey="logout" defaults="Logout" />
|
||||||
title={<Trans i18nKey="deleteAccount" defaults="Delete Account" />}
|
</LogoutButton>
|
||||||
description={
|
</SettingsSection>
|
||||||
<Trans
|
</SettingsContent>
|
||||||
i18nKey="deleteAccountDescription"
|
)}
|
||||||
defaults="Delete your account here.
|
|
||||||
This action is not reversible. All information related to this
|
|
||||||
account will be deleted permanently."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button htmlType="submit" variant="destructive">
|
|
||||||
<Trans i18nKey="deleteMyAccount" defaults="Yes, delete my account" />
|
|
||||||
</Button>
|
|
||||||
</SettingsSection> */}
|
|
||||||
</Settings>
|
</Settings>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
161
apps/web/src/app/[locale]/(admin)/sidebar.tsx
Normal file
161
apps/web/src/app/[locale]/(admin)/sidebar.tsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import {
|
||||||
|
BlocksIcon,
|
||||||
|
BookMarkedIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
LogInIcon,
|
||||||
|
Settings2Icon,
|
||||||
|
SparklesIcon,
|
||||||
|
UsersIcon,
|
||||||
|
VoteIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
import { ProBadge } from "@/components/pro-badge";
|
||||||
|
import { Trans } from "@/components/trans";
|
||||||
|
import { CurrentUserAvatar } from "@/components/user";
|
||||||
|
import { IfGuest, useUser } from "@/components/user-provider";
|
||||||
|
import { IfFreeUser } from "@/contexts/plan";
|
||||||
|
import { IconComponent } from "@/types";
|
||||||
|
|
||||||
|
function NavItem({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
icon: Icon,
|
||||||
|
current,
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
icon: IconComponent;
|
||||||
|
children: React.ReactNode;
|
||||||
|
current?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
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-3 rounded-md py-2 px-3 text-sm leading-6 font-semibold",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
current ? "text-gray-500" : "text-gray-400 group-hover:text-gray-500",
|
||||||
|
"h-5 w-5 shrink-0",
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { user } = useUser();
|
||||||
|
return (
|
||||||
|
<nav className="flex flex-1 flex-col ">
|
||||||
|
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||||
|
<li>
|
||||||
|
<ul role="list" className="-mx-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
<NavItem
|
||||||
|
current={pathname?.startsWith("/poll")}
|
||||||
|
href="/polls"
|
||||||
|
icon={VoteIcon}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="polls" defaults="Polls" />
|
||||||
|
</NavItem>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||||
|
<Trans i18nKey="comingSoon" defaults="Coming Soon" />
|
||||||
|
</div>
|
||||||
|
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
||||||
|
<li className="grid gap-1 pointer-events-none 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="-mx-2 space-y-1">
|
||||||
|
<IfFreeUser>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/settings/billing"
|
||||||
|
className="border rounded-md mb-4 px-4 py-3 bg-gray-50 hover:bg-gray-200 active:bg-gray-300 border-gray-200 hover:border-gray-300 grid"
|
||||||
|
>
|
||||||
|
<span className="flex mb-2 items-center gap-x-2">
|
||||||
|
<SparklesIcon className="h-5 text-gray-400 w-5" />
|
||||||
|
<span className="font-bold text-sm">
|
||||||
|
<Trans i18nKey="upgrade" />
|
||||||
|
</span>
|
||||||
|
<ProBadge />
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-500 leading-relaxed text-sm">
|
||||||
|
<Trans
|
||||||
|
i18nKey="unlockFeatures"
|
||||||
|
defaults="Unlock all Pro features."
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</IfFreeUser>
|
||||||
|
<IfGuest>
|
||||||
|
<li>
|
||||||
|
<NavItem href="/login" icon={LogInIcon}>
|
||||||
|
<Trans i18nKey="login" />
|
||||||
|
</NavItem>
|
||||||
|
</li>
|
||||||
|
</IfGuest>
|
||||||
|
<li>
|
||||||
|
<NavItem href="/settings/preferences" icon={Settings2Icon}>
|
||||||
|
<Trans i18nKey="preferences" />
|
||||||
|
</NavItem>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr className="my-2" />
|
||||||
|
<ul role="list" className="-mx-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="ghost"
|
||||||
|
className="group h-auto py-3 w-full justify-start"
|
||||||
|
>
|
||||||
|
<Link href="/settings/profile">
|
||||||
|
<CurrentUserAvatar />
|
||||||
|
<span className="grid ml-1 grow">
|
||||||
|
<span className="font-semibold">{user.name}</span>
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ChevronRightIcon className="h-4 w-4 opacity-0 group-hover:opacity-100 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,8 +3,8 @@ import { NextResponse } from "next/server";
|
||||||
import { resetUser } from "@/app/guest";
|
import { resetUser } from "@/app/guest";
|
||||||
import { absoluteUrl } from "@/utils/absolute-url";
|
import { absoluteUrl } from "@/utils/absolute-url";
|
||||||
|
|
||||||
export async function GET() {
|
export async function POST() {
|
||||||
const res = NextResponse.redirect(absoluteUrl());
|
const res = NextResponse.redirect(absoluteUrl("/login"), 302);
|
||||||
await resetUser(res);
|
await resetUser(res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
|
@ -6,34 +6,7 @@ import { getTranslation } from "@/app/i18n";
|
||||||
import { absoluteUrl } from "@/utils/absolute-url";
|
import { absoluteUrl } from "@/utils/absolute-url";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <>{children}</>;
|
||||||
<div className="relative">
|
|
||||||
<svg
|
|
||||||
className="absolute inset-x-0 top-0 z-10 hidden h-[64rem] w-full stroke-gray-300/75 [mask-image:radial-gradient(800px_800px_at_center,white,transparent)] sm:block"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<pattern
|
|
||||||
id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84"
|
|
||||||
width={240}
|
|
||||||
height={240}
|
|
||||||
x="50%"
|
|
||||||
y={-1}
|
|
||||||
patternUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<path d="M.5 240V.5H240" fill="none" />
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
strokeWidth={0}
|
|
||||||
fill="url(#1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div className="relative z-20">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Link from "next/link";
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { PageHeader } from "@/app/components/page-layout";
|
||||||
import { Poll } from "@/components/poll";
|
import { Poll } from "@/components/poll";
|
||||||
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -61,7 +62,8 @@ const GoToApp = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-2 p-3">
|
<PageHeader variant="ghost">
|
||||||
|
<div className="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
@ -69,7 +71,7 @@ const GoToApp = () => {
|
||||||
className={poll.userId !== user.id ? "hidden" : ""}
|
className={poll.userId !== user.id ? "hidden" : ""}
|
||||||
>
|
>
|
||||||
<Link href={`/poll/${poll.id}`}>
|
<Link href={`/poll/${poll.id}`}>
|
||||||
<ArrowUpLeftIcon className="h-4 w-4" />
|
<ArrowUpLeftIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="manage" />
|
<Trans i18nKey="manage" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -78,6 +80,7 @@ const GoToApp = () => {
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</PageHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,23 +90,10 @@ export default function InvitePage() {
|
||||||
<LegacyPollContextProvider>
|
<LegacyPollContextProvider>
|
||||||
<VisibilityProvider>
|
<VisibilityProvider>
|
||||||
<GoToApp />
|
<GoToApp />
|
||||||
<div className="mx-auto max-w-4xl space-y-4 px-3 sm:py-8">
|
<div className="lg:px-6 lg:py-5 p-3">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="-mx-1">
|
||||||
<Poll />
|
<Poll />
|
||||||
<div className="mt-4 space-y-4 text-center text-gray-500">
|
|
||||||
<div className="py-8">
|
|
||||||
<Trans
|
|
||||||
defaults="Powered by <a>{name}</a>"
|
|
||||||
i18nKey="poweredByRallly"
|
|
||||||
values={{ name: "rallly.co" }}
|
|
||||||
components={{
|
|
||||||
a: (
|
|
||||||
<Link
|
|
||||||
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold"
|
|
||||||
href="https://rallly.co"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import "tailwindcss/tailwind.css";
|
||||||
import "../../style.css";
|
import "../../style.css";
|
||||||
|
|
||||||
import languages from "@rallly/languages";
|
import languages from "@rallly/languages";
|
||||||
|
import { Toaster } from "@rallly/ui/toaster";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ export default function Root({
|
||||||
return (
|
return (
|
||||||
<html lang={locale} className={inter.className}>
|
<html lang={locale} className={inter.className}>
|
||||||
<body className="h-screen overflow-y-scroll">
|
<body className="h-screen overflow-y-scroll">
|
||||||
|
<Toaster />
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
19
apps/web/src/app/[locale]/menu/back-button.tsx
Normal file
19
apps/web/src/app/[locale]/menu/back-button.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export function BackButton() {
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
router.back();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
30
apps/web/src/app/[locale]/menu/page.tsx
Normal file
30
apps/web/src/app/[locale]/menu/page.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { Sidebar } from "@/app/[locale]/(admin)/sidebar";
|
||||||
|
import { BackButton } from "@/app/[locale]/menu/back-button";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-100">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<Link
|
||||||
|
className="active:translate-y-1 transition-transform inline-block"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/logo-mark.svg"
|
||||||
|
alt="Rallly"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="shrink-0"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<BackButton />
|
||||||
|
</div>
|
||||||
|
<div className="px-5 py-5">
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
3
apps/web/src/app/[locale]/poll/[urlId]/loading.tsx
Normal file
3
apps/web/src/app/[locale]/poll/[urlId]/loading.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function Loading() {
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -35,10 +35,10 @@ const GuestPollAlert = () => {
|
||||||
defaults="<0>Create an account</0> or <1>login</1> to claim this poll."
|
defaults="<0>Create an account</0> or <1>login</1> to claim this poll."
|
||||||
components={[
|
components={[
|
||||||
<RegisterLink
|
<RegisterLink
|
||||||
className="hover:text-primary underline"
|
className="hover:text-gray-800 underline"
|
||||||
key="register"
|
key="register"
|
||||||
/>,
|
/>,
|
||||||
<LoginLink className="hover:text-primary underline" key="login" />,
|
<LoginLink className="hover:text-gray-800 underline" key="login" />,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
@ -48,9 +48,11 @@ const GuestPollAlert = () => {
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className={cn("mx-auto w-full max-w-4xl space-y-3 sm:space-y-4")}>
|
<div className={cn("max-w-4xl space-y-4 mx-auto")}>
|
||||||
|
<div className="-mx-1 space-y-3 sm:space-y-6">
|
||||||
<GuestPollAlert />
|
<GuestPollAlert />
|
||||||
<Poll />
|
<Poll />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
20
apps/web/src/app/components/logo-link.tsx
Normal file
20
apps/web/src/app/components/logo-link.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export function LogoLink() {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="active:translate-y-1 transition-transform inline-block"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/logo-mark.svg"
|
||||||
|
alt="Rallly"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
priority={true}
|
||||||
|
className="shrink-0"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
14
apps/web/src/app/components/logout-button.tsx
Normal file
14
apps/web/src/app/components/logout-button.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Button, ButtonProps } from "@rallly/ui/button";
|
||||||
|
|
||||||
|
export function LogoutButton({
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: React.PropsWithChildren<ButtonProps>) {
|
||||||
|
return (
|
||||||
|
<form action="/auth/logout" method="POST">
|
||||||
|
<Button {...rest} type="submit">
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
57
apps/web/src/app/components/page-layout.tsx
Normal file
57
apps/web/src/app/components/page-layout.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
"use client";
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
|
||||||
|
export function PageContainer({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: React.PropsWithChildren<{ className?: string }>) {
|
||||||
|
return <div className={cn("", className)}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageTitle({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<h2 className={cn("font-semibold leading-9 truncate", className)}>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageHeader({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
variant?: "default" | "ghost";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"lg:px-6 lg:py-3 px-4 py-3",
|
||||||
|
{
|
||||||
|
"border-b bg-gray-50 sticky z-20 top-0": variant === "default",
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageContent({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return <div className={cn("lg:p-6 p-4", className)}>{children}</div>;
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ export const VerifyCode: React.FunctionComponent<{
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h1 className="mb-1">{t("verifyYourEmail")}</h1>
|
<h1 className="mb-1 font-bold text-2xl">{t("verifyYourEmail")}</h1>
|
||||||
<div className="mb-4 text-gray-500">
|
<div className="mb-4 text-gray-500">
|
||||||
{t("stepSummary", {
|
{t("stepSummary", {
|
||||||
current: 2,
|
current: 2,
|
||||||
|
@ -60,6 +60,7 @@ export const VerifyCode: React.FunctionComponent<{
|
||||||
b: <strong className="whitespace-nowrap" />,
|
b: <strong className="whitespace-nowrap" />,
|
||||||
a: (
|
a: (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
role="button"
|
role="button"
|
||||||
className="text-link"
|
className="text-link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -71,9 +71,9 @@ export const BillingPlans = () => {
|
||||||
</BillingPlan>
|
</BillingPlan>
|
||||||
<div className="space-y-4 rounded-md border p-4">
|
<div className="space-y-4 rounded-md border p-4">
|
||||||
<div>
|
<div>
|
||||||
<h3>
|
<BillingPlanTitle>
|
||||||
<Trans i18nKey="planPro" />
|
<Trans i18nKey="planPro" />
|
||||||
</h3>
|
</BillingPlanTitle>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="planProDescription"
|
i18nKey="planProDescription"
|
||||||
|
@ -132,7 +132,7 @@ export const BillingPlans = () => {
|
||||||
<TrendingUpIcon className="text-indigo mr-2 mt-0.5 h-6 w-6 shrink-0" />
|
<TrendingUpIcon className="text-indigo mr-2 mt-0.5 h-6 w-6 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 flex items-center gap-x-2">
|
<div className="mb-2 flex items-center gap-x-2">
|
||||||
<h3 className="text-sm">
|
<h3 className="text-sm font-semibold">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="upgradeNowSaveLater"
|
i18nKey="upgradeNowSaveLater"
|
||||||
defaults="Upgrade now, save later"
|
defaults="Upgrade now, save later"
|
||||||
|
|
|
@ -5,8 +5,6 @@ export const Container = ({
|
||||||
className,
|
className,
|
||||||
}: React.PropsWithChildren<{ className?: string }>) => {
|
}: React.PropsWithChildren<{ className?: string }>) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("mx-auto max-w-7xl px-3 sm:px-8", className)}>
|
<div className={cn("mx-auto max-w-7xl px-4", className)}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { useUnmount } from "react-use";
|
||||||
|
|
||||||
import { PollSettingsForm } from "@/components/forms/poll-settings";
|
import { PollSettingsForm } from "@/components/forms/poll-settings";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
import { setCookie } from "@/utils/cookies";
|
||||||
import { usePostHog } from "@/utils/posthog";
|
import { usePostHog } from "@/utils/posthog";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
|
@ -62,12 +63,16 @@ export const CreatePoll: React.FunctionComponent = () => {
|
||||||
|
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const queryClient = trpc.useUtils();
|
const queryClient = trpc.useUtils();
|
||||||
const createPoll = trpc.polls.create.useMutation();
|
const createPoll = trpc.polls.create.useMutation({
|
||||||
|
networkMode: "always",
|
||||||
|
onSuccess: () => {
|
||||||
|
setCookie("new-poll", "1");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
className="pb-16"
|
|
||||||
onSubmit={form.handleSubmit(async (formData) => {
|
onSubmit={form.handleSubmit(async (formData) => {
|
||||||
const title = required(formData?.title);
|
const title = required(formData?.title);
|
||||||
|
|
||||||
|
@ -100,7 +105,7 @@ export const CreatePoll: React.FunctionComponent = () => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="mx-auto max-w-4xl space-y-4 p-2 sm:p-8">
|
<div className="mx-auto max-w-4xl space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const DateCard: React.FunctionComponent<DateCardProps> = ({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{annotation ? (
|
{annotation ? (
|
||||||
<div className="absolute -right-3 -top-3 z-20">{annotation}</div>
|
<div className="absolute -right-3 -top-3 z-10">{annotation}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
{dow ? (
|
{dow ? (
|
||||||
|
|
|
@ -39,16 +39,12 @@ export const EventCard = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card fullWidthOnMobile={false}>
|
<Card className="overflow-visible" fullWidthOnMobile={false}>
|
||||||
<div className="divide-y">
|
|
||||||
<div
|
<div
|
||||||
className="h-2"
|
className="h-2 -mx-px rounded-t-md -mt-px"
|
||||||
style={{ background: generateGradient(poll.id) }}
|
style={{ background: generateGradient(poll.id) }}
|
||||||
/>
|
/>
|
||||||
<div className="bg-pattern p-4 sm:flex sm:flex-row-reverse sm:justify-between sm:px-6">
|
<div className="bg-pattern p-4 sm:flex grid gap-4 sm:justify-between sm:px-6">
|
||||||
<div className="mb-2">
|
|
||||||
<PollStatusBadge status={poll.status} />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-start gap-4 sm:gap-6">
|
<div className="flex items-start gap-4 sm:gap-6">
|
||||||
{poll.event ? (
|
{poll.event ? (
|
||||||
|
@ -59,6 +55,12 @@ export const EventCard = () => {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
|
<h1
|
||||||
|
className="text-xl font-bold tracking-tight mb-1"
|
||||||
|
data-testid="poll-title"
|
||||||
|
>
|
||||||
|
{preventWidows(poll.title)}
|
||||||
|
</h1>
|
||||||
{poll.event ? (
|
{poll.event ? (
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
{poll.event.duration === 0
|
{poll.event.duration === 0
|
||||||
|
@ -77,12 +79,6 @@ export const EventCard = () => {
|
||||||
).format("LT")}`}
|
).format("LT")}`}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<h1
|
|
||||||
className="text-xl font-bold tracking-tight sm:text-2xl"
|
|
||||||
data-testid="poll-title"
|
|
||||||
>
|
|
||||||
{preventWidows(poll.title)}
|
|
||||||
</h1>
|
|
||||||
{!poll.event ? (
|
{!poll.event ? (
|
||||||
<PollSubheader />
|
<PollSubheader />
|
||||||
) : (
|
) : (
|
||||||
|
@ -102,24 +98,27 @@ export const EventCard = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<PollStatusBadge status={poll.status} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 p-4 sm:px-6">
|
<div className="space-y-4 p-4 sm:px-6">
|
||||||
{poll.description ? (
|
{poll.description ? (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<TextIcon className="h-4 w-4 shrink-0 translate-y-1" />
|
<TextIcon className="h-4 w-4 text-muted-foreground shrink-0 translate-y-1" />
|
||||||
<div className="whitespace-pre-line leading-relaxed">
|
<div className="whitespace-pre-line">
|
||||||
<TruncatedLinkify>{poll.description}</TruncatedLinkify>
|
<TruncatedLinkify>{poll.description}</TruncatedLinkify>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{poll.location ? (
|
{poll.location ? (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<MapPinIcon className="h-4 w-4 translate-y-1" />
|
<MapPinIcon className="h-4 w-4 translate-y-1 text-muted-foreground" />
|
||||||
<TruncatedLinkify>{poll.location}</TruncatedLinkify>
|
<TruncatedLinkify>{poll.location}</TruncatedLinkify>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<MousePointerClickIcon className="h-4 w-4 shrink-0 translate-y-0.5" />
|
<MousePointerClickIcon className="h-4 w-4 shrink-0 text-muted-foreground translate-y-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
<span className="inline-flex items-center space-x-1">
|
<span className="inline-flex items-center space-x-1">
|
||||||
|
@ -138,7 +137,6 @@ export const EventCard = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,12 +16,10 @@ import { useParticipants } from "@/components/participants-provider";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
|
|
||||||
export const InviteDialog = () => {
|
export function CopyInviteLinkButton() {
|
||||||
const { participants } = useParticipants();
|
const [didCopy, setDidCopy] = React.useState(false);
|
||||||
const [isOpen, setIsOpen] = React.useState(participants.length === 0);
|
|
||||||
const poll = usePoll();
|
|
||||||
|
|
||||||
const [state, copyToClipboard] = useCopyToClipboard();
|
const [state, copyToClipboard] = useCopyToClipboard();
|
||||||
|
const poll = usePoll();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (state.error) {
|
if (state.error) {
|
||||||
|
@ -29,15 +27,36 @@ export const InviteDialog = () => {
|
||||||
}
|
}
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
const [didCopy, setDidCopy] = React.useState(false);
|
return (
|
||||||
|
<Button
|
||||||
|
className="grow min-w-0"
|
||||||
|
onClick={() => {
|
||||||
|
copyToClipboard(poll.inviteLink);
|
||||||
|
setDidCopy(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDidCopy(false);
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{didCopy ? (
|
||||||
|
<Trans i18nKey="copied" />
|
||||||
|
) : (
|
||||||
|
<span className="truncate min-w-0">{`${window.location.hostname}/invite/${poll.id}`}</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InviteDialog = () => {
|
||||||
|
const { participants } = useParticipants();
|
||||||
|
const [isOpen, setIsOpen] = React.useState(participants.length === 0);
|
||||||
|
const poll = usePoll();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild={true}>
|
<DialogTrigger asChild={true}>
|
||||||
<Button variant="primary" icon={Share2Icon}>
|
<Button variant="primary" icon={Share2Icon}>
|
||||||
<span className="hidden sm:block">
|
|
||||||
<Trans i18nKey="share" defaults="Share" />
|
<Trans i18nKey="share" defaults="Share" />
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
@ -63,22 +82,7 @@ export const InviteDialog = () => {
|
||||||
<Trans i18nKey="inviteLink" defaults="Invite Link" />
|
<Trans i18nKey="inviteLink" defaults="Invite Link" />
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<CopyInviteLinkButton />
|
||||||
className="w-full min-w-0 bg-gray-50 px-2.5"
|
|
||||||
onClick={() => {
|
|
||||||
copyToClipboard(poll.inviteLink);
|
|
||||||
setDidCopy(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setDidCopy(false);
|
|
||||||
}, 1000);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{didCopy ? (
|
|
||||||
<Trans i18nKey="copied" />
|
|
||||||
) : (
|
|
||||||
<span className="flex truncate">{poll.inviteLink}</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link target="_blank" href={`/invite/${poll.id}`}>
|
<Link target="_blank" href={`/invite/${poll.id}`}>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
FileBarChart,
|
ListIcon,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
PauseCircleIcon,
|
PauseCircleIcon,
|
||||||
|
@ -19,18 +19,18 @@ import {
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
ShieldCloseIcon,
|
ShieldCloseIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Head from "next/head";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
import { LogoutButton } from "@/app/components/logout-button";
|
||||||
import { InviteDialog } from "@/components/invite-dialog";
|
|
||||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
|
||||||
import {
|
import {
|
||||||
TopBar,
|
PageContainer,
|
||||||
TopBarTitle,
|
PageContent,
|
||||||
} from "@/components/layouts/standard-layout/top-bar";
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
} from "@/app/components/page-layout";
|
||||||
|
import { InviteDialog } from "@/components/invite-dialog";
|
||||||
import { LoginLink } from "@/components/login-link";
|
import { LoginLink } from "@/components/login-link";
|
||||||
import {
|
import {
|
||||||
PageDialog,
|
PageDialog,
|
||||||
|
@ -43,14 +43,11 @@ import ManagePoll from "@/components/poll/manage-poll";
|
||||||
import NotificationsToggle from "@/components/poll/notifications-toggle";
|
import NotificationsToggle from "@/components/poll/notifications-toggle";
|
||||||
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
||||||
import { PollStatusLabel } from "@/components/poll-status";
|
import { PollStatusLabel } from "@/components/poll-status";
|
||||||
import { Skeleton } from "@/components/skeleton";
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
import { NextPageWithLayout } from "../../types";
|
|
||||||
|
|
||||||
const StatusControl = () => {
|
const StatusControl = () => {
|
||||||
const poll = usePoll();
|
const poll = usePoll();
|
||||||
const queryClient = trpc.useUtils();
|
const queryClient = trpc.useUtils();
|
||||||
|
@ -152,41 +149,49 @@ const StatusControl = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AdminControls = () => {
|
const AdminControls = () => {
|
||||||
const poll = usePoll();
|
|
||||||
const pollLink = `/poll/${poll.id}`;
|
|
||||||
const pathname = usePathname();
|
|
||||||
return (
|
return (
|
||||||
<TopBar>
|
|
||||||
<div className="flex flex-col items-start justify-between gap-x-4 gap-y-2 sm:flex-row">
|
|
||||||
<div className="flex min-w-0 gap-4">
|
|
||||||
{pathname !== pollLink ? (
|
|
||||||
<Button asChild>
|
|
||||||
<Link href={pollLink}>
|
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
<TopBarTitle title={poll?.title} icon={FileBarChart} />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<NotificationsToggle />
|
<NotificationsToggle />
|
||||||
<StatusControl />
|
<StatusControl />
|
||||||
<ManagePoll />
|
<ManagePoll />
|
||||||
<InviteDialog />
|
<InviteDialog />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</TopBar>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Layout = ({ children }: React.PropsWithChildren) => {
|
const Layout = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const poll = usePoll();
|
||||||
|
const pollLink = `/poll/${poll.id}`;
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 grow flex-col">
|
<PageContainer>
|
||||||
|
<PageHeader className="flex md:flex-row flex-col md:items-center gap-x-4 gap-y-2.5">
|
||||||
|
<div className="flex min-w-0 md:basis-2/3 items-center gap-x-4">
|
||||||
|
<div className="md:basis-1/2 flex gap-x-4">
|
||||||
|
{pathname === pollLink ? (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/polls">
|
||||||
|
<ListIcon className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href={pollLink}>
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<PageTitle>{poll.title}</PageTitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex basis-1/3 md:justify-end">
|
||||||
<AdminControls />
|
<AdminControls />
|
||||||
<div>
|
|
||||||
<Container className="py-3 sm:py-8">{children}</Container>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>{children}</PageContent>
|
||||||
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -218,21 +223,19 @@ export const PermissionGuard = ({ children }: React.PropsWithChildren) => {
|
||||||
</PageDialogHeader>
|
</PageDialogHeader>
|
||||||
<PageDialogFooter>
|
<PageDialogFooter>
|
||||||
{user.isGuest ? (
|
{user.isGuest ? (
|
||||||
<Button asChild variant="primary" size="lg">
|
<Button asChild variant="primary">
|
||||||
<LoginLink>
|
<LoginLink>
|
||||||
<LogInIcon className="-ml-1 h-5 w-5" />
|
<LogInIcon className="-ml-1 h-4 w-4" />
|
||||||
<Trans i18nKey="login" defaults="Login" />
|
<Trans i18nKey="login" defaults="Login" />
|
||||||
</LoginLink>
|
</LoginLink>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild variant="primary" size="lg">
|
<LogoutButton>
|
||||||
<Link href="/logout">
|
<LogOutIcon className="-ml-1 h-4 w-4" />
|
||||||
<LogOutIcon className="-ml-1 h-5 w-5" />
|
|
||||||
<Trans i18nKey="loginDifferent" defaults="Switch user" />
|
<Trans i18nKey="loginDifferent" defaults="Switch user" />
|
||||||
</Link>
|
</LogoutButton>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<Button asChild size="lg">
|
<Button asChild>
|
||||||
<Link href={`/invite/${poll.id}`}>
|
<Link href={`/invite/${poll.id}`}>
|
||||||
<Trans i18nKey="goToInvite" defaults="Go to Invite Page" />
|
<Trans i18nKey="goToInvite" defaults="Go to Invite Page" />
|
||||||
<ArrowUpRight className="h-4 w-4" />
|
<ArrowUpRight className="h-4 w-4" />
|
||||||
|
@ -246,15 +249,6 @@ export const PermissionGuard = ({ children }: React.PropsWithChildren) => {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Title = () => {
|
|
||||||
const poll = usePoll();
|
|
||||||
return (
|
|
||||||
<Head>
|
|
||||||
<title>{poll.title}</title>
|
|
||||||
</Head>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Prefetch = ({ children }: React.PropsWithChildren) => {
|
const Prefetch = ({ children }: React.PropsWithChildren) => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
@ -265,18 +259,7 @@ const Prefetch = ({ children }: React.PropsWithChildren) => {
|
||||||
const watchers = trpc.polls.getWatchers.useQuery({ pollId: urlId });
|
const watchers = trpc.polls.getWatchers.useQuery({ pollId: urlId });
|
||||||
|
|
||||||
if (!poll.data || !watchers.data || !participants.data) {
|
if (!poll.data || !watchers.data || !participants.data) {
|
||||||
return (
|
return null;
|
||||||
<div>
|
|
||||||
<TopBar className="flex flex-col items-start justify-between gap-x-4 gap-y-2 sm:flex-row">
|
|
||||||
<Skeleton className="my-2 h-5 w-48" />
|
|
||||||
<div className="flex gap-x-2">
|
|
||||||
<Skeleton className="h-9 w-24" />
|
|
||||||
<Skeleton className="h-9 w-24" />
|
|
||||||
<Skeleton className="h-9 w-24" />
|
|
||||||
</div>
|
|
||||||
</TopBar>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
@ -295,7 +278,6 @@ export const PollLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
<Prefetch>
|
<Prefetch>
|
||||||
<LegacyPollContextProvider>
|
<LegacyPollContextProvider>
|
||||||
<Title />
|
|
||||||
<PermissionGuard>
|
<PermissionGuard>
|
||||||
<Layout>{children}</Layout>
|
<Layout>{children}</Layout>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
|
@ -303,12 +285,3 @@ export const PollLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
</Prefetch>
|
</Prefetch>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPollLayout: NextPageWithLayout["getLayout"] =
|
|
||||||
function getLayout(page) {
|
|
||||||
return (
|
|
||||||
<StandardLayout>
|
|
||||||
<PollLayout>{page}</PollLayout>
|
|
||||||
</StandardLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
import { cn } from "@rallly/ui";
|
"use client";
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import { Card } from "@rallly/ui/card";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react";
|
||||||
CreditCardIcon,
|
|
||||||
MenuIcon,
|
|
||||||
Settings2Icon,
|
|
||||||
UserIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { useToggle } from "react-use";
|
import { useToggle } from "react-use";
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
import {
|
||||||
|
PageContainer,
|
||||||
|
PageContent,
|
||||||
|
PageHeader,
|
||||||
|
PageTitle,
|
||||||
|
} from "@/app/components/page-layout";
|
||||||
import { IfCloudHosted } from "@/contexts/environment";
|
import { IfCloudHosted } from "@/contexts/environment";
|
||||||
import { Plan } from "@/contexts/plan";
|
|
||||||
|
|
||||||
import { IconComponent } from "../../types";
|
import { IconComponent } from "../../types";
|
||||||
import { IfAuthenticated, useUser } from "../user-provider";
|
|
||||||
|
|
||||||
const MenuItem = (props: {
|
const MenuItem = (props: {
|
||||||
icon: IconComponent;
|
icon: IconComponent;
|
||||||
|
@ -30,10 +26,10 @@ const MenuItem = (props: {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 text-sm font-medium",
|
"flex min-w-0 items-center gap-x-2 px-3 py-2 text-sm font-medium",
|
||||||
pathname === props.href
|
pathname === props.href
|
||||||
? "bg-gray-200"
|
? "bg-gray-200"
|
||||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800",
|
: "text-gray-500 hover:text-gray-800",
|
||||||
)}
|
)}
|
||||||
href={props.href}
|
href={props.href}
|
||||||
>
|
>
|
||||||
|
@ -44,45 +40,28 @@ const MenuItem = (props: {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
|
export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
// reset toggle whenever route changes
|
// reset toggle whenever route changes
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [isMenuOpen, toggle] = useToggle(false);
|
const [, toggle] = useToggle(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
toggle(false);
|
toggle(false);
|
||||||
}, [pathname, toggle]);
|
}, [pathname, toggle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<PageContainer>
|
||||||
<Container className="p-2 sm:py-8">
|
<PageHeader>
|
||||||
<Card className="mx-auto flex flex-col overflow-hidden md:min-h-[600px]">
|
<div className="flex items-center justify-between gap-x-4">
|
||||||
<div className="border-b bg-gray-50 p-3 md:hidden">
|
<PageTitle>
|
||||||
<Button onClick={toggle} icon={MenuIcon} />
|
<Trans i18nKey="settings" />
|
||||||
|
</PageTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex grow md:divide-x">
|
</PageHeader>
|
||||||
<div
|
<PageContent>
|
||||||
className={cn(
|
<div className="inline-flex mb-4 border rounded-md p-0.5 gap-x-2">
|
||||||
"absolute inset-0 z-10 grow bg-gray-50 md:static md:block md:shrink-0 md:grow-0 md:basis-56 md:px-5 md:py-4",
|
|
||||||
{
|
|
||||||
hidden: !isMenuOpen,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<div className="flex items-center justify-between gap-x-2.5 gap-y-2 p-3">
|
|
||||||
<div className="truncate text-sm font-semibold">
|
|
||||||
{user.name}
|
|
||||||
</div>
|
|
||||||
<Plan />
|
|
||||||
</div>
|
|
||||||
<IfAuthenticated>
|
|
||||||
<MenuItem href="/settings/profile" icon={UserIcon}>
|
<MenuItem href="/settings/profile" icon={UserIcon}>
|
||||||
<Trans i18nKey="profile" defaults="Profile" />
|
<Trans i18nKey="profile" defaults="Profile" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</IfAuthenticated>
|
|
||||||
<MenuItem href="/settings/preferences" icon={Settings2Icon}>
|
<MenuItem href="/settings/preferences" icon={Settings2Icon}>
|
||||||
<Trans i18nKey="preferences" defaults="Preferences" />
|
<Trans i18nKey="preferences" defaults="Preferences" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -92,11 +71,8 @@ export const ProfileLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</IfCloudHosted>
|
</IfCloudHosted>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="max-w-4xl py-4">{children}</div>
|
||||||
<div className="max-w-2xl grow">{children}</div>
|
</PageContent>
|
||||||
</div>
|
</PageContainer>
|
||||||
</Card>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
|
||||||
import { ListIcon, SparklesIcon } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import React from "react";
|
|
||||||
import { Toaster } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { Clock, ClockPreferences } from "@/components/clock";
|
|
||||||
import { Container } from "@/components/container";
|
|
||||||
import {
|
|
||||||
FeaturebaseChangelog,
|
|
||||||
FeaturebaseIdentify,
|
|
||||||
} from "@/components/featurebase";
|
|
||||||
import FeedbackButton from "@/components/feedback";
|
|
||||||
import { LoginLink } from "@/components/login-link";
|
|
||||||
import { Logo } from "@/components/logo";
|
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
import { UserDropdown } from "@/components/user-dropdown";
|
|
||||||
import { IfCloudHosted } from "@/contexts/environment";
|
|
||||||
import { useSubscription } from "@/contexts/plan";
|
|
||||||
import { appVersion, isFeedbackEnabled } from "@/utils/constants";
|
|
||||||
|
|
||||||
import { IconComponent, NextPageWithLayout } from "../../types";
|
|
||||||
import ModalProvider from "../modal/modal-provider";
|
|
||||||
import { IfGuest } from "../user-provider";
|
|
||||||
|
|
||||||
const NavMenuItem = ({
|
|
||||||
href,
|
|
||||||
target,
|
|
||||||
label,
|
|
||||||
icon: Icon,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
icon: IconComponent;
|
|
||||||
href: string;
|
|
||||||
target?: string;
|
|
||||||
label: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}) => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
return (
|
|
||||||
<Button variant="ghost" asChild>
|
|
||||||
<Link
|
|
||||||
target={target}
|
|
||||||
href={href}
|
|
||||||
className={cn(
|
|
||||||
pathname === href
|
|
||||||
? "text-foreground"
|
|
||||||
: "text-muted-foreground hover:text-foreground active:bg-gray-200/50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Upgrade = () => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className="hidden sm:inline-flex"
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link href="/settings/billing">
|
|
||||||
<SparklesIcon className="-ml-0.5 h-4 w-4" />
|
|
||||||
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const LogoArea = () => {
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center gap-x-4">
|
|
||||||
<Link
|
|
||||||
href="/polls"
|
|
||||||
className={clsx(
|
|
||||||
"inline-block transition-transform active:translate-y-1",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Logo size="sm" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Changelog = () => {
|
|
||||||
return <FeaturebaseChangelog />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MainNav = () => {
|
|
||||||
const subscription = useSubscription();
|
|
||||||
return (
|
|
||||||
<m.div
|
|
||||||
variants={{
|
|
||||||
hidden: { y: -56, opacity: 0, height: 0 },
|
|
||||||
visible: { y: 0, opacity: 1, height: "auto" },
|
|
||||||
}}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
exit="hidden"
|
|
||||||
className="border-b bg-gray-50/50"
|
|
||||||
>
|
|
||||||
<Container className="flex h-14 items-center justify-between gap-x-2.5">
|
|
||||||
<div className="flex shrink-0 gap-x-4">
|
|
||||||
<LogoArea />
|
|
||||||
<nav className="hidden gap-x-2 sm:flex">
|
|
||||||
<NavMenuItem
|
|
||||||
icon={ListIcon}
|
|
||||||
href="/polls"
|
|
||||||
label={<Trans i18nKey="polls" defaults="Polls" />}
|
|
||||||
/>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-x-2.5">
|
|
||||||
<nav className="flex items-center gap-x-1 sm:gap-x-1.5">
|
|
||||||
<IfCloudHosted>
|
|
||||||
{subscription?.active === false ? <Upgrade /> : null}
|
|
||||||
</IfCloudHosted>
|
|
||||||
<IfGuest>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
asChild
|
|
||||||
className="hidden sm:flex"
|
|
||||||
>
|
|
||||||
<LoginLink>
|
|
||||||
<Trans i18nKey="login" defaults="Login" />
|
|
||||||
</LoginLink>
|
|
||||||
</Button>
|
|
||||||
</IfGuest>
|
|
||||||
<IfCloudHosted>
|
|
||||||
<Changelog />
|
|
||||||
</IfCloudHosted>
|
|
||||||
<ClockPreferences>
|
|
||||||
<Button size="sm" variant="ghost">
|
|
||||||
<Clock />
|
|
||||||
</Button>
|
|
||||||
</ClockPreferences>
|
|
||||||
</nav>
|
|
||||||
<UserDropdown />
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</m.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StandardLayout: React.FunctionComponent<{
|
|
||||||
children?: React.ReactNode;
|
|
||||||
hideNav?: boolean;
|
|
||||||
}> = ({ children, hideNav, ...rest }) => {
|
|
||||||
const key = hideNav ? "no-nav" : "nav";
|
|
||||||
return (
|
|
||||||
<ModalProvider>
|
|
||||||
<Toaster />
|
|
||||||
<div className="flex min-h-screen flex-col" {...rest}>
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{!hideNav ? <MainNav /> : null}
|
|
||||||
</AnimatePresence>
|
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
|
||||||
<m.div
|
|
||||||
key={key}
|
|
||||||
variants={{
|
|
||||||
hidden: { opacity: 0, y: -56 },
|
|
||||||
visible: { opacity: 1, y: 0 },
|
|
||||||
}}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
exit={{ opacity: 0, y: 56 }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</m.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
{appVersion ? (
|
|
||||||
<div className="fixed bottom-0 right-0 z-50 rounded-tl-md bg-gray-200/90">
|
|
||||||
<Link
|
|
||||||
className="px-2 py-1 text-xs tabular-nums tracking-tight"
|
|
||||||
target="_blank"
|
|
||||||
href={`https://github.com/lukevella/rallly/releases/${appVersion}`}
|
|
||||||
>
|
|
||||||
{`${appVersion}`}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{isFeedbackEnabled ? (
|
|
||||||
<>
|
|
||||||
<FeaturebaseIdentify />
|
|
||||||
<FeedbackButton />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</ModalProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStandardLayout: NextPageWithLayout["getLayout"] =
|
|
||||||
function getLayout(page) {
|
|
||||||
return <StandardLayout>{page}</StandardLayout>;
|
|
||||||
};
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { m } from "framer-motion";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
|
||||||
import { IconComponent } from "@/types";
|
|
||||||
|
|
||||||
export const TopBar = (
|
|
||||||
props: React.PropsWithChildren<{
|
|
||||||
title?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}>,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<m.div
|
|
||||||
initial={{ y: 0 }}
|
|
||||||
exit={{ y: -100 }}
|
|
||||||
className={cn(
|
|
||||||
"sticky top-0 z-20 border-b bg-gray-50/75 py-3 backdrop-blur-md",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Container className={cn(props.className)}>{props.children}</Container>
|
|
||||||
</m.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TopBarTitle = ({
|
|
||||||
// icon: Icon,
|
|
||||||
title,
|
|
||||||
}: {
|
|
||||||
icon?: IconComponent;
|
|
||||||
title: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex h-9 min-w-0 items-center gap-2.5">
|
|
||||||
{/* <Icon className="-ml-0.5 h-6 w-6 shrink-0" /> */}
|
|
||||||
<div className="truncate font-medium tracking-tight">{title}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -26,18 +26,18 @@ export const PageDialogHeader = (props: React.PropsWithChildren) => {
|
||||||
|
|
||||||
export const PageDialogFooter = (props: React.PropsWithChildren) => {
|
export const PageDialogFooter = (props: React.PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 flex flex-col items-center justify-center gap-x-4 gap-y-4 sm:flex-row">
|
<div className="mt-6 flex flex-col items-center justify-center gap-2.5 sm:flex-row">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const PageDialogTitle = (props: React.PropsWithChildren) => {
|
export const PageDialogTitle = (props: React.PropsWithChildren) => {
|
||||||
return <h1 className="text-3xl">{props.children}</h1>;
|
return <h1 className="text-2xl font-bold">{props.children}</h1>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageDialogDescription = (props: React.PropsWithChildren) => {
|
export const PageDialogDescription = (props: React.PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
<p className="max-w-xl text-base leading-relaxed text-gray-600">
|
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground">
|
||||||
{props.children}
|
{props.children}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,60 +1,16 @@
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { Badge } from "@rallly/ui/badge";
|
import { Badge } from "@rallly/ui/badge";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
|
|
||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
import {
|
|
||||||
CalendarCheck2Icon,
|
|
||||||
CopyIcon,
|
|
||||||
DatabaseIcon,
|
|
||||||
HeartIcon,
|
|
||||||
ImageOffIcon,
|
|
||||||
LockIcon,
|
|
||||||
Settings2Icon,
|
|
||||||
TrendingUpIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { UpgradeButton } from "@/components/upgrade-button";
|
|
||||||
import { usePlan } from "@/contexts/plan";
|
import { usePlan } from "@/contexts/plan";
|
||||||
import { IconComponent } from "@/types";
|
|
||||||
import { annualPriceUsd, monthlyPriceUsd } from "@/utils/constants";
|
|
||||||
|
|
||||||
const Feature = ({
|
|
||||||
icon: Icon,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
upcoming,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
icon: IconComponent;
|
|
||||||
upcoming?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}>) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
"flex translate-y-0 cursor-default items-center justify-center gap-x-2.5 rounded-full border bg-gray-50 p-1 pr-4 shadow-sm transition-all hover:-translate-y-1 hover:bg-white/50",
|
|
||||||
upcoming ? "bg-transparent` border-dashed shadow-none" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn("bg-primary rounded-full p-1 text-gray-50", className)}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
<div className="text-sm font-semibold">{children}</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Teaser = () => {
|
const Teaser = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [tab, setTab] = React.useState("yearly");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<m.div
|
<m.div
|
||||||
transition={{
|
transition={{
|
||||||
|
@ -89,7 +45,7 @@ const Teaser = () => {
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h2 className="text-center">
|
<h2 className="text-center font-bold text-xl">
|
||||||
<Trans defaults="Pro Feature" i18nKey="proFeature" />
|
<Trans defaults="Pro Feature" i18nKey="proFeature" />
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mx-auto max-w-xs text-center text-sm leading-relaxed">
|
<p className="text-muted-foreground mx-auto max-w-xs text-center text-sm leading-relaxed">
|
||||||
|
@ -99,119 +55,12 @@ const Teaser = () => {
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
|
||||||
className="flex flex-col items-center gap-4"
|
|
||||||
value={tab}
|
|
||||||
onValueChange={setTab}
|
|
||||||
>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="monthly">
|
|
||||||
<Trans i18nKey="billingPeriodMonthly" />
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="yearly">
|
|
||||||
<Trans i18nKey="billingPeriodYearly" />
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="monthly">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-start justify-center gap-2.5">
|
|
||||||
<div className=" text-4xl font-bold">${monthlyPriceUsd}</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-semibold leading-5">USD</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
<Trans i18nKey="monthlyBillingDescription" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="yearly">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-start justify-center gap-2.5">
|
|
||||||
<div className="flex items-end gap-2">
|
|
||||||
<div className="font-bold text-gray-500 line-through">
|
|
||||||
${monthlyPriceUsd}
|
|
||||||
</div>
|
|
||||||
<div className=" text-4xl font-bold">
|
|
||||||
${(annualPriceUsd / 12).toFixed(2)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mt-1 text-xs font-semibold">USD</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
<Trans i18nKey="annualBillingDescription" />
|
|
||||||
</div>
|
|
||||||
<p className="mt-2">
|
|
||||||
<span className="rounded border border-dashed border-green-400 px-1 py-0.5 text-xs text-green-500">
|
|
||||||
<Trans
|
|
||||||
i18nKey="savePercent"
|
|
||||||
defaults="Save {percent}%"
|
|
||||||
values={{
|
|
||||||
percent: (annualPriceUsd / 12 / monthlyPriceUsd) * 100,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-primary text-center text-xs">
|
|
||||||
<Link
|
|
||||||
className="text-link"
|
|
||||||
href="https://rallly.co/blog/july-recap"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<TrendingUpIcon className="mr-2 inline-block h-4 w-4" />
|
|
||||||
<Trans
|
|
||||||
i18nKey="priceIncreaseSoon"
|
|
||||||
defaults="Price increase soon."
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p className="text-center text-xs text-gray-400">
|
|
||||||
<LockIcon className="mr-2 inline-block h-4 w-4" />
|
|
||||||
<Trans
|
|
||||||
i18nKey="lockPrice"
|
|
||||||
defaults="Upgrade today to keep this price forever."
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<h3 className="mx-auto max-w-sm text-center">
|
|
||||||
<Trans
|
|
||||||
i18nKey="features"
|
|
||||||
defaults="Get access to all current and future Pro features!"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
<ul className="flex flex-wrap justify-center gap-2 border-gray-100 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-gray-100 via-transparent">
|
|
||||||
<Feature className="bg-violet-500" icon={ImageOffIcon}>
|
|
||||||
<Trans i18nKey="noAds" defaults="No ads" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-rose-500" icon={DatabaseIcon}>
|
|
||||||
<Trans
|
|
||||||
i18nKey="plan_extendedPollLife"
|
|
||||||
defaults="Extend poll life"
|
|
||||||
/>
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-green-500" icon={CalendarCheck2Icon}>
|
|
||||||
<Trans i18nKey="finalizeFeature" defaults="Finalize" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-teal-500" icon={CopyIcon}>
|
|
||||||
<Trans i18nKey="duplicateFeature" defaults="Duplicate" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-gray-700" icon={Settings2Icon}>
|
|
||||||
<Trans i18nKey="settings" defaults="Settings" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-pink-600" icon={HeartIcon}>
|
|
||||||
<Trans i18nKey="supportProject" defaults="Support this project" />
|
|
||||||
</Feature>
|
|
||||||
</ul>
|
|
||||||
<div className="grid gap-2.5">
|
<div className="grid gap-2.5">
|
||||||
<UpgradeButton annual={tab === "yearly"}>
|
<Button variant="primary" asChild>
|
||||||
|
<Link href="/settings/billing">
|
||||||
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
||||||
</UpgradeButton>
|
</Link>
|
||||||
|
</Button>
|
||||||
<Button asChild className="w-full">
|
<Button asChild className="w-full">
|
||||||
<Link href={`/poll/${params?.urlId as string}`}>
|
<Link href={`/poll/${params?.urlId as string}`}>
|
||||||
<Trans i18nKey="notToday" defaults="Not Today" />
|
<Trans i18nKey="notToday" defaults="Not Today" />
|
||||||
|
|
|
@ -15,9 +15,9 @@ const LabelWithIcon = ({
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<span className={cn("inline-flex items-center gap-2", className)}>
|
<span className={cn("inline-flex items-center gap-1.5", className)}>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4 -ml-0.5" />
|
||||||
<span>{children}</span>
|
<span className="font-medium">{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -54,11 +54,15 @@ export const PollStatusLabel = ({
|
||||||
export const PollStatusBadge = ({ status }: { status: PollStatus }) => {
|
export const PollStatusBadge = ({ status }: { status: PollStatus }) => {
|
||||||
return (
|
return (
|
||||||
<PollStatusLabel
|
<PollStatusLabel
|
||||||
className={cn("rounded-full border py-0.5 pl-1.5 pr-3 text-sm", {
|
className={cn(
|
||||||
"border-blue-500 text-blue-500": status === "live",
|
"rounded-md font-medium whitespace-nowrap border py-1 px-2 text-xs",
|
||||||
"border-gray-500 text-gray-500": status === "paused",
|
{
|
||||||
"border-green-500 text-green-500": status === "finalized",
|
"border-pink-200 bg-pink-50 text-pink-600": status === "live",
|
||||||
})}
|
"bg-gray-100 border-gray-200 text-gray-500": status === "paused",
|
||||||
|
"text-indigo-600 bg-indigo-50 border-indigo-200":
|
||||||
|
status === "finalized",
|
||||||
|
},
|
||||||
|
)}
|
||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
import { Card } from "@/components/card";
|
import { Card } from "@/components/card";
|
||||||
import Discussion from "@/components/discussion";
|
import Discussion from "@/components/discussion";
|
||||||
|
@ -32,7 +34,7 @@ export const Poll = () => {
|
||||||
const PollComponent = isWideScreen ? DesktopPoll : MobilePoll;
|
const PollComponent = isWideScreen ? DesktopPoll : MobilePoll;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-3 sm:space-y-4")}>
|
<div className={cn("space-y-3 sm:space-y-6")}>
|
||||||
<EventCard />
|
<EventCard />
|
||||||
<Card fullWidthOnMobile={false}>
|
<Card fullWidthOnMobile={false}>
|
||||||
<VotingForm>
|
<VotingForm>
|
||||||
|
@ -47,6 +49,23 @@ export const Poll = () => {
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<div className="mt-4 space-y-4 text-center text-gray-500">
|
||||||
|
<div className="py-8">
|
||||||
|
<Trans
|
||||||
|
defaults="Powered by <a>{name}</a>"
|
||||||
|
i18nKey="poweredByRallly"
|
||||||
|
values={{ name: "rallly.co" }}
|
||||||
|
components={{
|
||||||
|
a: (
|
||||||
|
<Link
|
||||||
|
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold"
|
||||||
|
href="https://rallly.co"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ const PollSubheader: React.FunctionComponent = () => {
|
||||||
const { poll } = usePoll();
|
const { poll } = usePoll();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500 text-sm">
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<div>
|
<div>
|
||||||
<Trans
|
<Trans
|
||||||
|
|
|
@ -1,31 +1,15 @@
|
||||||
|
"use client";
|
||||||
import { Badge } from "@rallly/ui/badge";
|
import { Badge } from "@rallly/ui/badge";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
|
|
||||||
import { usePlan } from "@/contexts/plan";
|
import { IfFreeUser } from "@/contexts/plan";
|
||||||
|
|
||||||
export const ProBadge = ({ className }: { className?: string }) => {
|
export const ProBadge = ({ className }: { className?: string }) => {
|
||||||
const isPaid = usePlan() === "paid";
|
|
||||||
if (isPaid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<IfFreeUser>
|
||||||
<TooltipTrigger asChild className="inline-flex" type="button">
|
|
||||||
<Link href="/settings/billing">
|
|
||||||
<Badge className={className}>
|
<Badge className={className}>
|
||||||
<Trans i18nKey="planPro" />
|
<Trans i18nKey="planPro" />
|
||||||
</Badge>
|
</Badge>
|
||||||
</Link>
|
</IfFreeUser>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<Trans
|
|
||||||
i18nKey="pleaseUpgrade"
|
|
||||||
defaults="Please upgrade to Pro to use this feature"
|
|
||||||
/>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,19 +3,19 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
export const Settings = ({ children }: React.PropsWithChildren) => {
|
export const Settings = ({ children }: React.PropsWithChildren) => {
|
||||||
return <div className="px-4 py-3 md:p-6">{children}</div>;
|
return <div className="">{children}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsHeader = ({ children }: React.PropsWithChildren) => {
|
export const SettingsHeader = ({ children }: React.PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 md:mb-8">
|
<div className="mb-4 font-semibold text-lg md:mb-8">
|
||||||
<h2>{children}</h2>
|
<h2>{children}</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsContent = ({ children }: React.PropsWithChildren) => {
|
export const SettingsContent = ({ children }: React.PropsWithChildren) => {
|
||||||
return <div className="space-y-8">{children}</div>;
|
return <div className="space-y-6">{children}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsSection = (props: {
|
export const SettingsSection = (props: {
|
||||||
|
@ -24,12 +24,12 @@ export const SettingsSection = (props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-3 md:gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-10 gap-3 md:gap-8">
|
||||||
<div>
|
<div className="col-span-3">
|
||||||
<h2 className="mb-1 text-base font-semibold">{props.title}</h2>
|
<h2 className="mb-1 text-base font-semibold">{props.title}</h2>
|
||||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>{props.children}</div>
|
<div className="col-span-7">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
OnChangeFn,
|
||||||
|
PaginationState,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { Trans } from "@/components/trans";
|
||||||
|
|
||||||
export const Table = <
|
export const Table = <
|
||||||
T extends Record<string, unknown>,
|
T extends Record<string, unknown>,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -15,23 +23,42 @@ export const Table = <
|
||||||
columns: C[];
|
columns: C[];
|
||||||
data: T[];
|
data: T[];
|
||||||
footer?: React.ReactNode;
|
footer?: React.ReactNode;
|
||||||
|
pageCount?: number;
|
||||||
enableTableFooter?: boolean;
|
enableTableFooter?: boolean;
|
||||||
|
enableTableHeader?: boolean;
|
||||||
layout?: "fixed" | "auto";
|
layout?: "fixed" | "auto";
|
||||||
|
onPaginationChange?: OnChangeFn<PaginationState>;
|
||||||
|
paginationState: PaginationState | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const table = useReactTable<T>({
|
const table = useReactTable<T>({
|
||||||
data: props.data,
|
data: props.data,
|
||||||
columns: props.columns,
|
columns: props.columns,
|
||||||
|
pageCount: props.pageCount,
|
||||||
|
state: {
|
||||||
|
pagination: props.paginationState,
|
||||||
|
},
|
||||||
|
manualPagination: true,
|
||||||
|
onPaginationChange: props.onPaginationChange,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(props.className, "max-w-full overflow-x-auto")}>
|
<div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
props.className,
|
||||||
|
"max-w-full overflow-x-auto scrollbar-thin",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<table
|
<table
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-collapse",
|
"border-collapse",
|
||||||
props.layout === "auto" ? "w-full table-auto" : "table-fixed",
|
props.layout === "auto" ? "w-full table-auto" : "table-fixed",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{props.enableTableHeader ? (
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
|
@ -41,7 +68,9 @@ export const Table = <
|
||||||
style={{
|
style={{
|
||||||
width: header.getSize(),
|
width: header.getSize(),
|
||||||
maxWidth:
|
maxWidth:
|
||||||
props.layout === "auto" ? header.getSize() : undefined,
|
props.layout === "auto"
|
||||||
|
? header.getSize()
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
className="whitespace-nowrap border-b border-gray-100 px-3 py-2.5 text-left align-bottom text-sm font-semibold"
|
className="whitespace-nowrap border-b border-gray-100 px-3 py-2.5 text-left align-bottom text-sm font-semibold"
|
||||||
>
|
>
|
||||||
|
@ -56,16 +85,25 @@ export const Table = <
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
|
) : null}
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((row, i) => (
|
{table.getRowModel().rows.map((row, i) => (
|
||||||
<tr key={row.id}>
|
<tr key={row.id}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td
|
<td
|
||||||
|
style={{
|
||||||
|
width: cell.column.getSize(),
|
||||||
|
maxWidth:
|
||||||
|
props.layout === "auto"
|
||||||
|
? cell.column.getSize()
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"overflow-hidden border-gray-100 px-3 py-2.5",
|
"overflow-hidden align-middle border-gray-100 pr-8 py-4",
|
||||||
{
|
{
|
||||||
"border-b": table.getRowModel().rows.length !== i + 1,
|
"border-b": table.getRowModel().rows.length !== i + 1,
|
||||||
|
"pt-0": !props.enableTableHeader && i === 0,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -78,7 +116,7 @@ export const Table = <
|
||||||
{props.enableTableFooter ? (
|
{props.enableTableFooter ? (
|
||||||
<tfoot>
|
<tfoot>
|
||||||
{table.getFooterGroups().map((footerGroup) => (
|
{table.getFooterGroups().map((footerGroup) => (
|
||||||
<tr key={footerGroup.id}>
|
<tr key={footerGroup.id} className="relative">
|
||||||
{footerGroup.headers.map((header) => (
|
{footerGroup.headers.map((header) => (
|
||||||
<th className="border-t bg-gray-50" key={header.id}>
|
<th className="border-t bg-gray-50" key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
|
@ -95,5 +133,41 @@ export const Table = <
|
||||||
) : null}
|
) : null}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<hr className="my-2" />
|
||||||
|
<div className="flex items-center justify-between space-x-2 py-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon
|
||||||
|
className={cn("h-4 w-4", {
|
||||||
|
"text-gray-400": !table.getCanPreviousPage(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
<Trans
|
||||||
|
i18nKey="pageXOfY"
|
||||||
|
defaults="Page {currentPage} of {pageCount}"
|
||||||
|
values={{
|
||||||
|
currentPage: table.getState().pagination.pageIndex + 1,
|
||||||
|
pageCount: table.getPageCount(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn("h-4 w-4", {
|
||||||
|
"text-gray-400": !table.getCanNextPage(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,10 +20,10 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="text"
|
type="text"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"appearance-none rounded border text-gray-800 placeholder:text-gray-500",
|
"appearance-none text-sm rounded border text-gray-800 placeholder:text-gray-500",
|
||||||
className,
|
className,
|
||||||
{
|
{
|
||||||
"px-2 py-1": size === "md",
|
"px-2.5 py-2": size === "md",
|
||||||
"px-3 py-2 text-xl": size === "lg",
|
"px-3 py-2 text-xl": size === "lg",
|
||||||
"input-error": error,
|
"input-error": error,
|
||||||
"bg-gray-50 text-gray-500": forwardProps.disabled,
|
"bg-gray-50 text-gray-500": forwardProps.disabled,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
@ -33,18 +35,21 @@ import { isFeedbackEnabled } from "@/utils/constants";
|
||||||
|
|
||||||
import { IfAuthenticated, IfGuest, useUser } from "./user-provider";
|
import { IfAuthenticated, IfGuest, useUser } from "./user-provider";
|
||||||
|
|
||||||
export const UserDropdown = () => {
|
export const UserDropdown = ({ className }: { className?: string }) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
return (
|
return (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
data-testid="user-dropdown"
|
data-testid="user-dropdown"
|
||||||
asChild
|
asChild
|
||||||
className="group"
|
className={cn("group min-w-0", className)}
|
||||||
>
|
>
|
||||||
<Button variant="ghost" className="rounded-full">
|
<Button variant="ghost" className="flex justify-between">
|
||||||
<CurrentUserAvatar size="sm" className="-ml-1" />
|
<span className="flex items-center gap-x-2.5">
|
||||||
<ChevronDown className="h-4 w-4" />
|
<CurrentUserAvatar size="sm" className="shrink-0 -ml-1 " />
|
||||||
|
<span className="truncate">{user.name}</span>
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
@ -62,7 +67,7 @@ export const UserDropdown = () => {
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild={true}>
|
<DropdownMenuItem asChild={true}>
|
||||||
<Link href="/polls" className="flex items-center gap-x-2 sm:hidden">
|
<Link href="/polls" className="flex items-center gap-x-2 sm:hidden">
|
||||||
<ListIcon className="h-4 w-4" />
|
<ListIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="polls" defaults="Polls" />
|
<Trans i18nKey="polls" defaults="Polls" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -72,7 +77,7 @@ export const UserDropdown = () => {
|
||||||
href="/settings/profile"
|
href="/settings/profile"
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2"
|
||||||
>
|
>
|
||||||
<UserIcon className="h-4 w-4" />
|
<UserIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="profile" defaults="Profile" />
|
<Trans i18nKey="profile" defaults="Profile" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -82,7 +87,7 @@ export const UserDropdown = () => {
|
||||||
href="/settings/preferences"
|
href="/settings/preferences"
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2"
|
||||||
>
|
>
|
||||||
<Settings2Icon className="h-4 w-4" />
|
<Settings2Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="preferences" defaults="Preferences" />
|
<Trans i18nKey="preferences" defaults="Preferences" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -92,7 +97,7 @@ export const UserDropdown = () => {
|
||||||
href="/settings/billing"
|
href="/settings/billing"
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2"
|
||||||
>
|
>
|
||||||
<CreditCardIcon className="h-4 w-4" />
|
<CreditCardIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="Billing" defaults="Billing" />
|
<Trans i18nKey="Billing" defaults="Billing" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -104,7 +109,7 @@ export const UserDropdown = () => {
|
||||||
href="https://support.rallly.co"
|
href="https://support.rallly.co"
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2"
|
||||||
>
|
>
|
||||||
<LifeBuoyIcon className="h-4 w-4" />
|
<LifeBuoyIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="support" defaults="Support" />
|
<Trans i18nKey="support" defaults="Support" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -115,7 +120,7 @@ export const UserDropdown = () => {
|
||||||
href="https://support.rallly.co/self-hosting/pricing"
|
href="https://support.rallly.co/self-hosting/pricing"
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2"
|
||||||
>
|
>
|
||||||
<GemIcon className="h-4 w-4" />
|
<GemIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="pricing" defaults="Pricing" />
|
<Trans i18nKey="pricing" defaults="Pricing" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -127,7 +132,7 @@ export const UserDropdown = () => {
|
||||||
href="https://feedback.rallly.co"
|
href="https://feedback.rallly.co"
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2"
|
||||||
>
|
>
|
||||||
<MegaphoneIcon className="h-4 w-4" />
|
<MegaphoneIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="feedback" defaults="Feedback" />
|
<Trans i18nKey="feedback" defaults="Feedback" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -136,13 +141,13 @@ export const UserDropdown = () => {
|
||||||
<IfGuest>
|
<IfGuest>
|
||||||
<DropdownMenuItem asChild={true}>
|
<DropdownMenuItem asChild={true}>
|
||||||
<LoginLink className="flex items-center gap-x-2">
|
<LoginLink className="flex items-center gap-x-2">
|
||||||
<LogInIcon className="h-4 w-4" />
|
<LogInIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="login" defaults="login" />
|
<Trans i18nKey="login" defaults="login" />
|
||||||
</LoginLink>
|
</LoginLink>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild={true}>
|
<DropdownMenuItem asChild={true}>
|
||||||
<RegisterLink className="flex items-center gap-x-2">
|
<RegisterLink className="flex items-center gap-x-2">
|
||||||
<UserPlusIcon className="h-4 w-4" />
|
<UserPlusIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="createAnAccount" defaults="Register" />
|
<Trans i18nKey="createAnAccount" defaults="Register" />
|
||||||
</RegisterLink>
|
</RegisterLink>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -153,7 +158,7 @@ export const UserDropdown = () => {
|
||||||
>
|
>
|
||||||
{/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
|
{/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
|
||||||
<a href="/logout">
|
<a href="/logout">
|
||||||
<RefreshCcwIcon className="h-4 w-4" />
|
<RefreshCcwIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="forgetMe" />
|
<Trans i18nKey="forgetMe" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -162,7 +167,7 @@ export const UserDropdown = () => {
|
||||||
<DropdownMenuItem asChild className="flex items-center gap-x-2">
|
<DropdownMenuItem asChild className="flex items-center gap-x-2">
|
||||||
{/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
|
{/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
|
||||||
<a href="/logout">
|
<a href="/logout">
|
||||||
<LogOutIcon className="h-4 w-4" />
|
<LogOutIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<Trans i18nKey="logout" />
|
<Trans i18nKey="logout" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { UserIcon } from "lucide-react";
|
import { UserIcon } from "lucide-react";
|
||||||
|
|
||||||
|
|
|
@ -44,9 +44,9 @@ export const IfSubscribed = ({ children }: React.PropsWithChildren) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IfFreeUser = ({ children }: React.PropsWithChildren) => {
|
export const IfFreeUser = ({ children }: React.PropsWithChildren) => {
|
||||||
const plan = usePlan();
|
const subscription = useSubscription();
|
||||||
|
|
||||||
return plan === "free" ? <>{children}</> : null;
|
return subscription?.active === false ? <>{children}</> : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Plan = () => {
|
export const Plan = () => {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import ErrorPage from "@/components/error-page";
|
import ErrorPage from "@/components/error-page";
|
||||||
import { getStandardLayout } from "@/components/layouts/standard-layout";
|
|
||||||
import { NextPageWithLayout } from "@/types";
|
import { NextPageWithLayout } from "@/types";
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||||
|
|
||||||
|
@ -18,8 +17,6 @@ const Custom404: NextPageWithLayout = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Custom404.getLayout = getStandardLayout;
|
|
||||||
|
|
||||||
export const getStaticProps = getStaticTranslations;
|
export const getStaticProps = getStaticTranslations;
|
||||||
|
|
||||||
export default Custom404;
|
export default Custom404;
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.redirect(303, "/logout");
|
res.status(404).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(403).redirect("/logout");
|
res.status(404).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,27 +7,14 @@
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply text-foreground overflow-y-scroll overscroll-none bg-gray-200/50;
|
@apply text-foreground bg-gray-100 overscroll-none;
|
||||||
font-feature-settings: "rlig" 1, "calt" 1;
|
font-feature-settings: "rlig" 1, "calt" 1;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
@apply h-full font-sans text-base text-gray-700;
|
@apply h-full font-sans text-base overflow-hidden text-gray-700;
|
||||||
}
|
}
|
||||||
body #__next {
|
body #__next {
|
||||||
@apply min-h-screen;
|
@apply h-full;
|
||||||
}
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5 {
|
|
||||||
@apply font-sans font-bold tracking-tight;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
@apply text-2xl;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
@apply text-xl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
@ -39,7 +26,7 @@
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
@apply rounded outline-none focus-visible:ring-2 focus-visible:ring-gray-300;
|
@apply rounded outline-none focus-visible:ring-1 focus:ring-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
#floating-ui-root {
|
#floating-ui-root {
|
||||||
|
@ -49,7 +36,7 @@
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.text-link {
|
.text-link {
|
||||||
@apply text-primary-600 hover:text-primary-600 focus-visible:ring-primary-600 rounded-md font-medium outline-none hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1;
|
@apply hover:text-gray-800 rounded-md outline-none underline focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1;
|
||||||
}
|
}
|
||||||
.formField {
|
.formField {
|
||||||
@apply mb-4;
|
@apply mb-4;
|
||||||
|
|
11
apps/web/src/utils/cookies.ts
Normal file
11
apps/web/src/utils/cookies.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
export function setCookie(key: string, value: string) {
|
||||||
|
Cookies.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function popCookie(key: string) {
|
||||||
|
const value = Cookies.get(key);
|
||||||
|
Cookies.remove(key);
|
||||||
|
return value;
|
||||||
|
}
|
|
@ -205,6 +205,15 @@ export const DayjsProvider: React.FunctionComponent<{
|
||||||
return await dayjsLocales[l].import();
|
return await dayjsLocales[l].import();
|
||||||
}, [l]);
|
}, [l]);
|
||||||
|
|
||||||
|
const preferredTimeZone = config?.timeZone ?? getBrowserTimeZone();
|
||||||
|
|
||||||
|
const adjustTimeZone = React.useCallback(
|
||||||
|
(date: dayjs.ConfigType, keepLocalTime = false) => {
|
||||||
|
return dayjs(date).tz(preferredTimeZone, keepLocalTime);
|
||||||
|
},
|
||||||
|
[preferredTimeZone],
|
||||||
|
);
|
||||||
|
|
||||||
if (!state.value) {
|
if (!state.value) {
|
||||||
// wait for locale to load before rendering
|
// wait for locale to load before rendering
|
||||||
return null;
|
return null;
|
||||||
|
@ -234,16 +243,10 @@ export const DayjsProvider: React.FunctionComponent<{
|
||||||
dayjs.locale(dayjsLocale);
|
dayjs.locale(dayjsLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferredTimeZone = config?.timeZone ?? getBrowserTimeZone();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DayjsContext.Provider
|
<DayjsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
adjustTimeZone: (date, keepLocalTime) => {
|
adjustTimeZone,
|
||||||
return keepLocalTime
|
|
||||||
? dayjs(date).utc()
|
|
||||||
: dayjs(date).tz(preferredTimeZone);
|
|
||||||
},
|
|
||||||
dayjs,
|
dayjs,
|
||||||
locale: localeConfig, // locale defaults
|
locale: localeConfig, // locale defaults
|
||||||
timeZone: preferredTimeZone,
|
timeZone: preferredTimeZone,
|
||||||
|
|
|
@ -123,8 +123,6 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
await page.waitForURL("/polls");
|
await page.waitForURL("/polls");
|
||||||
|
|
||||||
await page.getByTestId("user-dropdown").click();
|
|
||||||
|
|
||||||
await expect(page.getByText("Test User")).toBeVisible();
|
await expect(page.getByText("Test User")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -145,8 +143,6 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
await page.waitForURL("/polls");
|
await page.waitForURL("/polls");
|
||||||
|
|
||||||
await page.getByTestId("user-dropdown").click();
|
|
||||||
|
|
||||||
await expect(page.getByText("Test User")).toBeVisible();
|
await expect(page.getByText("Test User")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -167,8 +163,6 @@ test.describe.serial(() => {
|
||||||
|
|
||||||
await page.waitForURL("/polls");
|
await page.waitForURL("/polls");
|
||||||
|
|
||||||
await page.getByTestId("user-dropdown").click();
|
|
||||||
|
|
||||||
await expect(page.getByText("Test User")).toBeVisible();
|
await expect(page.getByText("Test User")).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import test, { expect } from "@playwright/test";
|
|
||||||
import { sealData } from "iron-session";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test checks that a legacy token can be used to sign in.
|
|
||||||
*/
|
|
||||||
test("should convert guest legacy token", async ({ browser }) => {
|
|
||||||
const context = await browser.newContext();
|
|
||||||
const guestLegacyToken = await sealData(
|
|
||||||
{
|
|
||||||
user: {
|
|
||||||
id: "user-1234",
|
|
||||||
isGuest: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
password: process.env.SECRET_PASSWORD,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await context.addCookies([
|
|
||||||
{
|
|
||||||
name: "rallly-session",
|
|
||||||
value: guestLegacyToken,
|
|
||||||
httpOnly: true,
|
|
||||||
url: process.env.NEXT_PUBLIC_BASE_URL,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const page = await context.newPage();
|
|
||||||
await page.goto("/polls");
|
|
||||||
await page.getByTestId("user-dropdown").click();
|
|
||||||
await expect(page.locator("text=user-1234")).toBeVisible();
|
|
||||||
});
|
|
|
@ -35,7 +35,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/nextjs": "^7.77.0",
|
"@sentry/nextjs": "^7.77.0",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
"next": "^14.0.1",
|
"next": "^14.0.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
|
|
|
@ -420,6 +420,77 @@ export const polls = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
paginatedList: possiblyPublicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
pagination: z.object({
|
||||||
|
pageIndex: z.number(),
|
||||||
|
pageSize: z.number(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const [total, rows] = await prisma.$transaction([
|
||||||
|
prisma.poll.count({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
deleted: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.poll.findMany({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
deleted: false,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
location: true,
|
||||||
|
createdAt: true,
|
||||||
|
timeZone: true,
|
||||||
|
adminUrlId: true,
|
||||||
|
participantUrlId: true,
|
||||||
|
status: true,
|
||||||
|
event: {
|
||||||
|
select: {
|
||||||
|
start: true,
|
||||||
|
duration: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
start: true,
|
||||||
|
duration: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
closed: true,
|
||||||
|
participants: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
{ name: "desc" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
{ title: "asc" },
|
||||||
|
],
|
||||||
|
skip: input.pagination.pageIndex * input.pagination.pageSize,
|
||||||
|
take: input.pagination.pageSize,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { total, rows };
|
||||||
|
}),
|
||||||
list: possiblyPublicProcedure.query(async ({ ctx }) => {
|
list: possiblyPublicProcedure.query(async ({ ctx }) => {
|
||||||
const polls = await prisma.poll.findMany({
|
const polls = await prisma.poll.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -32,7 +32,9 @@ const Alert = React.forwardRef<
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Icon ? <Icon className="mb-2 h-6 w-6" /> : null}
|
{Icon ? (
|
||||||
|
<Icon className="mb-2 -mt-1 h-6 w-6 text-muted-foreground" />
|
||||||
|
) : null}
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
@ -60,7 +62,10 @@ const AlertDescription = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
className={cn(
|
||||||
|
"text-sm text-muted-foreground [&_p]:leading-relaxed",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -8,7 +8,7 @@ const badgeVariants = cva(
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-transparent bg-primary text-primary-foreground",
|
default: "border-transparent bg-primary text-primary-50",
|
||||||
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground",
|
"border-transparent bg-destructive text-destructive-foreground",
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const BillingPlanHeader = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BillingPlanTitle = ({ children }: React.PropsWithChildren) => {
|
export const BillingPlanTitle = ({ children }: React.PropsWithChildren) => {
|
||||||
return <h3>{children}</h3>;
|
return <h3 className="font-semibold">{children}</h3>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BillingPlanDescription = ({
|
export const BillingPlanDescription = ({
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { SpinnerIcon } from "@rallly/icons";
|
import { Loader2Icon } from "lucide-react";
|
||||||
import { cva, VariantProps } from "class-variance-authority";
|
import { cva, VariantProps } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "./lib/utils";
|
import { cn } from "./lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex border font-medium disabled:text-muted-foreground focus:ring-1 focus:ring-gray-200 disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center whitespace-nowrap rounded-md border",
|
"inline-flex border font-medium disabled:text-muted-foreground focus:ring-1 focus:ring-gray-300 disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center whitespace-nowrap rounded-md border",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary:
|
primary:
|
||||||
"border-transparent bg-primary text-white shadow-sm hover:bg-primary-500 active:bg-primary-700",
|
"border-transparent bg-primary text-white focus:ring-offset-1 shadow-sm hover:bg-primary-500 active:bg-primary-700",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground active:bg-destructive hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground focus:ring-offset-1 active:bg-destructive border-destructive hover:bg-destructive/90",
|
||||||
default:
|
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",
|
"rounded-md px-3.5 py-2.5 data-[state=open]:shadow-none data-[state=open]:bg-gray-100 active:bg-gray-200 focus:border-gray-300 hover:bg-gray-100 bg-gray-50",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "border-transparent hover:bg-gray-200 active:bg-gray-300",
|
ghost: "border-transparent hover:bg-gray-200 active:bg-gray-300",
|
||||||
|
@ -77,9 +77,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<SpinnerIcon className="inline-block h-4 w-4 animate-spin" />
|
<Loader2Icon className="h-4 w-4 animate-spin" />
|
||||||
) : Icon ? (
|
) : Icon ? (
|
||||||
<Icon className="-ml-0.5 h-4 w-4" />
|
<Icon className={cn("-ml-0.5 h-4 w-4")} />
|
||||||
) : null}
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@radix-ui/react-switch": "^1.0.2",
|
"@radix-ui/react-switch": "^1.0.2",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
"@rallly/icons": "*",
|
"@rallly/icons": "*",
|
||||||
"@rallly/languages": "*",
|
"@rallly/languages": "*",
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
|
|
|
@ -80,7 +80,7 @@ const SelectItem = React.forwardRef<
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-2 pl-8 pr-2 text-sm outline-none focus:bg-gray-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
127
packages/ui/toast.tsx
Normal file
127
packages/ui/toast.tsx
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "./lib/utils";
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-4 sm:right-4 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg p-5 shadow-huge transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background/75 backdrop-blur-lg text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
));
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
};
|
34
packages/ui/toaster.tsx
Normal file
34
packages/ui/toaster.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "./toast";
|
||||||
|
import { useToast } from "./use-toast";
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider duration={2000}>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
);
|
||||||
|
}
|
189
packages/ui/use-toast.ts
Normal file
189
packages/ui/use-toast.ts
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import type { ToastActionElement, ToastProps } from "./toast";
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1;
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000;
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
action?: ToastActionElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||||
|
return count.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes;
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"];
|
||||||
|
toast: ToasterToast;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"];
|
||||||
|
toast: Partial<ToasterToast>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"];
|
||||||
|
toastId?: ToasterToast["id"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"];
|
||||||
|
toastId?: ToasterToast["id"];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId);
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
});
|
||||||
|
}, TOAST_REMOVE_DELAY);
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action;
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId);
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = [];
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] };
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action);
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId();
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
});
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState);
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState);
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast };
|
155
yarn.lock
155
yarn.lock
|
@ -2332,10 +2332,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
webpack-bundle-analyzer "4.3.0"
|
webpack-bundle-analyzer "4.3.0"
|
||||||
|
|
||||||
"@next/env@14.0.1":
|
"@next/env@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.1.tgz#7d03c9042c205a320aef2ea3f83c2d16b6825563"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
|
||||||
integrity sha512-Ms8ZswqY65/YfcjrlcIwMPD7Rg/dVjdLapMcSHG26W6O67EJDF435ShW4H4LXi1xKO1oRc97tLXUpx8jpLe86A==
|
integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
|
||||||
|
|
||||||
"@next/eslint-plugin-next@14.0.1":
|
"@next/eslint-plugin-next@14.0.1":
|
||||||
version "14.0.1"
|
version "14.0.1"
|
||||||
|
@ -2344,50 +2344,50 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
glob "7.1.7"
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@14.0.1":
|
"@next/swc-darwin-arm64@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.1.tgz#75a5f872c4077ecd536d7496bc24f3d312d5dcd0"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
|
||||||
integrity sha512-JyxnGCS4qT67hdOKQ0CkgFTp+PXub5W1wsGvIq98TNbF3YEIN7iDekYhYsZzc8Ov0pWEsghQt+tANdidITCLaw==
|
integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@14.0.1":
|
"@next/swc-darwin-x64@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.1.tgz#7d8498fc680cc8b4d5181bee336818c63779bc5e"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
|
||||||
integrity sha512-625Z7bb5AyIzswF9hvfZWa+HTwFZw+Jn3lOBNZB87lUS0iuCYDHqk3ujuHCkiyPtSC0xFBtYDLcrZ11mF/ap3w==
|
integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@14.0.1":
|
"@next/swc-linux-arm64-gnu@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.1.tgz#184286794e67bed192de7dbb10d7f040c996f965"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
|
||||||
integrity sha512-iVpn3KG3DprFXzVHM09kvb//4CNNXBQ9NB/pTm8LO+vnnnaObnzFdS5KM+w1okwa32xH0g8EvZIhoB3fI3mS1g==
|
integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@14.0.1":
|
"@next/swc-linux-arm64-musl@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.1.tgz#e8121b860ebc97a8d2a9113e5a42878430e749d5"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
|
||||||
integrity sha512-mVsGyMxTLWZXyD5sen6kGOTYVOO67lZjLApIj/JsTEEohDDt1im2nkspzfV5MvhfS7diDw6Rp/xvAQaWZTv1Ww==
|
integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@14.0.1":
|
"@next/swc-linux-x64-gnu@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.1.tgz#cdc4276b11a10c892fd1cb7dd31e024064db9c3b"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
|
||||||
integrity sha512-wMqf90uDWN001NqCM/auRl3+qVVeKfjJdT9XW+RMIOf+rhUzadmYJu++tp2y+hUbb6GTRhT+VjQzcgg/QTD9NQ==
|
integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@14.0.1":
|
"@next/swc-linux-x64-musl@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.1.tgz#4a194a484ceb34fd370e8d1af571493859fb2542"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
|
||||||
integrity sha512-ol1X1e24w4j4QwdeNjfX0f+Nza25n+ymY0T2frTyalVczUmzkVD7QGgPTZMHfR1aLrO69hBs0G3QBYaj22J5GQ==
|
integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@14.0.1":
|
"@next/swc-win32-arm64-msvc@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.1.tgz#71923debee50f98ef166b28cdb3ad7e7463e6598"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
|
||||||
integrity sha512-WEmTEeWs6yRUEnUlahTgvZteh5RJc4sEjCQIodJlZZ5/VJwVP8p2L7l6VhzQhT4h7KvLx/Ed4UViBdne6zpIsw==
|
integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@14.0.1":
|
"@next/swc-win32-ia32-msvc@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.1.tgz#b8f46da899c279fd65db76f0951849290c480ef9"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
|
||||||
integrity sha512-oFpHphN4ygAgZUKjzga7SoH2VGbEJXZa/KL8bHCAwCjDWle6R1SpiGOdUdA8EJ9YsG1TYWpzY6FTbUA+iAJeww==
|
integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@14.0.1":
|
"@next/swc-win32-x64-msvc@14.0.4":
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.1.tgz#be3dd8b3729ec51c99ff04b51e2b235756d02b6e"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
|
||||||
integrity sha512-FFp3nOJ/5qSpeWT0BZQ+YE1pSMk4IMpkME/1DwKBwhg4mJLB9L+6EXuJi4JEwaJdl5iN+UUlmUD3IsR1kx5fAg==
|
integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
|
@ -2802,6 +2802,18 @@
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||||
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer@1.0.5":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4"
|
||||||
|
integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||||
|
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/react-dropdown-menu@^2.0.4":
|
"@radix-ui/react-dropdown-menu@^2.0.4":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.4.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.4.tgz"
|
||||||
|
@ -2989,6 +3001,14 @@
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-primitive" "1.0.3"
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
|
||||||
|
"@radix-ui/react-portal@1.0.4":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
|
||||||
|
integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/react-presence@1.0.0":
|
"@radix-ui/react-presence@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz"
|
||||||
|
@ -3161,6 +3181,25 @@
|
||||||
"@radix-ui/react-roving-focus" "1.0.4"
|
"@radix-ui/react-roving-focus" "1.0.4"
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-toast@^1.1.4":
|
||||||
|
version "1.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6"
|
||||||
|
integrity sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-collection" "1.0.3"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||||
|
"@radix-ui/react-portal" "1.0.4"
|
||||||
|
"@radix-ui/react-presence" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||||
|
"@radix-ui/react-visually-hidden" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/react-tooltip@^1.0.6":
|
"@radix-ui/react-tooltip@^1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz"
|
||||||
|
@ -6786,7 +6825,7 @@ gopd@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic "^1.1.3"
|
get-intrinsic "^1.1.3"
|
||||||
|
|
||||||
graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.2.4:
|
graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.2.11, graceful-fs@^4.2.4:
|
||||||
version "4.2.11"
|
version "4.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
|
@ -8066,6 +8105,11 @@ lucide-react@^0.265.0:
|
||||||
resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.265.0.tgz"
|
resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.265.0.tgz"
|
||||||
integrity sha512-znyvziBEUQ7CKR31GiU4viomQbJrpDLG5ac+FajwiZIavC3YbPFLkzQx3dCXT4JWJx/pB34EwmtiZ0ElGZX0PA==
|
integrity sha512-znyvziBEUQ7CKR31GiU4viomQbJrpDLG5ac+FajwiZIavC3YbPFLkzQx3dCXT4JWJx/pB34EwmtiZ0ElGZX0PA==
|
||||||
|
|
||||||
|
lucide-react@^0.294.0:
|
||||||
|
version "0.294.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.294.0.tgz#dc406e1e7e2f722cf93218fe5b31cf3c95778817"
|
||||||
|
integrity sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==
|
||||||
|
|
||||||
luxon@^3.2.1:
|
luxon@^3.2.1:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz"
|
resolved "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz"
|
||||||
|
@ -8586,28 +8630,29 @@ next-seo@^6.1.0:
|
||||||
resolved "https://registry.npmjs.org/next-seo/-/next-seo-6.1.0.tgz"
|
resolved "https://registry.npmjs.org/next-seo/-/next-seo-6.1.0.tgz"
|
||||||
integrity sha512-iMBpFoJsR5zWhguHJvsoBDxDSmdYTHtnVPB1ij+CD0NReQCP78ZxxbdL9qkKIf4oEuZEqZkrjAQLB0bkII7RYA==
|
integrity sha512-iMBpFoJsR5zWhguHJvsoBDxDSmdYTHtnVPB1ij+CD0NReQCP78ZxxbdL9qkKIf4oEuZEqZkrjAQLB0bkII7RYA==
|
||||||
|
|
||||||
next@^14.0.1:
|
next@^14.0.4:
|
||||||
version "14.0.1"
|
version "14.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-14.0.1.tgz#1375d94c5dc7af730234af48401be270e975cb22"
|
resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
|
||||||
integrity sha512-s4YaLpE4b0gmb3ggtmpmV+wt+lPRuGtANzojMQ2+gmBpgX9w5fTbjsy6dXByBuENsdCX5pukZH/GxdFgO62+pA==
|
integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env" "14.0.1"
|
"@next/env" "14.0.4"
|
||||||
"@swc/helpers" "0.5.2"
|
"@swc/helpers" "0.5.2"
|
||||||
busboy "1.6.0"
|
busboy "1.6.0"
|
||||||
caniuse-lite "^1.0.30001406"
|
caniuse-lite "^1.0.30001406"
|
||||||
|
graceful-fs "^4.2.11"
|
||||||
postcss "8.4.31"
|
postcss "8.4.31"
|
||||||
styled-jsx "5.1.1"
|
styled-jsx "5.1.1"
|
||||||
watchpack "2.4.0"
|
watchpack "2.4.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@next/swc-darwin-arm64" "14.0.1"
|
"@next/swc-darwin-arm64" "14.0.4"
|
||||||
"@next/swc-darwin-x64" "14.0.1"
|
"@next/swc-darwin-x64" "14.0.4"
|
||||||
"@next/swc-linux-arm64-gnu" "14.0.1"
|
"@next/swc-linux-arm64-gnu" "14.0.4"
|
||||||
"@next/swc-linux-arm64-musl" "14.0.1"
|
"@next/swc-linux-arm64-musl" "14.0.4"
|
||||||
"@next/swc-linux-x64-gnu" "14.0.1"
|
"@next/swc-linux-x64-gnu" "14.0.4"
|
||||||
"@next/swc-linux-x64-musl" "14.0.1"
|
"@next/swc-linux-x64-musl" "14.0.4"
|
||||||
"@next/swc-win32-arm64-msvc" "14.0.1"
|
"@next/swc-win32-arm64-msvc" "14.0.4"
|
||||||
"@next/swc-win32-ia32-msvc" "14.0.1"
|
"@next/swc-win32-ia32-msvc" "14.0.4"
|
||||||
"@next/swc-win32-x64-msvc" "14.0.1"
|
"@next/swc-win32-x64-msvc" "14.0.4"
|
||||||
|
|
||||||
nice-try@^1.0.4:
|
nice-try@^1.0.4:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue