diff --git a/.vscode/settings.json b/.vscode/settings.json
index 13292350cef..ee40c449843 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
- "source.fixAll": true
+ "source.fixAll": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifier": "non-relative",
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index 557dcbf446f..7393ba932b4 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -52,13 +52,12 @@ const nextConfig = {
},
];
},
- sentry: {
- hideSourceMaps: false,
- },
};
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
// recommended:
// release, url, org, project, authToken, configFile, stripPrefix,
@@ -70,8 +69,9 @@ const sentryWebpackPluginOptions = {
// 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
// ensure that your source maps include changes from all other Webpack plugins
-module.exports = withSentryConfig(
- withBundleAnalyzer(nextConfig, sentryWebpackPluginOptions),
-);
+module.exports = process.env.SENTRY_AUTH_TOKEN
+ ? withSentryConfig(withBundleAnalyzerConfig, sentryWebpackPluginOptions)
+ : withBundleAnalyzerConfig;
diff --git a/apps/web/package.json b/apps/web/package.json
index cde55bf8343..16179216e4d 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -53,6 +53,7 @@
"iron-session": "^6.3.1",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
+ "lucide-react": "^0.294.0",
"micro": "^10.0.1",
"nanoid": "^4.0.0",
"next-auth": "^4.24.5",
diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json
index c0a93c15fb2..9636e9617ff 100644
--- a/apps/web/public/locales/en/app.json
+++ b/apps/web/public/locales/en/app.json
@@ -112,9 +112,6 @@
"dates": "Dates",
"menu": "Menu",
"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",
"billing": "Billing",
"guestPollAlertDescription": "<0>Create an account0> or <1>login1> to claim this poll.",
@@ -135,7 +132,6 @@
"permissionDenied": "Unauthorized",
"permissionDeniedDescription": "If you are the poll creator, please login to access your poll",
"loginDifferent": "Switch user",
- "share": "Share",
"timeShownIn": "Times shown in {timeZone}",
"editDetailsDescription": "Change the details of your event.",
"finalizeDescription": "Select a final date for your event.",
@@ -210,14 +206,7 @@
"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.",
"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",
- "pleaseUpgrade": "Please upgrade to Pro to use this feature",
"pollSettingsDescription": "Customize the behaviour of your poll",
"requireParticipantEmailLabel": "Make email address required for participants",
"hideParticipantsLabel": "Hide participant list from other participants",
@@ -226,8 +215,28 @@
"authErrorDescription": "There was an error logging you in. Please try again.",
"authErrorCta": "Go to login page",
"continueAs": "Continue as",
- "finalizeFeature": "Finalize",
- "duplicateFeature": "Duplicate",
"pageMovedDescription": "Redirecting to {newUrl} ",
- "notRegistered": "Don't have an account? Register "
+ "notRegistered": "Don't have an account? Register ",
+ "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 in0> to your existing account or <1>create a new account1> 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."
}
diff --git a/apps/web/src/app/[locale]/(admin)/layout.tsx b/apps/web/src/app/[locale]/(admin)/layout.tsx
index 5f7fa20fbc6..e1138eddcc4 100644
--- a/apps/web/src/app/[locale]/(admin)/layout.tsx
+++ b/apps/web/src/app/[locale]/(admin)/layout.tsx
@@ -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 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";
const Auth = ({ children }: { children: React.ReactNode }) => {
@@ -22,13 +27,57 @@ const Auth = ({ children }: { children: React.ReactNode }) => {
return null;
};
-export default function Layout({ children }: { children: React.ReactNode }) {
+function MobileNavigation() {
+ return (
+
+ );
+}
+
+export default async function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ function SidebarLayout() {
+ return (
+
+ );
+ }
+
if (isSelfHosted) {
return (
- {children}
+
);
}
- return {children} ;
+ return ;
}
diff --git a/apps/web/src/app/[locale]/(admin)/menu-item.tsx b/apps/web/src/app/[locale]/(admin)/menu-item.tsx
new file mode 100644
index 00000000000..9235b17ad90
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/menu-item.tsx
@@ -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 (
+
+
+ {children}
+
+ );
+}
diff --git a/apps/web/src/app/[locale]/(admin)/new/loading.tsx b/apps/web/src/app/[locale]/(admin)/new/loading.tsx
new file mode 100644
index 00000000000..4349ac3a619
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/new/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/new/page.tsx b/apps/web/src/app/[locale]/(admin)/new/page.tsx
index 9d80dd37306..dbc65689cdf 100644
--- a/apps/web/src/app/[locale]/(admin)/new/page.tsx
+++ b/apps/web/src/app/[locale]/(admin)/new/page.tsx
@@ -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 { CreatePoll } from "@/components/create-poll";
-export default function Page() {
- return ;
+export default async function Page({ params }: { params: { locale: string } }) {
+ const { t } = await getTranslation(params.locale);
+ return (
+
+
+
+
+
+
+
+
+ );
}
export async function generateMetadata({
diff --git a/apps/web/src/app/[locale]/(admin)/polls/page.tsx b/apps/web/src/app/[locale]/(admin)/polls/page.tsx
index d6257a7fd4f..207a4af57da 100644
--- a/apps/web/src/app/[locale]/(admin)/polls/page.tsx
+++ b/apps/web/src/app/[locale]/(admin)/polls/page.tsx
@@ -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 { PollsPage } from "./polls-page";
+import { PollsList } from "./polls-list";
-export default function Page() {
- return ;
+export default async function Page({ params }: { params: { locale: string } }) {
+ const { t } = await getTranslation(params.locale);
+ return (
+
+
+
+
+
+
+
+
+ );
}
export async function generateMetadata({
diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx
new file mode 100644
index 00000000000..c616de3e156
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx
@@ -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 (
+
+ {children}
+
+ );
+}
+
+export function PollFolders() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx
new file mode 100644
index 00000000000..6c4d636ca8e
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx
@@ -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 (
+
+ );
+};
+
+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();
+
+export function PollsList() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const pathname = usePathname();
+ const pagination = React.useMemo(
+ () => ({
+ 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 (
+
+
+
+ {row.original.title}
+
+
+
+ {row.original.event ? (
+
+ {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")}`}
+
+ ) : (
+
+
+
+ )}
+
+ );
+ },
+ }),
+ columnHelper.accessor("status", {
+ header: () => null,
+ size: 200,
+ cell: ({ row }) => {
+ return (
+
+ );
+ },
+ }),
+ columnHelper.accessor("createdAt", {
+ header: () => null,
+ size: 1000,
+ cell: ({ row }) => {
+ const { createdAt } = row.original;
+ return (
+
+
+
+
+
+ );
+ },
+ }),
+ columnHelper.accessor("participants", {
+ header: () => null,
+ cell: ({ row }) => {
+ return (
+
+
+
+
+ {row.original.participants.length}
+
+
+
+ {row.original.participants.length > 0 ? (
+ <>
+ {row.original.participants
+ .slice(0, 10)
+ .map((participant, i) => (
+ {participant.name}
+ ))}
+ {row.original.participants.length > 10 ? (
+
+
+
+ ) : null}
+ >
+ ) : (
+
+ )}
+
+
+ );
+ },
+ }),
+ ],
+ [adjustTimeZone],
+ );
+
+ if (!data) return null;
+
+ if (data.total === 0) return ;
+
+ return (
+ {
+ 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}
+ />
+ );
+}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-page.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-page.tsx
deleted file mode 100644
index 435fd582a56..00000000000
--- a/apps/web/src/app/[locale]/(admin)/polls/polls-page.tsx
+++ /dev/null
@@ -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 (
-
- );
-};
-
-export function PollsPage() {
- const { data } = trpc.polls.list.useQuery();
- const { adjustTimeZone } = useDayjs();
-
- return (
-
-
- } icon={VoteIcon} />
-
-
-
-
- {data ? (
- data.length > 0 ? (
-
- {data.map((poll) => {
- const { title, id: pollId, createdAt, status } = poll;
- return (
-
-
-
-
- {poll.event ? (
-
- ) : (
-
- {status === "live" ? (
-
- ) : (
-
- )}
-
- )}
-
-
-
- {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}
-
-
-
- {title}
-
-
-
-
-
- {poll.participants.length > 0 ? (
-
- ) : null}
-
-
-
-
-
- );
- })}
-
- ) : (
-
- )
- ) : (
-
-
-
-
-
-
- )}
-
-
-
- );
-}
diff --git a/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx b/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx
index fa22ab1f843..3ffba9035e6 100644
--- a/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx
+++ b/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx
@@ -13,7 +13,6 @@ import { BillingPlans } from "@/components/billing/billing-plans";
import {
Settings,
SettingsContent,
- SettingsHeader,
SettingsSection,
} from "@/components/settings/settings";
import { Trans } from "@/components/trans";
@@ -239,9 +238,6 @@ export function BillingPage() {
return (
-
-
-
{t("billing")}
@@ -257,6 +253,7 @@ export function BillingPage() {
>
+
}
description={
diff --git a/apps/web/src/app/[locale]/(admin)/settings/billing/loading.tsx b/apps/web/src/app/[locale]/(admin)/settings/billing/loading.tsx
new file mode 100644
index 00000000000..4349ac3a619
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/settings/billing/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx
index 486b021f83b..255920bbac3 100644
--- a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx
+++ b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx
@@ -1,10 +1,61 @@
-"use client";
-import { ProfileLayout } from "@/components/layouts/profile-layout";
+import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react";
+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: React.ReactNode;
-}) {
- return {children} ;
+ params,
+}: React.PropsWithChildren<{
+ params: { locale: string };
+}>) {
+ 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 (
+
+
+
+
+
+
+
+
+ {children}
+
+
+ );
}
diff --git a/apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx b/apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx
new file mode 100644
index 00000000000..81a019660cc
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx
@@ -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 (
+
+ {props.children}
+
+ );
+}
+
+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 (
+ <>
+
+ {menuItems.map((item, i) => (
+
+
+ {item.title}
+
+ ))}
+
+ {
+ const item = menuItems.find((item) => item.title === value);
+ if (item) {
+ router.push(item.href);
+ }
+ }}
+ >
+
+
+
+
+ {menuItems.map((item, i) => (
+
+
+
+ {item.title}
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/[locale]/(admin)/settings/preferences/loading.tsx b/apps/web/src/app/[locale]/(admin)/settings/preferences/loading.tsx
new file mode 100644
index 00000000000..4349ac3a619
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/settings/preferences/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx b/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx
index 66c4288ccea..a6c6cc1b765 100644
--- a/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx
+++ b/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx
@@ -7,7 +7,6 @@ import { LanguagePreference } from "@/components/settings/language-preference";
import {
Settings,
SettingsContent,
- SettingsHeader,
SettingsSection,
} from "@/components/settings/settings";
import { Trans } from "@/components/trans";
@@ -17,9 +16,6 @@ export function PreferencesPage() {
return (
-
-
-
{t("settings")}
@@ -35,6 +31,7 @@ export function PreferencesPage() {
>
+
}
description={
diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/loading.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/loading.tsx
new file mode 100644
index 00000000000..4349ac3a619
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/settings/profile/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx
index a972b3d3a36..a3817410414 100644
--- a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx
+++ b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-page.tsx
@@ -1,13 +1,19 @@
"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 Link from "next/link";
import { useTranslation } from "next-i18next";
+import { LogoutButton } from "@/app/components/logout-button";
import { ProfileSettings } from "@/components/settings/profile-settings";
import {
Settings,
- SettingsHeader,
+ SettingsContent,
SettingsSection,
} from "@/components/settings/settings";
+import { TextInput } from "@/components/text-input";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
@@ -15,54 +21,78 @@ export const ProfilePage = () => {
const { t } = useTranslation();
const { user } = useUser();
- if (user.isGuest) {
- return null;
- }
return (
{t("profile")}
-
-
-
- }
- description={
-
- }
- >
-
-
- {/* }
- description={
-
- }
- >
-
- */}
- {/* }
- description={
-
- }
- >
-
-
-
- */}
+ {user.isGuest ? (
+
+ }
+ description={ }
+ >
+
+
+
+
+
+
+
+
+
+ ,
+ ,
+ ]}
+ />
+
+
+
+
+
+
+
+
+ ) : (
+
+ }
+ description={
+
+ }
+ >
+
+
+
+
+ }
+ description={
+
+ }
+ >
+
+
+
+
+
+
+ )}
);
};
diff --git a/apps/web/src/app/[locale]/(admin)/sidebar.tsx b/apps/web/src/app/[locale]/(admin)/sidebar.tsx
new file mode 100644
index 00000000000..f1c82c4b4a0
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/sidebar.tsx
@@ -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 (
+
+
+ {children}
+
+ );
+}
+
+export function Sidebar() {
+ const pathname = usePathname();
+ const { user } = useUser();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {user.name}
+
+ {user.email}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/[locale]/logout/route.ts b/apps/web/src/app/[locale]/auth/logout/route.ts
similarity index 65%
rename from apps/web/src/app/[locale]/logout/route.ts
rename to apps/web/src/app/[locale]/auth/logout/route.ts
index aec0ccd4f43..7e55c2e1e62 100644
--- a/apps/web/src/app/[locale]/logout/route.ts
+++ b/apps/web/src/app/[locale]/auth/logout/route.ts
@@ -3,8 +3,8 @@ import { NextResponse } from "next/server";
import { resetUser } from "@/app/guest";
import { absoluteUrl } from "@/utils/absolute-url";
-export async function GET() {
- const res = NextResponse.redirect(absoluteUrl());
+export async function POST() {
+ const res = NextResponse.redirect(absoluteUrl("/login"), 302);
await resetUser(res);
return res;
}
diff --git a/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx b/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx
index 5c63f677622..066964b670f 100644
--- a/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx
+++ b/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx
@@ -6,34 +6,7 @@ import { getTranslation } from "@/app/i18n";
import { absoluteUrl } from "@/utils/absolute-url";
export default function Layout({ children }: { children: React.ReactNode }) {
- return (
-
-
-
-
-
-
-
-
-
-
{children}
-
- );
+ return <>{children}>;
}
export async function generateMetadata({
diff --git a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx
index 90de9860de7..1f496b1f5a5 100644
--- a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx
+++ b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx
@@ -6,6 +6,7 @@ import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
import React from "react";
+import { PageHeader } from "@/app/components/page-layout";
import { Poll } from "@/components/poll";
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
import { Trans } from "@/components/trans";
@@ -61,23 +62,25 @@ const GoToApp = () => {
const { user } = useUser();
return (
-
-
+
);
};
@@ -87,23 +90,10 @@ export default function InvitePage() {
-
-
-
-
-
- ),
- }}
- />
+
diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx
index c5895dd6ab0..9d017b346fa 100644
--- a/apps/web/src/app/[locale]/layout.tsx
+++ b/apps/web/src/app/[locale]/layout.tsx
@@ -2,6 +2,7 @@ import "tailwindcss/tailwind.css";
import "../../style.css";
import languages from "@rallly/languages";
+import { Toaster } from "@rallly/ui/toaster";
import { Inter } from "next/font/google";
import React from "react";
@@ -26,6 +27,7 @@ export default function Root({
return (
+
{children}
diff --git a/apps/web/src/app/[locale]/menu/back-button.tsx b/apps/web/src/app/[locale]/menu/back-button.tsx
new file mode 100644
index 00000000000..b6f91669a99
--- /dev/null
+++ b/apps/web/src/app/[locale]/menu/back-button.tsx
@@ -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 (
+
{
+ router.back();
+ }}
+ >
+
+
+ );
+}
diff --git a/apps/web/src/app/[locale]/menu/page.tsx b/apps/web/src/app/[locale]/menu/page.tsx
new file mode 100644
index 00000000000..a537bf5fa9c
--- /dev/null
+++ b/apps/web/src/app/[locale]/menu/page.tsx
@@ -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 (
+
+ );
+}
diff --git a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/duplicate/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/duplicate/page.tsx
similarity index 100%
rename from apps/web/src/app/[locale]/(admin)/poll/[urlId]/duplicate/page.tsx
rename to apps/web/src/app/[locale]/poll/[urlId]/duplicate/page.tsx
diff --git a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/edit-details/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/edit-details/page.tsx
similarity index 100%
rename from apps/web/src/app/[locale]/(admin)/poll/[urlId]/edit-details/page.tsx
rename to apps/web/src/app/[locale]/poll/[urlId]/edit-details/page.tsx
diff --git a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/edit-options/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx
similarity index 100%
rename from apps/web/src/app/[locale]/(admin)/poll/[urlId]/edit-options/page.tsx
rename to apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx
diff --git a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/edit-settings/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/edit-settings/page.tsx
similarity index 100%
rename from apps/web/src/app/[locale]/(admin)/poll/[urlId]/edit-settings/page.tsx
rename to apps/web/src/app/[locale]/poll/[urlId]/edit-settings/page.tsx
diff --git a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/finalize/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/finalize/page.tsx
similarity index 100%
rename from apps/web/src/app/[locale]/(admin)/poll/[urlId]/finalize/page.tsx
rename to apps/web/src/app/[locale]/poll/[urlId]/finalize/page.tsx
diff --git a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/layout.tsx b/apps/web/src/app/[locale]/poll/[urlId]/layout.tsx
similarity index 100%
rename from apps/web/src/app/[locale]/(admin)/poll/[urlId]/layout.tsx
rename to apps/web/src/app/[locale]/poll/[urlId]/layout.tsx
diff --git a/apps/web/src/app/[locale]/poll/[urlId]/loading.tsx b/apps/web/src/app/[locale]/poll/[urlId]/loading.tsx
new file mode 100644
index 00000000000..4349ac3a619
--- /dev/null
+++ b/apps/web/src/app/[locale]/poll/[urlId]/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/page.tsx
similarity index 81%
rename from apps/web/src/app/[locale]/(admin)/poll/[urlId]/page.tsx
rename to apps/web/src/app/[locale]/poll/[urlId]/page.tsx
index 3159f05faaa..cc89f1bc875 100644
--- a/apps/web/src/app/[locale]/(admin)/poll/[urlId]/page.tsx
+++ b/apps/web/src/app/[locale]/poll/[urlId]/page.tsx
@@ -35,10 +35,10 @@ const GuestPollAlert = () => {
defaults="<0>Create an account0> or <1>login1> to claim this poll."
components={[
,
-
,
+
,
]}
/>
@@ -48,9 +48,11 @@ const GuestPollAlert = () => {
export default function Page() {
return (
-
-
-
+
);
}
diff --git a/apps/web/src/app/components/logo-link.tsx b/apps/web/src/app/components/logo-link.tsx
new file mode 100644
index 00000000000..bbf374a5efa
--- /dev/null
+++ b/apps/web/src/app/components/logo-link.tsx
@@ -0,0 +1,20 @@
+import Image from "next/image";
+import Link from "next/link";
+
+export function LogoLink() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/app/components/logout-button.tsx b/apps/web/src/app/components/logout-button.tsx
new file mode 100644
index 00000000000..5b3613c6dc3
--- /dev/null
+++ b/apps/web/src/app/components/logout-button.tsx
@@ -0,0 +1,14 @@
+import { Button, ButtonProps } from "@rallly/ui/button";
+
+export function LogoutButton({
+ children,
+ ...rest
+}: React.PropsWithChildren
) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/components/page-layout.tsx b/apps/web/src/app/components/page-layout.tsx
new file mode 100644
index 00000000000..c5f5bce2ad0
--- /dev/null
+++ b/apps/web/src/app/components/page-layout.tsx
@@ -0,0 +1,57 @@
+"use client";
+import { cn } from "@rallly/ui";
+
+export function PageContainer({
+ children,
+ className,
+}: React.PropsWithChildren<{ className?: string }>) {
+ return {children}
;
+}
+
+export function PageTitle({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function PageHeader({
+ children,
+ className,
+ variant = "default",
+}: {
+ children?: React.ReactNode;
+ className?: string;
+ variant?: "default" | "ghost";
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function PageContent({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return {children}
;
+}
diff --git a/apps/web/src/components/auth/auth-forms.tsx b/apps/web/src/components/auth/auth-forms.tsx
index 05f7bdb7cf3..2a3f2983569 100644
--- a/apps/web/src/components/auth/auth-forms.tsx
+++ b/apps/web/src/components/auth/auth-forms.tsx
@@ -44,7 +44,7 @@ export const VerifyCode: React.FunctionComponent<{
})}
>
- {t("verifyYourEmail")}
+ {t("verifyYourEmail")}
{t("stepSummary", {
current: 2,
@@ -60,6 +60,7 @@ export const VerifyCode: React.FunctionComponent<{
b:
,
a: (
{
diff --git a/apps/web/src/components/billing/billing-plans.tsx b/apps/web/src/components/billing/billing-plans.tsx
index f791f65cf65..d0281bc4f17 100644
--- a/apps/web/src/components/billing/billing-plans.tsx
+++ b/apps/web/src/components/billing/billing-plans.tsx
@@ -71,9 +71,9 @@ export const BillingPlans = () => {
-
+
) => {
return (
-
- {children}
-
+ {children}
);
};
diff --git a/apps/web/src/components/create-poll.tsx b/apps/web/src/components/create-poll.tsx
index fe3732e1ea5..9cfdae897ed 100644
--- a/apps/web/src/components/create-poll.tsx
+++ b/apps/web/src/components/create-poll.tsx
@@ -16,6 +16,7 @@ import { useUnmount } from "react-use";
import { PollSettingsForm } from "@/components/forms/poll-settings";
import { Trans } from "@/components/trans";
+import { setCookie } from "@/utils/cookies";
import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc/client";
@@ -62,12 +63,16 @@ export const CreatePoll: React.FunctionComponent = () => {
const posthog = usePostHog();
const queryClient = trpc.useUtils();
- const createPoll = trpc.polls.create.useMutation();
+ const createPoll = trpc.polls.create.useMutation({
+ networkMode: "always",
+ onSuccess: () => {
+ setCookie("new-poll", "1");
+ },
+ });
return (